3 OpenSpec 30 agent identity pool lifecycle
Larandar edited this page 2026-09-04 00:40:39 +02:00

OpenSpec 30 — Agent identity pool lifecycle

Status: proposed
Version: 0.1.2
Sessions: Ting/Vedanta#30 — bring-up · Ting/Vedanta#31 — checkout and return
Epic: Ting/Ting#2
Projects from: OpenSpec 61 v0.1.4 (delivered)

Proposal

Vedanta owns the durable drone pool: it provisions a fixed set of reusable principals, allocates one to a session, and takes it back afterwards so that the next occupant inherits nothing. From outside, a persona outlives any single container; inside, no two occupancies of one slot can reach each other.

This package is the implementation of a contract that is already ratified. OpenSpec 61 settles what the lifecycle guarantees; this settles what Vedanta builds to make those guarantees true, and nothing here may weaken them.

Authority

61 is delivered and is not reopened. Its Session is closed and its guarantees are ratified; carrying implementation detail into it would blur the split between what the estate promises and what one service does about it, and would re-ratify a document this work does not change. Ruled on Ting/Ting#2 Q1, 2026-09-03.

The clauses this package implements are 61's amendment 0.1.2 (session completion preserves the worker) and the ratified reconciliation of 2026-08-30, which is the authoritative production reading of Epic #2. Everything in #2's body and its 2026-08-09 comments is provenance: where they prescribe a different credential path — a queen-startup batch of (identity, token) pairs, direct Vedanta access, LLDAP minting, a shared runtime token, env-file delivery — they are superseded and are not instructions.

Vedanta is the implementer. Epic #2 describes the pool manager as "a new, 100%-code service, parallel to the steward … NOT queen's responsibility and NOT Jostoph's either", written before the service was named. It denotes Vedanta; flake-ops#96 carries the naming and the division in one line — "Vedanta mints and allocates; Jostoph rotates and disables." Allocation is checkout. This has been misread once, so it is stated here rather than left to inference.

Design

The pool

Ten principals, provisioned once, UUID-pinned, durable. A principal is not created per container and not destroyed at session end. Membership makes a slot eligible; it does not allocate it, and it confers no Forgejo account, no standing Forge token, and no active lease.

A name is reserved before the principal is createdswarm-alpha-drone-<word>-<word>-<word>-<word> — and the returned UUID is pinned to that name and slot with no rename pass. The ordering is the point: a create-then-name flow has a window in which a principal exists under a name nothing reserved, and a retry across that window is how one slot becomes two principals. A retry therefore adopts matching name/slot metadata, and on collision denies or re-reserves rather than duplicating.

The two boundaries

Bring-up and the steady-state cycle are separate Sessions, ruled at #2 Q3. They differ in what a reviewer must decide and in what a mistake costs: bring-up is a one-time act over the whole pool, gated on a human; the cycle runs continuously over one slot at a time. They are not one coherent PR.

Checkout

Allocation binds a slot to a session under a new immutable AssignmentGrant, inheriting no lease, token, or authority from any previous occupancy. It stamps the occupancy onto the slot's OpenBao entity alias.

The stamp is custom_metadata, not a claim, and the reason is structural. A kanidm claim map is valuesByGroup, so a claim can carry only what the directory asserts about an account. Which occupancy is calling changes per allocation and has no group to hang on — one group per assignment would leak the pool's churn into the directory. Alias metadata is rewritten from claim mappings on every login; custom_metadata is written out of band and a login does not touch it, which is exactly the lifetime this value needs. Its consumer is written and under review at flake-ops#471, and that path shape is the acceptance.

A neighbouring name is not this value. The private contract's assignment_id (QueenAuthorityRef) is the queen's tenure assignment that authorized a request — one value shared by every drone under a queen. It cannot separate two occupants of one slot, which is this stamp's whole job. A path built on it looks isolated and is not.

Return

