session 47: the tested contract becomes the running one — a private socket, a read-only projection, and no pool bring-up (#47) #51
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Meta/Campaign
Meta/Epic
Meta/Session
Priority/Critical
Priority/High
Priority/Low
Priority/Medium
Reviewed/Confirmed
Reviewed/Curated
Reviewed/Duplicate
Reviewed/Invalid
Reviewed/Won't Fix
Scope/Campaign
Status/Abandoned
Status/Blocked
Status/Conflicted
Status/In Progress
Status/In Review
Status/Need Grooming
Status/Need More Info
Status/Ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Ting/Vedanta!51
Loading…
Reference in a new issue
No description provided.
Delete branch "tango/47-private-service"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #47. Records ruling D2 on #45 (closed) and D2b on #46. Files #50 for the half this leaves undone.
Why
#20 closed with the private command contract, the journal, both native adapters and the lost-response semantics written and tested — and nothing serving them.
vedanta servemounted jostoph-rs's evaluate router and stopped there;private_api,operations,kanidm,forgejoandreconcilewere library modules no process instantiated. The delivery run for Ting/Ting#60 found that as the first Vedanta delta.The same run found a boundary mismatch the capture doubles had hidden: PR #38 reserved slot names, created principals, pinned UUIDs and rendered grants.
contracts.identity.openbao-lease-backendforbids exactly that, and the freeholder ruled it (2026-09-05): "Vedanta MUST NOT — this is a provider job."What lands
src/private_transport.rs) on a unix socket. The caller is the kernel-reported peer uid, mapped to OpenBao's configured user; any other uid isunauthenticatedat the validator's first stage, before the journal and before the body is believed. There is no header a caller could set to become OpenBao. Routes:POST /v1/lifecycle/commands,GET …/operations/{id}(metadata only),POST …/operations/{id}/acknowledge,GET …/projection/{grant},GET …/health.src/projection.rs): loaded at start, re-read per command, refused when it says what the contract forbids (a lease ceiling above 1800 s, a claimant that is not the principal, a duplicate grant).runtime_viewjoins it with the journal so a rotate names its true predecessor and a second mint on a live lease is refused.src/native.rs): repositories named → a Forge capability on the queen holder's account, scopes mapped exactly (repository:read→read:repository; an unknown operation is refused, never widened), deleted by exact ID. No repositories → a Kanidm api-token. Either issuer may be absent and answersnative_unavailablerather than pretending.OperationJournal::{complete,fail}), with two new error classes:native_refused(final) andnative_unavailable(retryable).src/pool.rskeeps the slot types, the realized mapping as a read (Realized::load; there is no save) and the ratification gate;Provisioner,Ledger,NamerandPoolDirectoryare removed.DeployedGrantcarries aSeatDelegation— which seat, which operations its holder must be entitled to, which account mode — and the service joins the seat's one active tenure in at command time from aTenureSource. Two records claiming a seat are no holder, as are none.module.nixgainsservices.vedanta.private.*;AGENTS.mdandCONTEXT.mdsay what runs.Evidence
devenv shell --:cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace— green on 2026-09-06. 152 lib tests (was 128), 1 bin, 32 inopenbao-plugin, 3 doctests.The listener is exercised over a real unix socket, not a mock: OpenBao's uid mints once and the journal keeps the token ID; a stranger's uid is refused with no journal write and no mutation; a replayed operation ID does not mint twice; status never carries the value; a tampered signature, an unknown grant, an unheld seat and a doubly-claimed seat are each refused independently.
Both producers of the projection file — the provider library (nixops4-providers#27) and the deployment (flake-ops#426) — have their actual rendered output vendored under
fixtures/and parsed by the real parser, so a drift in either fails a test here instead of denying in production.Nothing here has spoken to a live Kanidm or Forgejo.
sa-vedantadoes not exist until flake-ops#98 runs.What a reviewer must check
/etc/passwdresolution at startup means OpenBao's unit needs a static user — flake-ops#426 switches it offDynamicUser, and that migration moves a raft store.runtime_view's choice for a rotate or invalidate with nothing minted: the expected generation comes from the command, so the denial lands on the predecessor stage, which is the stage that is wrong.completeafter the mutation and still answers success if that write fails — the operation stayspendingfor #23's sweep rather than telling a caller "failed" about a token that is live. Argued in the commit body.active_queen_tenuresis empty until #50 exists, so every drone lease denies at the seat stage. Fail-closed and deliberate.contracts.identity.openbao-lease-backendstill forbids what D2b permits; the delta is Ting/contracts#10, unratified.Until now vedanta serve mounted jostoph-rs's evaluate router and nothing else; the private command contract, journal and adapters were library modules no process ran. serve now also binds the private surface on a unix socket when VEDANTA_PRIVATE_SOCKET is set, and fails the unit when the surface is configured incompletely. Peer identity comes from the transport: the kernel-reported peer uid is mapped to OpenBao's configured user, and any other uid is unauthenticated at the validator's first stage — before the journal, before the body is believed. There is no header a caller could set to become OpenBao. Routes: POST /v1/lifecycle/commands, GET /operations/{id} (metadata only), POST /operations/{id}/acknowledge, GET /projection/{grant}, GET /health. The tests exercise all of it over a real socket: a stranger's uid, a replayed operation ID, a tampered signature, an unknown grant, and that status never carries the value. axum is named as a direct dependency; it is the same axum jostoph-rs already brings, so the closure gains no second HTTP stack. Refs: Ting/Vedanta#47.Ruling D2b (Larandar, 2026-09-05): provisioning makes the SEAT exist once; allowing a principal to take it is Vedanta's effect. So the tenure OpenBao resolves at its seat-tenure stage is Vedanta's to serve, not a field of the static grant projection — a grant's queen field says whom the seat is delegated to, not that anyone holds it. GET /v1/lifecycle/projection/{grant} now answers the deployed grant flattened beside active_queen_tenures. Nothing records a tenure yet (the seat-taking command is #50), so the list is empty by construction, which is the fail-closed answer: zero holders denies every drone lease until the seat can actually be taken. checkout.rs stays as the drone-slot half of allocation; AGENTS.md and CONTEXT.md say so, and the contract's 'MUST NOT allocate pool slots' is amended by Ting/contracts#10. Refs: Ting/Vedanta#46, #47, #50, Ting/contracts#10.Reviewer note: item 5 of "what a reviewer must check" is now settled. The freeholder ratified and merged the D2b delta (Ting/contracts#11, issue #10 closed, 2026-09-06).
contracts.identity.openbao-lease-backendno longer forbids what this PR implements: the prohibition is on creating seats and authoring grants, and seat taking is named as Vedanta's bounded effect, with OpenBao resolving the active tenure from Vedanta's occupancy rather than from a provisioned file — which is exactly whatsrc/projection.rs'sSeatDelegationandsrc/private_transport.rs'sTenureSourcedo here.One caveat while it lasts: the materialized projection at
openspec/specs/contracts.identity.openbao-lease-backend/spec.mdstill shows the old clause, because archiving the change into the specs is the store's separate after-ratification step and has not run yet. A reviewer reading the published spec rather than the merged change will see the pre-ruling wording. That archive is in progress; this note will not be edited when it lands, so check the store if the distinction matters to your verdict.The other four items stand unchanged.
Verdict: changes required.
src/private_transport.rs:345–355: when the deployed projection becomes unreadable or invalid,refreshed_projection()returns the last good active copy indefinitely. A failed revocation deployment therefore retains authority. Refuse mutations when authoritative input cannot be read or validated.src/main.rs:212–217:tokio::join!waits for both futures. If one bind fails while the other server keeps running, the error never reaches systemd. Use failure-propagating concurrency that terminates the sibling listener.Cross-change concern with #53: the existing validator requires the current matching queen even for exact invalidation (
src/private_api.rs:556). Releasing or replacing that tenure can make cleanup of its outstanding tokens impossible. Validate bounded cleanup against recorded issuance ownership without restoring mint authority, retaining the original native issuer identity. This validator condition is not itself newly introduced here; the running listener and seat-release path expose the interaction.Validation rerun:
nix develop --command just checkpassed, with 152 library tests + 1 binary test, formatting and clippy clean. These findings are outside the existing positive tests. Review used the merged D2b ruling and contracts#12’s materialization; the earlier wording discrepancy is not treated as a defect in this PR.Reviewed commit:
6a25a763cd89324ec38a5777b0c15428d76e484e.Agent review performed by Codex at the user’s request. Posted through the PR author’s account; this records review evidence, not an independent collaborator approval or a new owner ruling.
Both findings fixed.
878b2bfandf989809, pushed on top of the reviewed6a25a763. No rebase — #53 is stacked here.1 — Fail closed when the projection cannot be re-read
refreshed_projection()is split. The command route now uses a fail-closed version that returns aResultand propagates the failure; the two GET routes keep a best-effortcached_projection().New error class
ProjectionUnavailable, retryable, HTTP 503 — the same bucket asNativeUnavailable. It was not folded intoGrantDigestMismatch, which means something else entirely ("the projection loaded fine, and your grant is not it"). Peer-first ordering is preserved, so a stranger still learns onlyunauthenticated.The split is a judgment, and here is its defence.
POST /commandsis where authority is exercised, so it must refuse;GET /projection/{grant}and/healthare introspection, and serving the last good read there does not authorize anything. If OpenBao builds a view from a momentarily stale projection read and then submits a command, Vedanta still refuses it — the enforcement point is the mutation, and it is closed. Argue it the other way and the fix is one line.Test:
a_projection_that_goes_unreadable_after_a_good_start_refuses_the_next_commandcorrupts the file after a successful mint and proves the next command is refused 503 with no second mutation. It fails against the old code.2 — One listener's exit now terminates the other
tokio::join!replaced withrace_to_first_exit()overtokio::select!, which drops the losing future the moment either listener exits.try_join!was considered and rejected: it propagates the error but leaves the sibling running, which is the actual defect — a unit that looks alive with one live listener.The worker checked jostoph-rs's
service.rsto confirm neither listener hides a detachedtokio::spawn, so dropping the future genuinely closes its socket. Two tests use a "never resolves, flags on Drop" future to prove termination, not merely error propagation; both would hang forever underjoin!.One honest note it left: with no hidden spawns in today's code,
select!overtry_join!is defensive rather than strictly required. It is recorded in case a future refactor introduces one.Measured
cargo fmt --checkclean,clippy --workspace --all-targets -- -D warningsclean,cargo test --workspacegreen: 153 lib (152 + 1), 3 bin (1 + 2), 32 plugin. Re-run independently by the queen.module.nixandopenbao-plugin/**untouched — neither finding needed them.The cross-change concern you raised with #53 (the validator requiring a matching current queen even for exact invalidation, so releasing a tenure could strand its outstanding tokens) is not addressed here and is not this PR's to address. It is carried to #53.