pool: bring-up — reserve, provision, and wait for a person #38

Merged
larandar merged 1 commit from pool/30-bring-up into fastlane 2026-09-03 22:33:01 +00:00
Owner

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

src/pool.rs, new module. The steady-state cycle is #31 and is not here.

What it does

#32 — reserve before create. A four-word name under the swarm-alpha-drone- prefix is written to a durable ledger before anything exists in the directory, and the returned UUID is pinned to name and slot in one call with no rename pass.

The ordering is the requirement, not a preference. A create-then-record flow that dies in the middle leaves an account under a name nothing recorded, and the next run cannot tell whether that account is one it just made or one that was always there — the only behaviours left are refuse-forever or risk-a-duplicate. Reserving first makes the retry's question answerable: adopt what matches this name and this slot; anything else is a collision to deny or re-reserve.

Slot metadata is what makes adoption safe. A name alone cannot say whether the account under it is this slot's. An account with no slot is someone's hand-made one. Adopting either silently takes over another principal, so both are collisions.

#33 — ten principals, one grant each, nothing else. No Forge account, no standing token, no active lease: those belong to a lease at checkout, and a pool that issues them at birth has ten standing credentials nobody is using. Distinct UUIDs and distinct grants, so the pool is not one identity wearing ten names.

#34 — activation waits for a person. Per Ting/Ting#2 Q4, ratification is presence, not channel — so Gate holds a recorded act, and there is deliberately no method a notification could call. Presenting the mapping and ratifying it are different verbs; only the second opens the gate.

is_active re-derives the digest from what exists now rather than trusting the stored one. The question is not "was something ratified" but "is what exists the thing that was ratified", so a mapping that drifts afterwards closes the gate with nobody having to notice the drift.

Design notes worth arguing with

  • Ports, not a kanidm client. PoolDirectory has four methods and that is the entire authority provisioning holds — no delete, no rename, no credential mint. It also makes the state machine testable without a wire.
  • The ledger tolerates a torn tail and refuses a torn middle, matching operations.rs for the same reason: the last append may not have completed, but a damaged line anywhere else means the file is not what it claims.
  • POOL_SIZE = 10 is a constant, not a knob. Ten is the number the realized mapping gets ratified against; a different size is a different ratification.

Verification

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

The 14 new tests assert behaviour rather than return values, using a capture double that records calls:

claim how it is proven
reservation is durable before the account exists the double creates the account then fails; the ledger still holds the name, and no UUID
a retry adopts second run creates nothing — asserted on the create count, not the result
a foreign slot collides create count is zero
an unowned account collides same, with found_slot: None
provisioning twice converges create and grant counts stay at ten
the digest covers what it claims every field mutated in turn must change it

Not here

  • No live directory. PoolDirectory has no kanidm implementation yet — that lands when sa-vedanta exists, which is flake-ops#98's owner act.
  • No presentation surface for the mapping. The gate records the act; how a freeholder is shown the mapping is deliberately outside it.

Closes #32
Closes #33
Closes #34
Refs #30

