id: did-minting title: The server mints the identifier, not its caller status: open crates: [didbot-pds, didbot-identity] dependsOn: [agent-accounts] exitCriterion: > A provisioning request carries no identifier and gets an account whose DID the server chose; two requests carrying the same correlation key get the same account; and a DID whose account has been deleted is never minted again. #
did-minting #
A did:web DID is a hostname, and this server takes that hostname from
whoever asked for the account.
ProvisionRequest::agent_id is
documented as "the DNS label the DID will be minted from", and
Provisioner::provision passes it straight to AgentDid::mint. The server
checks two things about it: that it is a legal DNS label
(validate_label — lowercase ASCII,
digits, hyphens, no hyphen at either end) and that the minted host sits at or
below the zone. Neither of those is a claim about identity.
Everything else about the identifier is the caller's decision. That is the wrong side of the boundary for the one value in this system that can never be changed afterwards.
What follows from it #
A harness may not have an identifier to give. A top-level session has no
agent_id of its own, and a client papers over it by deriving a label out of
whatever session bookkeeping it holds, because the harness does not promise
the format. That is a good workaround living in the wrong place — it is one
client's convention, not a rule the server enforces, and a second client is
free to do something else.
Nothing makes the identifier globally unique. derive_agent_id renders 40
bits of an FNV-1a digest, and its own documentation puts the collision odds at
roughly one in twenty thousand for ten thousand agents on one server. It
argues that is acceptable because a collision is an availability problem: the
second provisioning fails against a DID that already exists. That argument
holds only for one server and only while both accounts are live.
A deleted DID can be minted again. The duplicate check is
self.store.get(&did).is_some() — live accounts only. Names get a hold
(DEFAULT_HOLD, thirty days) precisely so
a recycled name cannot point at a different agent. DIDs get nothing, even
though a DID is the thing at:// URIs and signatures are written against.
Provisioner::remove already knows this: it forgets the commit history
because "a later account minted at the same DID [would] inherit a head naming
blocks nothing holds". It defends the block store against the scenario and
leaves the identity alone.
The caller chooses what its permanent public name says. Nothing stops a request minting a label that reads as somebody else's agent, or one that carries harness-internal detail — a ticket id, a path, a username — into a name that is published in DNS, served in a DID document, and never revocable.
These are four symptoms of one thing: the identifier is an input where it should be an output.
The shape of the fix #
The server mints. A request carries whatever correlation the caller has — possibly nothing — and that value is used to answer "does this context already have an account", never to name it.
What this is not #
Not a change to attestation. What authorizes a provisioning is node's and attestation's question, and it stays theirs; this epic is only about who chooses the name once a request is admitted.
Not zone-scale. That epic counts names against a finite pool and records against a provider's quota. This one is about where a single name comes from, and a wider mint space makes zone-scale's arithmetic slightly worse rather than better.
Done #
Nothing yet.