pool: the steady-state cycle — bind, return, and never infer a free slot #39

Merged
larandar merged 1 commit from pool/31-checkout-return into fastlane 2026-09-03 22:33:33 +00:00
Owner

Session #31's three Deliverables, under OpenSpec 30 v0.1.1.

Stacked on #38 (bring-up), which it needs for Slot, Grant, Realized and Gate. Review that one first; this targets its branch rather than fastlane so the diff is only src/checkout.rs.

What it does

#35 — bind under a new grant, stamp the occupancy. Checkout takes an available slot, issues a new immutable grant inheriting nothing, and writes the occupancy onto the slot's entity alias.

Two traps are written into the types because both cost real time to find:

  • The stamp is custom_metadata, not a claim, and structurally cannot be one. A kanidm claim map is valuesByGroup, so a claim carries only what the directory asserts about an account — and occupancy changes per allocation with no group to hang it on. Alias metadata is rewritten from claim mappings on every login; custom_metadata is written out of band and a login does not touch it.
  • It is not the contract's assignment_id. That names the queen's tenure assignment, one value shared by every drone under a queen, so it cannot separate two occupants of one slot. A path built on it looks isolated and is not.

A failed stamp does not hand out the slot. A drone whose alias still carries its predecessor's assignment would read the predecessor's workspace — precisely what the segment exists to prevent — so the checkout fails instead of proceeding.

#36 — return preserves the worker. Revoke, rotate, advance the generation, return the UUID.

Revoke first, release last. A slot returned before its capability is revoked can be allocated while its predecessor still holds authority, so the order is load-bearing. A failure partway leaves the slot held and the completion retryable — never half-returned.

The principal surviving is proven by the port, not asserted in prose: Occupancy has no delete, suppress, or tombstone method, so destroying a principal is unreachable from this module. A test cannot prove "did not delete" against a port that could.

#37 — only an explicit release frees a slot. Per Ting/Ting#2 Q2, the liveness mechanism stays undefined until incidents show what evidence exists. Rather than carry that as a gap, nothing infers liveness at all — the type reads no clock, which is what the elapsed-time test asserts.

The chosen failure is exhaustion: loud, bounded, recoverable by a release, and it produces exactly the incident record the eventual mechanism gets designed against. A wrong automatic release would instead fail silently into two live drones sharing one identity.

Verification

nix develop gate green: fmt stable, clippy --workspace --all-targets -D warnings clean, cargo test --workspace passing — 96 lib tests, up from 85.

claim how it is proven
two occupants of one slot are isolated same slot reused; assignments and grants differ, and the alias carries the current one
a failed stamp hands out nothing slot stays Available
an unratified pool allocates nothing zero grants issued
return revokes, rotates, advances asserted on the recorded calls and the generation
a partial return is retryable rotation fails → slot still Held; retry succeeds
no elapsed time frees a slot 1000 attempts against a held pool, all Exhausted
exhaustion refuses rather than reclaims held count unchanged after refusal
release still revokes a dead holder's capability is revoked, not merely forgotten

Not here

  • No live effects. Occupancy has no OpenBao or Kanidm implementation yet; both wait on sa-vedanta, which is flake-ops#98's owner act.
  • No restart recovery. Cycle::new starts every slot available, which is correct for a fresh pool and not for a restart with live holders. Flagged in the doc comment — recovering held slots from durable state is not in this Session's scope and must land before this runs against a real pool.
  • No held-slot alerting. held() exposes what an alert would be built on; raising it belongs to #11's mandate and flake-ops#470's plane.

Closes #35
Closes #36
Closes #37
Refs #31

