feat(broker): authenticated ingress for the native plan_admitted mint (mint-control S3) - #316
Draft
SollanSystems wants to merge 24 commits into
Draft
SollanSystems wants to merge 24 commits into
SollanSystems wants to merge 24 commits into
Conversation
… boundary The role enum's own doc comment requires a new broker endpoint to extend it and configure its worker identity boundary explicitly at host startup, so the plan-admission ingress needs its own variant before it can have a policy.
The single kernel signer realm (Q4) leaves one deployment fact the mint cannot derive on its own: which operator identity an authenticated client UID stands for. That mapping is closed and injective, so an unmapped UID cannot reach the mint and two peers can never collapse to one recorded decided_by.
…e byte Peer verification is the first operation and repeats before every blocking read, so a wrong-role, unconfigured, or broker-aliasing peer cannot cause even a frame header read. The compiled plan bytes ride the same authenticated channel as a sealed memfd passed by SCM_RIGHTS: the broker-owned CAS is 0700 and broker-uid-owned, so the requester structurally cannot stage its own object, and a 16 KiB frame cannot carry a 1 MiB plan. Requiring the write, shrink, grow, and seal seals is what makes the bytes the host hashes and the bytes the host stages the same bytes.
…omain The response binds the request id and the content reference the caller named, and nothing the mint derived. A sealed outcome carries an admission event id and a reconciliation outcome carries none; no other pairing can be signed, and no refusal reason ever reaches the wire.
…host The host owns one socket on the inherited listener, one role, one run, and one kernel signer. It stages the sealed bytes the authenticated peer handed it into the broker-owned CAS — the broker is the only principal that writes there — derives decided_by from the peer's UID through the closed deployment identity policy, and re-derives the trusted base from the run's own kernel-signed start on the protected tape rather than from anything the request said. The listener path validator is shared with the promotion-decision endpoint rather than copied, so the two cannot drift apart on socket ownership or mode. Non-Linux fails closed: the confinement boundary, the custody, and SCM_RIGHTS delivery are all Linux-only, and there is no reduced mode to fall back to. Nothing drives this ingress. The mint must not gain a transport-reachable consumer before the approval arm verifies and requires checkpoint coverage of the admission it names.
…eader The kernel's admitted-plan reader opens this database over node:sqlite and hardcodes four names. Renaming any of them on the Rust side resolved to not-sealed and refused every governed dispatch with a misleading diagnosis, with no failing test on either side. Both directions are now pinned, and the state vocabulary is asserted against the CHECK itself rather than whichever constraint happens to fire first.
…ity matrix The row's claim of no transport-reachable writer stops being true with S3, so it says so plainly and states what reaching the writer now costs: an enrolled socket, a configured UID, and a peer check that runs before any frame byte. The ordering constraint that no consumer may drive it before S4 is unchanged.
… authority hosts The host reads its listener from inherited FD 3, so without a socket unit the socket it validates can never exist and the ingress is unreachable in a real deployment. The units mirror the operator-facing promotion-decision endpoint: a root-owned 0660 socket in the operator group, and a hardened service whose only writable paths are the ledger and the CAS it stages plan bytes into.
… truncated frame MSG_CTRUNC does not mean nothing was delivered: Linux installs as many descriptors as the control buffer holds and closes only the remainder. Refusing before wrapping the installed ones in OwnedFd leaked them, and the accept loop runs forever, so an authenticated peer could repeat one oversized frame until the host's descriptor table was exhausted. Ownership now precedes judgement on both the truncated and the malformed-length paths.
…ing its bytes The CAS is the mint's trust root and shares a filesystem with the ledger, so staging content that can never become an admission handed any authenticated peer a write amplifier on an accept loop that never ends. The digest check and the asserted-PASS precondition now both run before the stage, using the composition's own constant so the two cannot drift.
The host never runs git. It takes git_head from the run's own start event and establishes that the kernel signer attested to it, so the value originates with whoever recorded the run start and is exactly as trustworthy as the key that signs the admission — not an independent second source. Records the standing constraint that run_started must not become kernel-signable from caller-supplied input while it is the mint's trusted-base source, and pins its denylist membership by name, since every other test there iterates whatever the arrays happen to contain.
…gh the real host The resolver's refusal branches were only ever exercised against S2's injected fake, and the recorded operator was never proven to depend on the peer. Both are now driven through the production host: five run-start variants each yield reconciliation and mint nothing, and a lower-UID decoy client makes the recorded decided_by discriminate between the peer that connected and the first client in the config. Verified by mutation: deleting the signer comparison, the verification check, or the peer-UID lookup each turns one of these red.
… claims Request identifiers now require the canonical hyphenated spelling, matching what the config and the signed response already demanded, so a request and its own answer cannot disagree textually about one identity. The frame reader no longer refuses a buffer that arrived complete exactly at the deadline. Domain separation is tested by consequence — a same-key signature over the undomained payload must fail — instead of restating that the prefix function prepends its own constant, and the ingress content bound is pinned against the storage bound it mirrors through a test-only accessor rather than a published constant.
Covers the socket and unit pair, the operator-created buildplane-plan-authority identity the units name but nothing in-repo provisions, the closed plan-admission-v1.json config, what an operator does on each of the three diagnostics, and why a refused request is answered with silence. Records the trusted-base standing constraint alongside the no-consumer-before-S4 one.
A request that clears the peer, run, identity, digest, and validation checks is staged before the mint runs, so one the mint later declines has already written its bytes. The matrix said otherwise; it now says what is actually true, and the runbook records the residual — up to 1 MiB of self-named bytes per connection on a CAS volume sharing a filesystem with the ledger, inherent to resolving content through the broker-owned store, with quota and collection left to the operator. Also couples the truncation regression test to the ancillary capacity constant, so widening the capacity keeps the test exercising MSG_CTRUNC instead of silently retiring the branch, and names the read-path validator that makes the payload-variant match unreachable.
The tape is one events.db per project, so a sealed plan_admitted minted for run A satisfied signedByKernel, authorizedNextStep, and sealed identically when a packet for run B named it as provenance_ref — and authorized the dispatch. The reader already selected run_id but never exposed it. AdmittedPlanRecord now carries runId, the projection lookup binds run_id as defence in depth, and the gate refuses a wrong-run admission after the sealed check with a distinct message naming both runs. Latent today (no production minting path yet, Q7), but the bypass lived in the S2-landed gate that this slice is the adjacent consumer of.
request_id is caller-controlled and unique only within a run — storage keys (run_id, request_id) — so a sealed response for run A verified identically for run B that reused the same request id and content reference. run_id is now part of the response binding, the signed wire shape, the canonical payload, and the verifier's expected-binding check; the host binds it to config.run_id, the run already proven equal to the request's. Rust-internal signed wire, no ledger-client fixture regeneration.
H3: the asserted plan-digest canonical form and idempotency-key shape are request fields the mint refuses without loading the bytes back, so the ingress now refuses them before staging — closing the deeper write amplification beyond the digest/PASS pre-checks. The predicates are shared with the storage layer so the pre-stage and mint-time checks cannot drift. The irreducible residual (validations that need the CAS-resolved content) stays as the accepted, operator-owned NEW-2 residual. M1: the host stops discarding the CAS write result and fails the connection closed on error, rather than proceeding into a mint that would resolve a silently absent object and answer reconciliation. The CAS itself no longer wedges on a stale temp: a crash between create and rename left a deterministic .tmp-<hash> that made every retry fail AlreadyExists forever; the temp name is now unique per attempt. M2: document that the projection is one-admission-per-(run,request), not one-per-run, and that a stricter UNIQUE(run_id) is an open product question for the reviewer — not changed unprompted.
The dispatch gate binds each admission to its own run, but a run may still hold several sealed admissions under distinct request ids; each authorizes only the packet naming its event id. Records that a one-admission-per-run rule is an open product question rather than an omission.
The mint's empty / over-length / non-UTF-8 checks on the plan bytes operated only after the bytes were resolved back out of the CAS — i.e. post-stage — so a request with a matching content-ref but non-UTF-8 bytes still wrote up to the input bound into the trust-root CAS before the mint declined it. The three checks are extracted into one shared bp-ledger predicate that derive_plan_admission_content_v1 and the host pre-stage block both call, so the mint-time and pre-stage checks cannot drift, and the host now refuses inadmissible bytes before put_canonical_bytes. Empty and over-length were already refused at the ingress descriptor layer; UTF-8 was not. The only remaining post-stage disqualifications are the tape/projection-dependent ones (run-start attestation, idempotency conflict, fresh-snapshot rejection) — the irreducible NEW-2 residual, and the runbook now says so precisely.
The comment claimed a stranded temp is cleaned by the rename of a subsequent write of nearby content. That is false: a rename moves its own temp into place and never touches an abandoned one. Replaced with the truth — a crash between create and rename strands a .tmp-<hash>-<pid>-<seq> that no code path reclaims — and disclosed the residual honestly as low-rate and inert, with reclamation left to the operator. No sweeper added.
Round-2 changed @buildplane/kernel runtime behavior (AdmittedPlanRecord gains runId + the run-match gate) without a changeset. Declares @buildplane/kernel: patch.
Contributor
Merge Protections🔴 2 of 2 protections blocking · waiting on 🙋 you
🔴 require green CI on mainWaiting for
This rule is failing.
🔴 🚦 Auto-queueWaiting for
This rule is failing.When all merge protections are satisfied and these conditions match, this pull request will be queued automatically.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mint-control S3 — authenticated ingress for the native plan_admitted mint (L0)
Lands the authenticated transport in front of the S2 mint composition: a dedicated protected authority host that a configured, SO_PEERCRED-authenticated client reaches over an FD-3 unix socket to cause a native
plan_admittedmint. Completes the mint's ingress half; no transport-reachable consumer is wired (per the ratified §2.4 constraint — that lands at S4).Base:
origin/main3bdadf7(S2, #315). 19 commits.What it adds
BrokerAuthorityRoleV1::PlanAdmissionrole + confinement wiring.deny_unknown_fieldsplan-admission request wire struct.buildplane-plan-admission-host+ socket/run/buildplane/authority-host/plan-admission-v1.sock(FD 3) + config loaderplan_admission_host_config.rs+ systemd units.decided_byderived solely from the authenticated peer UID via config (request cannot name an identity — Q4).trusted_basetaken from the run's own kernel-signedrun_startedon the protected tape (attested, not independently re-derived — disclosed below).plan_admissionsschema to the Rust DDL.Review ceremony (L0, 4-role)
1174f85(14 findings incl. one HIGH fd-leak on the truncated-cmsg path, all closed and re-derived).5b85689→ repaired → HELD at9c0566a. Codex found two genuine authorization/evidence bypasses nothing else caught:plan_admittedfrom run A could authorize dispatch in run B sharing oneevents.db. Fixed:AdmittedPlanRecord.runIdexposed (inside the signed bytes), projection lookup + gate both bind the run. (latent — no production path can mint a sealed plan_admitted yet, Q7; the gate is S2-landed code, fixed here.)(request_id, plan_content_ref); a sealed response for run A verified for run B. Fixed:run_idadded inside the signed payload + verifier comparison.cas.rsused a deterministic.tmp-<hash>name withcreate_new, so a crash mid-write stranded a temp that wedged every retry forever — fixed with a unique-per-attempt name; the host now fails closed on CAS write error.Disclosures (verified real)
trusted_baseis an attestation, not an independent derivation. It comes off the run's own kernel-signedrun_started(requires exactly one Verified event under the pinned keys + configured kernel signer) — strictly stronger than a config value, but it rests on the same kernel key the admission signature does; it is not a second independent source. Held honest today by a standing constraint (recorded in the resolver doc, compat matrix, and runbook):run_startedmust not become kernel-signable from caller-supplied input while it is the mint's trusted-base source. Pinned by a denylist-membership test.(run_id, request_id), not one-per-run. Cross-run authorization is closed by the per-admission run binding (H1), not an at-most-one-per-run rule;UNIQUE(run_id)would forbid the candidate-transaction model and is left for an operator gate.Constraints held
plan_admittedstays inserve.rsREJECTED_ONLY_WHEN_SIGNED(serve.rs logic unchanged; +30/−0 test-only).planforge-plan-admission.test.tsverbatim.Gates
cargo test --manifest-path native/Cargo.toml(no-p): 1371 passed / 107 suites.pnpm typecheck,pnpm build,cargo fmt --check: clean.git diff packages/ledger-client/{src/generated,fixtures}: clean.@buildplane/kernel: patch(the cross-run dispatch-binding fix).L0 slice — not auto-merge eligible; admin-merge only.