Ordinary completion revokes the session capability, confirms negative runtime liveness, rotates the checkout credential, advances the generation, and returns the same worker-<UUID> slot to the pool. The principal survives. Deletion, suppression, tombstoning and recreation are reserved for compromise or irrecoverable retirement, and are a different path with a different authority.

Freeing a slot

A slot is freed by an explicit release and by nothing else. No timeout frees a slot, no staleness rule frees a slot, no unreachable or silent runtime frees a slot, and no absence of evidence frees a slot.

The contract requires availability to rest on negative liveness — evidence the previous holder is gone. The mechanism that would supply that evidence is deliberately undefined: ruled on Ting/Ting#2 Q2 (2026-09-04), "until we have the problem a few times we will not know."

Rather than carry that as a gap, the package takes the shape that needs no mechanism at all. Nothing infers liveness, so there is nothing to get wrong. The contract's requirement is satisfied trivially and conservatively: a slot never becomes available on inference, because inference does not exist here.

The failure this chooses, named rather than discovered. A drone that dies without releasing holds its slot indefinitely; ten such deaths exhaust the pool, allocation refuses, and the swarm cannot start. That is loud, bounded, non-destructive, and recoverable by an explicit release — and it produces exactly the incident record a real liveness mechanism must be designed against. The alternative, an automatic release resting on an unvalidated mechanism, fails silently, and its failure is two live drones sharing one identity: unbounded, and found late.

Held slots are drift Vedanta observes and cannot resolve, so they are raised under the Vedanta#11 mandate rather than accumulating quietly.

What returns later. The liveness source, as an amendment against evidence. It will arrive as a new way to release — not as a change to the rule that only a release frees a slot, which is why nothing here needs revisiting to accommodate it.

Recovery: an empty memory is not an empty pool

A slot is available only where durable state says so. Never because the process has no record of it.

This is the same guarantee as freeing a slot, one level down. That rule is stated per transition — nothing infers a slot is free — and a restart does not take a transition: a cycle that comes back with an empty map never inferred anything, it simply never knew. The guarantee therefore has to hold over the initial state as well, or it does not hold at all, and the failure it lets through is the one the whole design exists to prevent: one identity handed to two live drones.

So occupancy is durable. The holder — session, assignment, grant, generation — is written before the slot is handed out and cleared after the credential is dead, and startup reconstructs each slot from that record.

Ordering, in both directions, errs toward held. The write precedes the stamp, so a process that dies anywhere after it leaves a slot recorded as held for an occupancy that may never have gone live. That is the recoverable side: an operator releases it, and the cost is one slot out of ten. The alternative — recording after the hand-out — leaves a window where a live drone holds a slot nothing remembers, and the cost there is unbounded.

A refusal is not a crash, and the two are treated differently on purpose. When the stamp is refused, nothing was handed out and nothing is live, so the record is compensated immediately rather than left for a human. An interrupted process cannot reach that compensation, which is precisely why the write comes first.

Unreadable state denies; it does not default. "I do not know what is held" and "nothing is held" are different answers and only one is safe. A ledger that cannot be read fails the constructor, so a cycle that does not know what it holds cannot be built and therefore cannot allocate — a structural refusal rather than a check someone can forget to write. A torn tail is tolerated, since the last append may not have completed; a torn line anywhere else means the file is not what it claims.

Ratification is presence, not channel

The realized name/UUID/grant-ID/version/digest mapping is presented for human ratification before activation. Per #2 Q4: any surface qualifies so long as a freeholder is demonstrably at a keyboard and acting, and none qualifies on delivery alone. Activation blocks on a freeholder act, never on a notification having been sent. An alert raised into a plane nobody is obliged to read is not ratification; neither is a message delivered to an absent human. The alert plane may carry the notice and cannot constitute the ratification.

Specification Delta

Requirement: bring-up creates eligibility, never an active credential

