Everything here assumes an account is a harness session. A CI bot, a build
machine or a long-running daemon needs an account too, and none of them has a
session, a model or a parent. They are not agents and they still need somewhere
to live.
The first instinct is a two-way split, ephemeral against permanent. That is the
wrong axis, and relaxing the pruning default is what shows it: if a session's
DID stays resolvable for months anyway, both kinds are long-lived and the split
distinguishes nothing. Three things vary independently instead.
A CI identity is asserted, renewing, kept. A session is issued, admitted once,
expiring. A cloud machine is asserted and renewing, and its retention is
whatever that machine's lifetime turns out to be — cloud instances are
disposable too, and sometimes they are not. So the kind picks defaults and the
account carries the values, rather than the kind deciding behaviour directly.
pinned is derived. It was a boolean on
AgentAccount standing in for a
retention policy, and once retention is a value a pin is one of its
settings — two mechanisms for one decision, and the one that lost would
still be the one some caller was reading. pinned is a method over
Retention::Forever now: AccountStore takes a retention rather than a
boolean, unpinning names the kind's own default rather than clearing a
flag, and a log written before this replays its boolean into the
retention it meant. AccountState
(provisioning/active/frozen/soft-deleted, see
agent-accounts 's Done section) is the lifecycle
axis this epic asked for and stays its own field, independent of
retention — a frozen or soft-deleted account can still be pinned or not,
and the two answer different questions.
Decide whether a deployment may provision without an owner. It may
not silently. owner is a constructor argument on Provisioner and
required by the registration schema, so a deployment that cannot name
one does not compile — and the development server, which carries
did:web:owner.invalid so that trying the stack does not require a
human DID, warns at startup that nobody is accountable for the accounts
the run mints. Saying it in a doc comment beside the constant was not
the deployment saying it; the person looking for somebody accountable is
reading the log.
Reserved names: a third state in the registry. NameRegistry
now holds Live, Released(when) and Reserved(Reservation): a
reserved name is unclaimable by the namer before any account holds it,
survives the account it was reserved for, and never ages out —
prune and snapshot treat it the way the ledger's own state is
treated, not the way a hold's cache entry is. The reason is an open
enum rather than a boolean, because "reserved" and "reserved because a
zone lives there" are different answers to an operator debugging a
failed provisioning; a zone apex and an operational block are the two
reasons today. zone-scale 's ZoneManager is what
actually reserves a zone's apex, as a side effect of creating the zone
rather than a separate step an operator could forget — see that epic
for the write-up of the inverse direction (a zone cannot be created
where an agent already lives) and the open question of whether a zone
may be created at a name still inside its release hold.
One lexicon describes an account, and it is the registration.
bot.did.profile is retired. Its presentation half was
always app.bsky.actor.profile's job and that record is written already;
its harness and agent type were admission facts and are the registration
record's agent member; its parent was the weaker of two copies of the
same claim, and the server's own is the one the index reads now. What
went nowhere is the model and the record switch, deliberately: a model
changes every turn and is stamped on the records a turn writes, and the
switch is enforced at the write path and answered from the account, so a
stored copy could only ever disagree with the store it was copied from.
The second write is gone with it. putAgentProfile existed because
a session's transcript has no assistant row when SessionStart fires, so
the model arrived one request later; the hook kept a note of what it had
already published to avoid a round trip per tool call. With no
account-level model there is nothing to refresh, so the route, the state
it needed and the PreToolUse call all went. This closes
provenance 's "carry the model without a second write"
by removing the write rather than by finding a cheaper one.
actor.registration is the record every repository is born holding ,
composed from the ledger as its predecessor was, and agent.identity and
agent.attestation are gone. The attestation lexicon folded in rather
than being replaced: what stays true of an admission — which backend, how
much it proved, which node — is part of what the server registered. The
attest crate stopped stamping a $type on its Provenance at the same
time, because that value is facts the server stores and was never written
into a repository.
An owner is required, and it is a DID. An account nobody is
answerable for defeats the one property a stranger can act on, so the
schema refuses it rather than leaving it to a server to remember. It is a
constructor argument rather than a builder for the same reason: a
deployment that cannot name an owner does not compile, let alone
register anything. The development server carries a default that resolves
to nothing and says so, because refusing to start would make the flag
mandatory before anybody could try the stack.
The admission facts moved into the ledger. The record is composed
from the bookkeeping and from nothing else — that is what makes the
mirror checkable — so a fact the record carries has to be a fact the
bookkeeping holds. Provisioned gained the harness, the agent type and
the parent, defaulted on the way in so a log written before they existed
still replays. generation is counted from the same ledger rather than
stored, because a second copy is a second thing to be wrong.
The node is published, as the one the account was admitted on. It was
held back before, on the grounds that a node identifier links otherwise
unrelated agents to one machine. That is a real property and not one to
solve by omission for a deployment run by its own operator. What the
field must not do is claim to be current, so it is named for admission:
an agent resumed on another machine has not made the record wrong.
The agent member requires nothing of its own. It required a harness
when the lexicon was drafted, which was wrong twice: an agent is the kind
defined by what it does not have, and every fact a harness reports is
one it may have failed to read. The union still does the work it was
chosen for on the two kinds with real requirements.
The bot.did.registration lexicon, with the kind as an
open union. One record for every kind of account, which is what keeps
revision, handles and keys from being written twice. The per-kind
fields sit in a union member rather than behind a kind string, because
a lexicon cannot make one field's presence depend on another's value and
a union member can declare its own required — so an agent without a
harness, a pipeline without a binding and a host without an identifier
are each refused by the validator rather than by prose. The union is open:
the server is the only writer, so there is no hostile variant to refuse,
and closing it would mean every reader had to be upgraded before any
deployment could mint a new kind. $type is the only discriminator; a
kind string beside it would be a second one to disagree with.
Called a registration, because that is what it is. The identity is
the DID and the keys in its document; this record is the server's account
of admitting an account and keeping it, and naming it identity would
both misdescribe it and spend the one name a credential-bearing record
should have. registration is not a human-signup word in this
neighbourhood: SPIRE calls the server-side workload entries registration
entries, DNS has registries and registrars with no human in the loop, and
this plan already said "a registration and lifecycle event feed" in
agent-accounts before the record existed. The trait
that provisions is Registry. enrollment was the runner-up and leans
further towards devices; it matched nothing already written here.
Only admission facts and bookkeeping, never present state. A record
that claims permanence may not carry a field that describes what an
account is doing now: the model changes every turn, the effort with it,
and the machine changes when a session resumes elsewhere or a container
is rescheduled. So the node is recorded as the one an account was
admitted on , which stays true forever, and the harness as the one that
asked for the account. Anything genuinely current is either stamped on
the records a turn writes or is a question for the server, and anything
the server must attest across an account's life is a history array, which
is what handles and keys already are.
Three kinds drawn from what the credentials actually say , rather than
from how the accounts feel: an agent, a continuous integration job, and a
host. Writing the fields out is what separated the last two — a job is
bound to a workflow definition and a host to a machine, and before the
fields existed they looked like one kind.
A binding is an issuer and a stable subject, and nothing else.
Everything else a credential carries changes per presentation, so it is
provenance on the records that presentation wrote. The subject must be
the platform's stable identifier rather than its readable one: every
platform here issues both, a binding on the readable one breaks silently
at the next rename, and the failure looks like an admission failure.
Where the platform offers an audience, not setting one is what lets a
token minted for somebody else be replayed here — and a credential format
with no audience and no expiry, which is what a detached-signature
instance identity document is, has to be bounded by the seen-nonce store
instead.
The verified claims are carried, and the credential is not. A
permanent record is the wrong place for a proof that expires, and
inlining one would grow the record on every rotation. The old
agent.attestation field for raw evidence does not come across.