Session [#31](https://jo.et0.pw/Ting/Vedanta/issues/31)'s three Deliverables, under [OpenSpec 30](https://jo.et0.pw/Ting/Vedanta/wiki/OpenSpec-30-agent-identity-pool-lifecycle) v0.1.1. **Stacked on [#38](https://jo.et0.pw/Ting/Vedanta/pulls/38)** (bring-up), which it needs for `Slot`, `Grant`, `Realized` and `Gate`. Review that one first; this targets its branch rather than fastlane so the diff is only `src/checkout.rs`. ## What it does **[#35](https://jo.et0.pw/Ting/Vedanta/issues/35) — bind under a new grant, stamp the occupancy.** Checkout takes an available slot, issues a **new** immutable grant inheriting nothing, and writes the occupancy onto the slot's entity alias. Two traps are written into the types because both cost real time to find: - **The stamp is `custom_metadata`, not a claim, and structurally cannot be one.** A kanidm claim map is `valuesByGroup`, so a claim carries only what the directory asserts about an account — and occupancy changes per allocation with no group to hang it on. Alias `metadata` is rewritten from claim mappings on every login; `custom_metadata` is written out of band and a login does not touch it. - **It is not the contract's `assignment_id`.** That names the *queen's tenure* assignment, one value shared by every drone under a queen, so it cannot separate two occupants of one slot. A path built on it looks isolated and is not. **A failed stamp does not hand out the slot.** A drone whose alias still carries its predecessor's assignment would read the predecessor's workspace — precisely what the segment exists to prevent — so the checkout fails instead of proceeding. **[#36](https://jo.et0.pw/Ting/Vedanta/issues/36) — return preserves the worker.** Revoke, rotate, advance the generation, return the UUID. **Revoke first, release last.** A slot returned before its capability is revoked can be allocated while its predecessor still holds authority, so the order is load-bearing. A failure partway leaves the slot **held** and the completion retryable — never half-returned. The principal surviving is proven by the *port*, not asserted in prose: `Occupancy` has no delete, suppress, or tombstone method, so destroying a principal is unreachable from this module. A test cannot prove "did not delete" against a port that could. **[#37](https://jo.et0.pw/Ting/Vedanta/issues/37) — only an explicit release frees a slot.** Per [Ting/Ting#2](https://jo.et0.pw/Ting/Ting/issues/2#issuecomment-15474) Q2, the liveness mechanism stays undefined until incidents show what evidence exists. Rather than carry that as a gap, nothing infers liveness at all — **the type reads no clock**, which is what the elapsed-time test asserts. The chosen failure is exhaustion: loud, bounded, recoverable by a release, and it produces exactly the incident record the eventual mechanism gets designed against. A wrong automatic release would instead fail silently into two live drones sharing one identity. ## Verification `nix develop` gate green: fmt stable, `clippy --workspace --all-targets -D warnings` clean, `cargo test --workspace` passing — **96 lib tests, up from 85**. | claim | how it is proven | |---|---| | two occupants of one slot are isolated | same slot reused; assignments and grants differ, and the alias carries the *current* one | | a failed stamp hands out nothing | slot stays `Available` | | an unratified pool allocates nothing | zero grants issued | | return revokes, rotates, advances | asserted on the recorded calls and the generation | | a partial return is retryable | rotation fails → slot still `Held`; retry succeeds | | no elapsed time frees a slot | 1000 attempts against a held pool, all `Exhausted` | | exhaustion refuses rather than reclaims | held count unchanged after refusal | | release still revokes | a dead holder's capability is revoked, not merely forgotten | ## Not here - **No live effects.** `Occupancy` has no OpenBao or Kanidm implementation yet; both wait on `sa-vedanta`, which is [flake-ops#98](https://jo.et0.pw/lar.ad/flake-ops/issues/98)'s owner act. - **No restart recovery.** `Cycle::new` starts every slot available, which is correct for a fresh pool and *not* for a restart with live holders. Flagged in the doc comment — recovering held slots from durable state is not in this Session's scope and must land before this runs against a real pool. - **No held-slot alerting.** `held()` exposes what an alert would be built on; raising it belongs to [#11](https://jo.et0.pw/Ting/Vedanta/issues/11)'s mandate and [flake-ops#470](https://jo.et0.pw/lar.ad/flake-ops/issues/470)'s plane. Closes #35 Closes #36 Closes #37 Refs #31
Checkout binds a slot under a NEW immutable grant and stamps the occupancy onto
its entity alias; completion revokes, rotates, advances the generation and gives
the slot back with its principal intact; and nothing anywhere frees a slot on
inference.

THE OCCUPANCY STAMP IS custom_metadata, and the reason is structural. A kanidm
claim map is valuesByGroup, so a claim carries only what the directory asserts
about an account — and which occupancy is calling changes per allocation, with
no group to hang it on. 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 the lifetime this value needs.

Nor is it the contract's existing `assignment_id`: that names the QUEEN'S TENURE
assignment, one value shared by every drone under a queen, so it cannot separate
two occupants of one slot. A path built on it looks isolated and is not. Both
traps are written at the type.

A FAILED STAMP DOES NOT HAND OUT THE SLOT. A drone whose alias still carries its
predecessor's assignment would read the predecessor's workspace — the exact
failure the segment exists to prevent — so the checkout fails instead.

REVOKE FIRST, RELEASE LAST. A slot returned before its capability is revoked can
be allocated while its predecessor still holds authority, so the order is
load-bearing and a failure partway leaves the slot held and the completion
retryable. Never half-returned.

The principal survives, and the port is how that is proven rather than asserted:
`Occupancy` has no delete, suppress, or tombstone method, so destroying a
principal is not reachable from this module. A test cannot prove "did not
delete" against a port that could.

ONLY AN EXPLICIT RELEASE FREES A SLOT (Ting/Ting#2 Q2). No timeout, no staleness
rule, no unreachable runtime, no absence of evidence — the type reads no clock
at all, which is what the elapsed-time test asserts. The chosen failure is
exhaustion: loud, bounded, recoverable, and it produces the incident record the
eventual liveness mechanism will be designed against, where a wrong automatic
release would fail silently into two live drones sharing one identity.
larandar changed target branch from pool/30-bring-up to fastlane 2026-09-03 22:33:01 +00:00
larandar merged commit 7b9b5baa20 into fastlane 2026-09-03 22:33:33 +00:00
larandar deleted branch pool/31-checkout-return 2026-09-03 22:33:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Ting/Vedanta!39
No description provided.