id: labels title: What is true about an agent arrives as a label an app already draws status: open crates: [didbot-pds, didbot-identity, didbot-serve, didbot-lexicon] dependsOn: [pds-writes, ownership] exitCriterion: > The operator subscribes to this server in a client, and that client draws, on an agent's account, that it is automated and whose it is — from labels the server signed as its own first account and served itself. #
labels #
Everything this system knows about an agent is a record in a repository. A reader who wants any of it has to know the collection, fetch it, and decide how to draw it. Nothing does that. A label is the one statement about an account that existing clients already render without being taught: a subscriber's client puts it on the profile and on every post.
So this epic buys reach, not assurance. The facts do not become truer for being
signed a second time — a label from a service we run is our claim, exactly as
bot.did.registration is. What changes is that somebody else's
software shows it to somebody who never asked us anything.
The server is its own first account #
The whole thing is one stack: the server holds the accounts, is an account itself, signs the labels, and serves them. The operator subscribes to it the way they would subscribe to anyone's labeler, and their ordinary client draws what this deployment knows about the agents it minted.
That resolves the earlier open question about whose key signs. Not the operator's personal account, and not a separate service beside the index — the server's own DID, which already exists and is already reserved.
For a deployment at foo.example, all five of these hold at once, and four are
already built:
foo.exampleis the PDS. The URL an operator types.foo.exampleis a DID this server manages and no account can take.did:web:foo.exampleis whatZone::service_didreturns, anddid_documentanswers for it before it scans accounts. The reservation is structural rather than a blocklist: an agent hostname is always a label plus the zone, so it is strictly below the zone and never equal to it.foo.exampleis the labeler. Two additions to that document, an#atproto_labelerservice entry and an#atproto_labelverification method.- Agents are its immediate subdomains.
Zone::newin did.rs is exactly this layout, and is the constructor a delegated subzone is the variant of. - One certificate covers the apex and
*.foo.example. Both names are needed, because a wildcard does not match the bare domain it is under.
What is missing is the account. The apex DID today has an empty
verificationMethod and no alsoKnownAs: it holds no key, no repository and
no handle, because nothing has needed it to. A labeler needs a declaration
record at self, which needs a repository, which needs a signing key.
What this shape gives up, and why that is acceptable here #
Recorded because it is a real cost and the decision was to pay it.
- The apex goes from holding no keys to holding two, a repository signing key and a label signing key, on the host that already holds the custodial agent keys. deployment separates hosts so that a compromise is bounded; this puts one more thing inside the same boundary. It is bounded by what that host could already do: whoever holds it can be any agent under the zone, so being able to sign labels about them too is not a new class of lie.
did:webhas no audit log. Whoever serves the apexdid.jsoncan replace the label signing key, and a subscriber cannot tell it ever changed. Adid:plckeeps a rotation history a stranger can audit. This is the argument for the ecosystem labeler below being adid:plc, and it is not an argument for this one: a reader subscribing to a deployment's labeler is already trusting that deployment for the accounts themselves.- A flat namespace leaves the DNS credential nothing to be scoped to.
Deployment says to narrow it to the subzone. With agents at
*.foo.examplethere is no subzone, so the credential that mints agent names can also repoint the apex, its document and therefore the labeler's key. Delegatingagents.foo.exampleis what buys that back, and it is available whenever it is worth the second name. - One certificate for both names is a choice, not a requirement. The rule
that a wildcard must not shadow the server's own name still holds —
*.foo.examplecannot servefoo.example. Putting the apex in the same subject-alternative-name list hands that separation back. A second issuance for the apex keeps it, and costs one certificate.
Why not the operator's own account #
Asked and answered no. An operator can make their personal DID a labeler: a
did:plc gets the service entry and the label verification method through
requestPlcOperationSignature, signPlcOperation and submitPlcOperation,
which their own server signs for them. The reasons not to are what that turns
their account into.
- They acquire a moderation inbox.
app.bsky.labeler.serviceis also a report destination, and clients offer it to subscribers. Declarable away with an explicit emptyreasonTypes— empty is distinct from absent, which means every reason — but the default is to receive reports nobody is staffing. - Their profile becomes a service. A labeler DID renders as a moderation service with a subscribe control rather than as a person.
- One labeler per DID. One service entry, one declaration record, one vocabulary, for as long as they hold the identity.
- The DID document change is in the PLC audit log permanently, and every label they ever sign points at that DID as its source.
- It only carries their authority if they hold the key. If this stack holds it, or if the endpoint in their document points at our host, then the operator's labeler is our claim wearing their name — the asymmetry policy-store exists to protect.
The vocabulary is the constraint, not the plumbing #
The work #
Later, and separately: an ecosystem labeler #
One service saying that an account is automated, subscribable by anyone,
across deployments. It should not be this server's apex: it is not this zone's
server, and naming it after this zone claims a scope it does not have. Its own
did:plc, for the audit log and because it is the resolution case every client
already handles. Who operates an agent needs no label value of its own — a
label carries its signer, so a statement from any operator's labeler already
says whose the agent is, and a global service would otherwise need a declared
value per operator that no client has a string to draw for.
Not started, and not started until the stack above works end to end.
Later, and separately: a public blocklist of every agent under did.bot #
The labeler says what an account is. It does not do anything about it — these
labels are inform, they blur nothing, and a reader who has decided they want
no automated accounts in their replies still has to act on each one by hand.
The thing that answers that is a moderation list: one record per account,
subscribed to once, applied by the subscriber's own client as a block.
Offering it is the point of offering it. A server that mints agent accounts onto a public network and leaves everyone else to notice them one at a time is handing its costs to strangers. One subscription covering every account this project has ever minted is the smallest honest version of the opt-out, and it is a list somebody else's software already knows how to consume.
- A list, not a label.
app.bsky.graph.listwith amodlistpurpose, oneapp.bsky.graph.listitemper account, and the subscriber's ownapp.bsky.graph.listblockdoing the blocking at their end. Nothing here blocks anybody. The list is our claim about which accounts these are; the block is the reader's decision about it, and stays revocable by them. - Its own
did:plc, and not this deployment's apex, for the reason the ecosystem labeler wants one:pds.did.botis a single zone and the claim is about did.bot as a whole, so naming it after one deployment claims a scope it does not have and leaves the key rotation unauditable. - Being listed is not an accusation. Every account on it is here because it
is automated and this project minted it, which is the same fact the
botself-label and the registration record already state. Nothing is inferred about behaviour, and there is no path onto the list that runs through somebody reporting an account. - It is not e-stop. Halting an agent is something the operator does to their own accounts. This changes nothing on this side and takes effect only in the client of a reader who chose to subscribe.