Session [#30](https://jo.et0.pw/Ting/Vedanta/issues/30)'s three Deliverables, under [OpenSpec 30](https://jo.et0.pw/Ting/Vedanta/wiki/OpenSpec-30-agent-identity-pool-lifecycle) v0.1.1. `src/pool.rs`, new module. The steady-state cycle is [#31](https://jo.et0.pw/Ting/Vedanta/issues/31) and is not here. ## What it does **[#32](https://jo.et0.pw/Ting/Vedanta/issues/32) — reserve before create.** A four-word name under the `swarm-alpha-drone-` prefix is written to a durable ledger *before* anything exists in the directory, and the returned UUID is pinned to name and slot in one call with no rename pass. The ordering is the requirement, not a preference. A create-then-record flow that dies in the middle leaves an account under a name nothing recorded, and the next run cannot tell whether that account is one it just made or one that was always there — the only behaviours left are refuse-forever or risk-a-duplicate. Reserving first makes the retry's question answerable: adopt what matches this name **and** this slot; anything else is a collision to deny or re-reserve. **Slot metadata is what makes adoption safe.** A name alone cannot say whether the account under it is this slot's. An account with *no* slot is someone's hand-made one. Adopting either silently takes over another principal, so both are collisions. **[#33](https://jo.et0.pw/Ting/Vedanta/issues/33) — ten principals, one grant each, nothing else.** No Forge account, no standing token, no active lease: those belong to a lease at checkout, and a pool that issues them at birth has ten standing credentials nobody is using. Distinct UUIDs and distinct grants, so the pool is not one identity wearing ten names. **[#34](https://jo.et0.pw/Ting/Vedanta/issues/34) — activation waits for a person.** Per [Ting/Ting#2](https://jo.et0.pw/Ting/Ting/issues/2#issuecomment-15365) Q4, ratification is **presence, not channel** — so `Gate` holds a recorded *act*, and there is deliberately no method a notification could call. Presenting the mapping and ratifying it are different verbs; only the second opens the gate. `is_active` re-derives the digest from what exists *now* rather than trusting the stored one. The question is not "was something ratified" but "is what exists the thing that was ratified", so a mapping that drifts afterwards closes the gate with nobody having to notice the drift. ## Design notes worth arguing with - **Ports, not a kanidm client.** `PoolDirectory` has four methods and that is the entire authority provisioning holds — no delete, no rename, no credential mint. It also makes the state machine testable without a wire. - **The ledger** tolerates a torn tail and refuses a torn middle, matching `operations.rs` for the same reason: the last append may not have completed, but a damaged line anywhere else means the file is not what it claims. - **`POOL_SIZE = 10` is a constant, not a knob.** Ten is the number the realized mapping gets ratified against; a different size is a different ratification. ## Verification `nix develop` gate green: `cargo fmt` stable, `cargo clippy --workspace --all-targets -D warnings` clean, `cargo test --workspace` passing — **85 lib tests, up from 71**. The 14 new tests assert behaviour rather than return values, using a capture double that records calls: | claim | how it is proven | |---|---| | reservation is durable before the account exists | the double creates the account *then* fails; the ledger still holds the name, and no UUID | | a retry adopts | second run creates nothing — asserted on the create count, not the result | | a foreign slot collides | create count is zero | | an unowned account collides | same, with `found_slot: None` | | provisioning twice converges | create and grant counts stay at ten | | the digest covers what it claims | every field mutated in turn must change it | ## Not here - No live directory. `PoolDirectory` has no kanidm implementation yet — that lands when `sa-vedanta` exists, which is [flake-ops#98](https://jo.et0.pw/lar.ad/flake-ops/issues/98)'s owner act. - No presentation surface for the mapping. The gate records the act; how a freeholder is shown the mapping is deliberately outside it. Closes #32 Closes #33 Closes #34 Refs #30
Ten durable slots, each with a four-word name pinned to the UUID the directory
returns, each carrying one distinct immutable grant, and none of it live until a
freeholder has acted on what was actually created.

RESERVE BEFORE CREATE, because the alternative has a window with no safe exit.
A create-then-record flow that dies in the middle leaves an account under a name
nothing recorded, and the next run cannot tell whether that account is one it
made a moment ago or one that was always there — leaving only "refuse forever"
or "risk a duplicate". The reservation is durable first, so the retry's question
has an answer: adopt what matches this name AND this slot, and treat anything
else as a collision to deny or re-reserve. Never a second principal.

Slot metadata is what makes adoption safe. A name alone cannot say whether the
account under it is this slot's, and an account with no slot at all is someone's
hand-made one — adopting either silently takes over another principal, so both
are collisions.

Provisioning creates identity and authority and nothing else: no Forge account,
no standing token, no active lease. Those belong to a lease at checkout, and a
pool that issues them at birth has ten standing credentials nobody is using.

RATIFICATION IS PRESENCE, NOT CHANNEL (Ting/Ting#2 Q4), so the gate holds a
recorded ACT and there is deliberately no method a notification could call.
Presenting the mapping and ratifying it are different verbs and only the second
opens the gate. It is recorded against a digest of the realized mapping, and
`is_active` re-derives that digest from what exists now rather than trusting the
stored one — the question is not "was something ratified" but "is what exists
the thing that was ratified", so a mapping that drifts afterwards closes the
gate without anyone having to notice the drift.

The ledger tolerates a torn tail and refuses a torn middle, matching the
operation journal.

Ports rather than a kanidm client, so the state machine is testable without a
wire. The capture double records calls, so "running twice converges" is asserted
as a property of the create and grant counts rather than of the return value,
and the digest test mutates every field in turn to prove none of them is outside
what a ratification covers.
larandar merged commit 688db5c626 into fastlane 2026-09-03 22:33:01 +00:00
larandar deleted branch pool/30-bring-up 2026-09-03 22:33:01 +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!38
No description provided.