--- 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`](../crates/didbot-pds/src/provision.rs) 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`](../crates/didbot-identity/src/did.rs) — 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`](../crates/didbot-pds/src/names.rs), 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. - [ ] **Split the identifier from the correlation key.** `ProvisionRequest` grows a `correlation: Option` that is opaque to identity, and loses `agent_id`. Nothing about the key reaches the DID, so it needs no label rules and can be as long, as structured or as absent as a harness requires. This is a breaking change to the provisioning surface and to every caller of it — `didbot-pds`'s `--demo` is the one in this repository — so it lands as a `!` commit. - [ ] **Choose what the server mints from.** [`didbot_name::generated`](../crates/didbot-name/src/generated.rs) already ships `Uuid`, `Random`, `Counter` and `Timestamp` with their disclosure properties written down; the machinery exists and is wired to handles rather than to DIDs. A random label of sufficient width makes reuse negligible rather than a policy to enforce, which is the argument for it over a counter — but a counter is durable and auditable, and this is a decision to make explicitly rather than inherit. Which zone it is minted *from* is [name-pools](name-pools.md)'s question. - [ ] **Keep provisioning idempotent.** Today `agent_id` doubles as the idempotency key: re-provisioning the same session hits the duplicate refusal. Once the server mints, the store needs a durable correlation-key-to-DID index to answer the same question, and a request with no correlation key at all has to mean "a new account, always". - [ ] **Decide what a deleted DID becomes.** [name-pools](name-pools.md) answers this — the DID pool is never reclaimed from, so a deleted account frees the account and not the name — and this item is the part that lands here: a permanent ledger of minted DIDs, consulted before every mint. `Provisioner::remove`'s comment about a later account inheriting a stale head stops being a defence against something the design allows and becomes a note about something it forbids. - [ ] **Say what happens to already-minted DIDs.** They are permanent by definition, so this cannot be a migration that renames them. The epic exits with old accounts untouched and new ones minted server-side. - [ ] **Take a name hint without letting it fail.** A caller may say what it would like to be called; how that is resolved, reserved and reported back is [name-pools](name-pools.md)'s, but the request type is this epic's: `correlation` is not a hint and a hint is not an identifier. - [ ] **Keep the readable prefix a server affordance.** Once the server mints, a client has nothing to derive. The slug — the readable prefix that let an operator match a DID to a session by eye — is a real affordance and should be something the server attaches, not the identifier itself. ## What this is not Not a change to attestation. What authorizes a provisioning is [node](node.md)'s and [attestation](attestation.md)'s question, and it stays theirs; this epic is only about who chooses the name once a request is admitted. Not [zone-scale](zone-scale.md). 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.