From 21d35b76fee7797d627a1cc5947adda69b856494 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 28 Aug 2026 09:53:20 -0400 Subject: [PATCH] docs(labels): make the server its own first account and labeler Records the decided shape: the apex did:web is the account, the labeler and the service that writes the labels, with the operator subscribing to it. Says what that concentration costs, and why an owner's personal DID is not it. Change-Id: I5bf7ccdd51dceb6410fdaf92b18a3440d7dbf9e3 --- plan/labels.md | 232 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 153 insertions(+), 79 deletions(-) diff --git a/plan/labels.md b/plan/labels.md index 51510c87..5603f552 100644 --- a/plan/labels.md +++ b/plan/labels.md @@ -2,12 +2,12 @@ id: labels title: What is true about an agent arrives as a label an app already draws status: open -crates: [vibescrobble-index, vibescrobble-query, vibescrobble-serve, vibescrobble-lexicon] -dependsOn: [ownership, index] +crates: [vibescrobble-pds, vibescrobble-identity, vibescrobble-serve, vibescrobble-lexicon] +dependsOn: [pds-writes, ownership] exitCriterion: > - A reader who has never heard of this project subscribes to a labeler here, - and their client draws, on an agent's account, that it is automated and whose - it is — from labels the reader verified against a key we did not hand them. + 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 @@ -23,80 +23,154 @@ signed a second time — a label from a service we run is our claim, exactly as `zone.quernstone.agent.identity` is. What changes is that somebody else's software shows it to somebody who never asked us anything. -## The two labelers - -**One global, saying that an account is automated.** An agent account looks -like a person's account to every client in the network, and that is the fact -worth broadcasting first. Anybody may subscribe, including people with no -relationship to this deployment. - -**One per owner, saying whatever that owner wants said about their own -agents** — model, family, the repository it works in, the swarm it belongs to. -Subscribing to it is opting into one owner's vocabulary, which is the right -shape: nobody else has to carry it. - -Ownership falls out of the split rather than needing a label value of its own. -A label carries its signer in `src`, so a statement from an owner's labeler -already says whose the agent is. A global service that tried to say the same -thing would need a declared value per owner, edited into a record every time -somebody new arrives, and no client would have a string to draw for it. - -- [ ] **Settle the vocabulary before publishing one.** A label value is a short - token from a set declared in advance, with a locale string per value. It - is not a field and it carries no payload: "which repository this agent - works in" is one declared value per repository, forever, in a record that - readers have already subscribed to. Whatever the owner labeler ends up - offering has to survive that, and "arbitrary labels on your own bots" does - not survive it as stated. Decide which facts are a small closed set - (agent type, model family, harness) and which stay records - ([provenance](provenance.md) is where they live now). -- [ ] **Check that the ecosystem will subscribe to us at all**, before - anything else here. A labeler is discovered through a service entry in its - DID document and a declaration record in its repository; every account - here is `did:web`, nothing federates yet, and no relay carries these - repositories. If a mainstream client will not resolve a `did:web` labeler, - the render payoff is a demonstration against our own reader until - federation lands. This is a day's work to find out and it gates the rest. -- [ ] **Whose key signs the owner labeler.** If this stack holds it, the - "owner's labeler" is our claim wearing the owner's name, which is the - asymmetry [policy-store](policy-store.md) exists to protect. Either the - owner signs — the same posture as the site that writes their policy — or - the service is named for what it is, an operator labeler reporting what - the owner told us. -- [ ] **Where the service runs: the control plane, not the personal data - server.** Three reasons and one consequence. The facts are already - computed on the index side, from the firehose and the vouch chain, and a - labeler on the server would recompute them. A label signing key must not - sit beside the custodial agent keys and the zone credential - ([deployment](../docs/deployment.md) separates those hosts on purpose). - And the server is the party a reader is auditing; a labeler is the - reader's instrument. The consequence is that the labeler *account* can - still be hosted here — it is an ordinary account with a declaration record - — while the *service*, its endpoint and its key live with the index. -- [ ] **Emit from the index rather than at provisioning.** One subscription, - one derivation, one place where a label and the record it came from can - disagree and be caught. -- [ ] **The account-lifetime problem.** Agent accounts are ephemeral and the - label sequence is append-only and permanent. A thousand accounts a week - that each live an hour is a stream nobody wants to replay. Set `exp` from - the account's expected life so readers age labels out without a - retraction, decide whether a deleted account is negated or left to expire, - and say what the sequence looks like after a year. -- [ ] **Retraction, and how it reaches a reader.** A wrong label is negated, - not deleted, and a client that is not subscribed at that moment learns on - its next backfill. Whatever [e-stop](e-stop.md) halts, it does not unsay - a label that has already been sequenced. -- [ ] **Serve both label endpoints, and be honest about which.** Query answers - a reader asking about specific accounts; subscribe is the live sequence a - client backfills from with a cursor. The firehose work in - [index](index.md) is the closest thing already built, and the cursor - semantics are not the same. -- [ ] **Say what a label is not.** These are informational, not moderation - signals: nothing here should ask a client to blur or hide an account, and - the declaration record has to be written so it does not. +## 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 owner'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.example` is the PDS.** The URL an operator types. +- **`foo.example` is a DID this server manages and no account can take.** + `did:web:foo.example` is what `Zone::service_did` returns, and + [`did_document`](../crates/vibescrobble-pds/src/provision.rs) answers 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.example` is the labeler.** Two additions to that document, an + `#atproto_labeler` service entry and an `#atproto_label` verification method. +- **Agents are its immediate subdomains.** `Zone::new` in + [did.rs](../crates/vibescrobble-identity/src/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](../docs/deployment.md) 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:web` has no audit log.** Whoever serves the apex `did.json` can + replace the label signing key, and a subscriber cannot tell it ever changed. + A `did:plc` keeps a rotation history a stranger can audit. This is the + argument for the ecosystem labeler below being a `did: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.example` + there is no subzone, so the credential that mints agent names can also + repoint the apex, its document and therefore the labeler's key. Delegating + `agents.foo.example` is 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.example` cannot serve `foo.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 owner's own account + +Asked and answered no. An owner *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.service` is also a + report destination, and clients offer it to subscribers. Declarable away with + an explicit empty `reasonTypes` — 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 owner's + labeler is our claim wearing their name — the asymmetry + [policy-store](policy-store.md) exists to protect. + +## The vocabulary is the constraint, not the plumbing + +- [ ] **Settle what gets a value before publishing one.** A label value is a + short token (`[a-z-]`, declared with a locale string per value) from a set + written into a record subscribers already hold. It is not a field and it + carries no payload: "which repository this agent works in" is one declared + value per repository, forever. Decide which facts are a small closed set — + agent type, model family, harness — and which stay records in + [provenance](provenance.md), which is where they live now. +- [ ] **"This account is automated" may need no labeler at all.** A `bot` + self-label in each agent's `app.bsky.actor.profile` says it to every + client with no subscription and no service, and that is the cheaper half. + Settle whether the labeler's job is only what a self-label cannot do — + a third party saying whose the agent is — before building for the rest. +- [ ] **Say what a label is not.** These are informational: severity `inform`, + blurs `none`, and nothing here asks a client to hide an account. + +## The work + +- [ ] **The server becomes an account.** A repository at the apex DID, a + signing key, a handle it claims and answers for at + `/.well-known/atproto-did`, and the `app.bsky.labeler.service` + declaration at `self`. Everything else here waits on it. +- [ ] **The two service entries and the label key** in the document + `for_service` builds, which today carries one service and no keys. +- [ ] **Sign and sequence labels.** DAG-CBOR signed with the label key, an + append-only sequence, `neg` for retraction rather than deletion. +- [ ] **Serve both endpoints.** Query answers a reader asking about named + accounts; subscribe is the live sequence a client backfills with a + cursor. The firehose in [index](index.md) is the closest thing built and + the cursor semantics are not the same. +- [ ] **Emit from the ledger the accounts already live in**, so a label and the + record it came from have one source and can be checked against each other. +- [ ] **The account-lifetime problem.** Agent accounts are ephemeral and a + label sequence is append-only and permanent. Set `exp` from the account's + expected life so readers age labels out without a retraction, decide + whether a deleted account is negated or left to expire, and say what the + sequence looks like after a year. +- [ ] **Check what will resolve a `did:web` labeler**, and treat the answer as + information rather than a gate. Nothing here federates yet and no relay + carries these repositories, so the first subscriber is our own reader + either way; whether a mainstream client will also resolve it decides how + much of this is visible outside the deployment. +- [ ] **Retraction reaches a reader on their next backfill, and no sooner.** + Whatever [e-stop](e-stop.md) halts, it does not unsay a label already + sequenced. + +## 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. Ownership needs no label value of its own — a label carries +its signer, so a statement from a per-owner or per-operator labeler already +says whose the agent is, and a global service would otherwise need a declared +value per owner that no client has a string to draw for. + +Not started, and not started until the stack above works end to end. ## Done -Nothing. The epic is a decision first — the vocabulary, the key, and whether -anything in the wider network will resolve a `did:web` labeler — and no code -should land before those three are answered. +Nothing. Four of the five properties the layout needs are built, which is why +this is written as one epic rather than a proposal: what is missing is the +server's own account, not the naming. -- 2.51.2