Scenario: the pool is provisioned

  • GIVEN an empty pool and a declared swarm-alpha/queen seat
  • WHEN bring-up completes and is ratified
  • THEN ten UUID-pinned principals exist, each carrying one distinct immutable grant
  • AND no Forgejo account, standing Forge token, or active lease was created

Requirement: a name is reserved before a principal exists

Scenario: provisioning is retried mid-flight

  • GIVEN a reserved name whose principal creation may already have succeeded
  • WHEN provisioning retries
  • THEN it adopts the principal matching that name and slot
  • AND no second principal is created for the slot
  • AND a mismatched collision is denied or re-reserved

Requirement: activation waits for a person

Scenario: the mapping is realized but unratified

  • GIVEN a realized name/UUID/grant mapping
  • WHEN no freeholder has acted on it
  • THEN the pool is not active and no slot may be allocated
  • AND delivery of a notice does not by itself satisfy this

Requirement: a returned slot carries nothing forward

Scenario: a slot is reused by a later session

  • GIVEN a slot returned by ordinary completion
  • WHEN it is allocated again
  • THEN the new session holds a new immutable grant
  • AND no lease, token, or authority from the previous session remains valid

Requirement: occupancy is what isolates, not the slot

Scenario: two successive occupants of one slot

  • GIVEN a slot allocated, returned, and allocated again
  • WHEN the second occupant reads its workspace and report paths
  • THEN it cannot reach the first occupant's workspace or reports

Requirement: the principal survives ordinary completion

Scenario: a session ends normally

  • GIVEN a drone session completing without compromise
  • WHEN return runs
  • THEN the session capability, leases, and resource tokens are revoked
  • AND the durable principal is neither deleted, suppressed, nor tombstoned

Requirement: a restart does not free a held slot

Scenario: the process restarts while a drone holds a slot

  • GIVEN a slot bound to a live session
  • WHEN the cycle restarts
  • THEN the slot is still held, by the same session, assignment and generation
  • AND it is not offered to a new session

Scenario: the durable record cannot be read

  • GIVEN occupancy state that is unreadable or damaged
  • WHEN the cycle starts
  • THEN it does not start with every slot available
  • AND allocation is refused

Requirement: a live holder blocks reissue

Scenario: liveness cannot be shown negative

  • GIVEN a slot whose previous holder cannot be shown to be gone
  • WHEN allocation is attempted
  • THEN the slot is not reissued

Tasks

  1. Bring-up (#30)
    1. Reserve the four-word name before creation; pin the UUID to name and slot without renaming.
    2. Provision exactly ten principals, idempotently under retry; adopt on match, deny or re-reserve on collision.
    3. Issue one distinct immutable AssignmentGrant per slot, delegated to the swarm-alpha/queen seat.
    4. Present the realized mapping and block activation on a freeholder act.
    5. Prove bring-up creates no Forge account, standing token, or active lease.
  2. Checkout and return (#31)
    1. Bind a slot under a new immutable grant that inherits nothing.
    2. Stamp occupancy onto the slot's alias custom_metadata, against flake-ops#471's path shape.
    3. Ordinary completion: revoke, confirm negative liveness, rotate, advance generation, return the UUID.
    4. Prove the principal survives return and that tombstoning is off this path.
    5. Free a slot only on explicit release; prove no elapsed time or failed check frees one.
    6. Persist occupancy before hand-out and clear it after the credential is dead; reconstruct on start, and refuse when the record cannot be read.

Delivery decomposition

Two Sessions, two reviews. Deliverables are derived from this package and carried as each Session's dependency set: bring-up is #32, #33, #34; the cycle is #35, #36, #37. Nothing in this package authorizes an apply: bring-up is gated on ratification.

Open

  • Q2 (Epic #2) — the negative-liveness source. Answered provisionally 2026-09-04: nothing infers liveness; only an explicit release frees a slot. No acceptance is blocked. The mechanism itself returns as an amendment once held-slot incidents show what evidence was available at the time.