Repository navigation
Approval broker: per-release derived helper sudoers grant + ntfy runtime-observe helper contract - #12124
Conversation
…ime-observe helper contract The broker release install now derives ONE exact sudoers line for its own release's helper invocation, and installs it with the release. That lets #12077's enrolment-code verb read the live ntfy process as gunbc-ntfy with no PID wildcard and no authored script. - gunbc.executor_privileged_operation: RunGunbcEntryAs variant, plus an exhaustive OperationPrincipal. The sudoers (run_as) and the sudo -u elevation are read from that one match, so no existing grant moves. - gunbc.live_deploy.emit: approval_broker_ntfy_runtime_observe_operation, the helper argv and the grant line from one operation, and the grant steps in the dark install (visudo -cf staged, install 0440, visudo -cf installed, cmp readback). The single-line drop-in replaces the previous release's line by construction. Sudoers-special words refuse with a poison. Also release_revision named-argument for the verb. - gunbc.auth.approval_ntfy_runtime_observe: the helper's contract (entry identity, run-as, typed observation, total line codec). The entry body and the four joins land in #12077. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
SOURCE HOLD at exact head fbf351ecd992a229d99e9a005afc0e77b74add1d.
The main direction is right: one RunGunbcEntryAs value projects both the sudoers command and sudo -u argv; the release SHA is in every path; there is no wildcard or current pointer; existing root operations keep their rendering; and the helper contract carries the live-process facts #12077 needs.
Three source walls remain.
1. The exact command executes code writable by its grantee
approval_broker_helper_grantee() is fleet_posix_operator_user (briansrls). The binary, source roots and entry are under the broker release locus. approval_broker_tree_copy_steps creates that locus with install_d_owned_command(spec, ...); the deployment service user is also briansrls, and the directories are 0755.
So the principal holding the sudo rule can replace the named binary, a .dag source/import, or an ancestor directory entry and then invoke the identical argv as gunbc-ntfy. The SHA/path spelling stays fixed while the privileged code changes. A digest over the binary alone is insufficient because gunbc run interprets the source roots.
This is the same class the existing executor_privileged_operation annotation already forbids: a fixed argv is not a privilege wall when the grantee authors its payload.
Require a helper closure whose binary, entry, imported source roots, symlink targets and writable ancestors are not writable by briansrls—for example a root-owned read-only release locus or separate root-owned helper snapshot—and make that standing a prerequisite of emitting the grant.
Required RED/control:
- grantee-writable binary or source closure -> no grant;
- root-owned, non-grantee-writable exact closure -> grant admitted;
- post-install replacement of one imported byte is impossible or detected before execution.
2. The sudoers drop-in is installed with the wrong group, and the checks do not test it
The transaction installs root:ghrunner 0440 /etc/sudoers.d/gunbc-approval-broker-helpers. Sudo validates included files against the configured sudoers_uid and sudoers_gid; the default GID is 0. Nothing here declares or observes sudoers_gid=1001.
Both checks are visudo -c -f <explicit path>. With an explicit path, check mode does not enforce owner or mode unless -O / -P are supplied. Therefore syntax checks and the unprivileged cmp can all pass while the real sudo loader rejects or ignores the file.
Install root:the observed/configured sudoers GID (normally root), enforce owner and mode on the installed check, and perform byte readback through a privileged fixed-path observation rather than weakening the file group for ghrunner.
The replacement should also be atomic. GNU install unlinks the destination before opening the replacement; interruption can leave the included file absent or partial after the staged syntax check. Validate a root-owned temporary file in /etc/sudoers.d, then atomically rename it over the final path.
Required controls:
- root:ghrunner 0440 -> refused;
- root:configured-sudoers-gid 0440 with exact bytes -> admitted;
- interruption before rename -> prior valid file remains;
- partial content never becomes the included file.
3. The wire codec permits both coproduct arms in one document
ntfy_runtime_wire_unknown_key accepts the union of all keys. The observed decoder ignores refused_read / refused_cause; the refused decoder ignores all observed-only fields. A document can therefore carry both observed and refused evidence and be accepted according only to outcome=.
Make each outcome admit its exact key population, or require canonical decode/re-encode equality.
Required REDs:
- observed payload plus a refused field -> undecodable;
- refused payload plus an observed-only field -> undecodable;
- both encoder outputs still round-trip.
The declared frontier for the body and #12077 joins is otherwise honest. I accept the reported 15/15 claims as evidence for the source paths they exercise; they do not establish sudo's installed-file ownership rules or the immutability of the code behind the exact command.
…outcome objects refuse (review 70435) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…or AmbiguousBareNameRead) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Review 70435, both findings are addressed in 27e7859.
Witness file: 14/14 PASS locally. fd63b73 also fixes the floor red: Still open, from the side-chat HOLD: walls 1 (the grantee can write the helper's closure) and 2 (sudoers file owner/mode and an atomic replace). Both are in progress; a decision on the deploy roster's binaries is pending. — sent from calm-deer-391 |
…census row Side-chat HOLD walls 1 and 2 (parent decision A), and review 70452. - Wall 1: the grant names /opt/gunbc/approval-broker-helpers/<sha>, a copy root makes at install from the deploying checkout (cp -R -P --no-preserve=all; install -o root for the binary). A readback stops the stage unless the snapshot and every ancestor are root-owned, not group- or world-writable, and link-free. The grant's admission refuses any locus whose owner is the grantee (the release dir is briansrls, the grantee). - Wall 2: stage -> visudo -cf -> install root:root 0440 to a dotted temp name (sudoers(5) #includedir skips it) -> visudo -c -O -P -f -> mv -T -> visudo -c -O -P -f final -> privileged read into stage -> cmp. The previous release's snapshot is retired after the rename. - Roster: SudoCp and SudoMv added to ci_runner_sudo_binaries. No new privilege class: the roster already holds whole install and visudo. - privileged_effect_census: a row for approval_broker_helper_grant_steps; the selection's verdict is Conforms, pinned by a claim. - extdeps.tools.diffutils imports Unit, Bool and Int from their declaring modules (floor AmbiguousBareNameRead). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Review 70452 is addressed in f8beaa6.
The same head also lands side-chat walls 1 and 2 (root-owned helper snapshot; atomic root:root sudoers replace with — sent from calm-deer-391 |
briansrls
left a comment
There was a problem hiding this comment.
SOURCE HOLD at exact head f8beaa6f63c41fd0693daf2467293d0ba699c822.
The three prior walls are substantially repaired:
- the granted binary and both source roots now live under a release-named root-owned snapshot, not the
briansrls-owned release tree; - the sudoers policy is staged under an ignored dotted name, installed
root:root 0440, checked withvisudo -c -O -P, atomically renamed withmv -T, checked again, and read back through a privileged copy beforecmp; - the helper wire now selects one outcome-specific exact key population, rejects duplicates and mixed-arm members, and both outcome encodings round-trip.
One fail-closed defect remains in wall 1.
The snapshot readback treats an observation failure as a clean snapshot
approval_broker_helper_snapshot_readback_command emits one outer command of this shape:
test -z "$(find <ancestors> ... -print)$(find <snapshot> ... -print)"The comment relies on set -e, but Bash observes the exit status of the outer test, not the failed commands hidden inside its substitutions. If either find cannot observe its subject — a missing path, permission/traversal error, I/O error, or another nonzero read — and emits no stdout, the substitutions become empty, test -z "" returns zero, and the deploy continues to install the sudoers grant.
A minimal reproduction is:
set -e
test -z "$(find /definitely-not-there -maxdepth 0 -print)"
echo survivedfind reports failure and survived still executes. pipefail does not change command-substitution status propagation here.
That means the grant can be written without establishing the very fact that makes the fixed argv safe: that the binary, source closure and ancestors were successfully observed as root-owned, non-writable and link-free. This is the same no-observation-versus-clean-observation conflation the wall is intended to eliminate.
Required repair:
- Perform the observations with authority sufficient to traverse the root-owned snapshot (or otherwise prove traversal is always available).
- Propagate each observation command's nonzero status independently before judging its output. For example, two separate assignment commands under
set -efollowed by the emptiness test, or an explicit typed/branching refusal; do not hide the reads inside the successfultestcommand. - Check ancestor symlinks explicitly rather than only incidentally through permission bits.
Required executed controls:
- nonexistent or unreadable snapshot/ancestor -> nonzero; no grant installation is reached;
- observable root-owned, non-group/world-writable, link-free tree -> zero;
- one writable entry or symlink -> nonzero;
- mutating away the per-read status propagation makes the observation-failure RED fail.
The existing witness only greps the emitted text for test -z, ancestor paths and -type l; it does not execute the readback or discriminate a failed find. The 19/19 receipt therefore does not cover this counterexample.
I found no further blocker in the root-owned-locus selection, exact argv/grant projection, atomic sudoers replacement, privileged byte readback, mixed-outcome codec, or census enrollment. The declared #12077 body/join frontier remains fail-closed.
briansrls
left a comment
There was a problem hiding this comment.
SOURCE HOLD at exact head f8beaa6f63c41fd0693daf2467293d0ba699c822.
The three prior walls are substantially repaired:
- The granted binary and interpreted source roots now live under the root-owned release snapshot, while the grantee-owned release locus is refused.
- The sudoers file is staged as root:root 0440 under an ignored dotted name, checked with
visudo -c -O -P -f, atomically renamed withmv -T, checked again, and read back through a privileged copy. - The JSON decoder admits an exact key population per selected outcome and refuses mixed-arm, unknown, or repeated members.
One fail-open remains in the load-bearing snapshot readback.
approval_broker_helper_snapshot_readback_command renders approximately:
test -z "$(find <ancestors> ... -print)$(find <snapshot> ... -print)"A find observation error does not become this command's status. If either find cannot traverse a directory, loses the path, or otherwise exits nonzero while printing no matching path to stdout, command substitution supplies an empty string and test -z returns success. Under set -e, the stage therefore continues and may install the grant over a closure whose ownership/link population was not observed. The current claim checks only that the emitted string contains test -z, the ancestor paths, and some -type l; it does not execute or discriminate this error path.
This is directly reproducible shell behavior:
set -e
test -z "$(find /definitely/not/here -maxdepth 0 -print)"
# find reports ENOENT, but test succeeds and execution continuesMake each inventory read fail-closed before its output is judged—for example, capture each find in a simple assignment/step whose own exit status is honored, then test the successfully captured outputs; or use an equivalently strong modeled verifier. The ancestor predicate should also state -type l explicitly rather than relying incidentally on a symlink's permission bits.
Required executed controls:
- missing or untraversable snapshot/ancestor -> the readback command exits nonzero and no grant step is reached;
- clean root-owned, non-writable, link-free tree -> succeeds;
- bad owner/mode or a symlink in either the ancestor chain or snapshot -> exits nonzero.
The atomic sudoers transaction, root-owned locus construction, grantee-writable-locus refusal, mixed-outcome codec refusal, and census row are otherwise accepted. One non-blocking ledger correction: ntfy_runtime_wire_keys is enforced by the decoder, while the encoder still spells its object members directly; the source should not claim that both directions derive their fields from that table unless the encoder is actually projected from it.
…er derives from the key table Side-chat HOLD on f8beaa6: `test -z "$(find ...)$(find ...)"` took test's status, so a find that failed (missing path, unreadable entry) printed nothing and read as clean. Each find is now captured into its own assignment, whose status bash -e acts on, and only then does test decide between violation and clean. Ancestors are checked for being a symlink explicitly (-type l). The owner is a parameter (helper_snapshot_readback_command_at) so the emitted text can be executed against a scratch tree. Review 5289236032: the JSON encoder's members are now ntfy_runtime_wire_keys mapped through one value function, the same table the decoder admits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Side-chat HOLD on f8beaa6 (fail-open snapshot readback) is fixed in 15f470c. Review 5289236032's encoder note is fixed in the same commit. Fix.
Executed evidence.
Encoder. Its members are now Witness file: 19/19 PASS at this head. The tree is clean. — sent from calm-deer-391 |
briansrls
left a comment
There was a problem hiding this comment.
SOURCE HOLD at exact head 15f470cfc8e25de7befb71d94ec9e61d54a3a733.
The SOURCE repair closes review 5289194100 / 5289236032:
- each
findis now a standalone assignment command, so under the productionset -euo pipefailits nonzero command-substitution status stops the stage before the finaltest; - the final
test -znow distinguishes only clean versus observed violation after both observations completed; - ancestor symlinks are explicitly in the predicate;
- the encoder now projects its members from the same per-arm key table the decoder admits.
The session execution is credible and discriminating. In particular, the old one-command test -z "$(find)$(find)" mutation reproduces the fail-open while this exact emitted form refuses missing/unreadable observations and observed owner/mode/link violations.
One evidence wall remains before landing: enroll the real shell execution.
The statement that the corpus has no effectful claim pattern is not correct. Existing witnesses already execute modeled shell programs through LocalExec, including test.claim.spark_snapshot_verify_wrapper_local_wet (extdeps.languages.bash.invocation::bash_program_command + gunbc.command_runner::run_shell_command_observe) and test.claim.devboot_text_blob_real_execution (run_shell_commands(..., LocalExec) over a temporary filesystem fixture). This PR therefore does not need a new harness or a host mutation.
Why the current shape-only claim is insufficient: the defect is Bash STATUS semantics. Pinning three rendered lines proves the assignments are spelled separately, but does not prove that the actual invocation context honors their status; a future wrapper/context change could preserve those substrings while suppressing errexit. The session receipt will not automatically run after such a change.
Required minimal witness:
- add a local real-execution witness consuming
helper_snapshot_readback_command_atitself; - execute it through the modeled Bash invocation and
LocalExec, withset -euo pipefailand a terminal marker such asGUNBC-GRANT-REACHED; - use the existing scratch-owner parameter so no root or real sudoers path is needed;
- assert a clean observable tree exits zero and reaches the marker;
- assert at least one observation failure (missing snapshot or unreadable subtree) exits nonzero and does not reach the marker;
- assert at least one observed violation (writable entry or symlink) exits nonzero and does not reach the marker.
The already-posted old-form mutation receipt may remain the mutation evidence; it need not become production vocabulary. The source-shape claim should remain as a projection pin beside the executing witness.
Correction for the ledger: this exact head emits bare find, not sudo -n find. That is acceptable now because an inability to traverse is a nonzero observation and fails closed; the PR body/comment should describe the actual command.
I found no additional source blocker in the root-owned snapshot, atomic root:root sudoers replacement, exact grant/argv projection, mixed-arm wire refusal, or privileged-effect census row. Re-request after the enrolled LocalExec claims pass on the committed head.
…alExec over temp trees) Runs helper_snapshot_readback_command_at's rendered text under set -euo pipefail through bash_program_command, run_shell_command_observe and LocalExec. Clean tree: exit 0 and the grant marker. Missing snapshot or ancestor: nonzero, no marker. Writable entry, snapshot symlink or symlinked ancestor: nonzero, no marker. Reverting to the fail-open single test -z form turns both observation-failure claims red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
APPROVE at exact head 454b5852c9af5fa239da58c0efc1a2c581a85110, superseding REQUEST_CHANGES review 5289525416.
The remaining evidence wall is closed. The new enrolled witness consumes helper_snapshot_readback_command_at itself, not a copied shell spelling, and executes the rendered program under the production strict option set through the repository's admitted bash_program_command -> run_shell_command_observe -> LocalExec route.
The six cases discriminate both outcome classes:
- a clean observable tree exits zero and reaches
GUNBC-GRANT-REACHED; - a missing snapshot or missing ancestor exits nonzero and does not reach the marker, proving each assignment propagates its own failed
findthrougherrexitbefore the final emptiness test; - a group-writable entry, a symlink inside the snapshot, or a symlinked ancestor exits nonzero and does not reach the marker, proving successfully completed observations still reject discovered violations.
The mutation reverting the production renderer to the former single test -z "$(find ...)$(find ...)" form makes exactly the two observation-failure claims fail while leaving the clean/violation controls green. That is the right discriminator for the reported fail-open. The existing shape-pinning claim remains useful beside the executing witness.
The ledger correction is also right: production uses bare find; inability to traverse is a nonzero observation and now fails closed. No source claim depends on sudo find.
The earlier accepted walls remain intact: root-owned release-specific helper closure and ancestor check; grantee-writable-locus refusal; root:root 0440 dotted-temp + visudo -c -O -P + atomic mv -T + privileged byte readback; exact release-bound argv/grant; outcome-exclusive JSON grammar with encoder and decoder projected from the same arm key table; and privileged-effect census enrollment. The declared limitation that no witness applies real sudoers state on srv1 remains honest and is not a source blocker.
No further source finding remains in scope. Land after the required exact-head checks complete green; do not apply the operator steps until the landed revision and #12077's helper body/join frontier are both in place.
…, wet-lane enrollment - Review 70484: helper_snapshot_readback_command_at builds its three statements (two fail-fast assignments over command-substituted finds, then test -z) from v2.extdeps.languages.bash_build nodes, serialized fail-closed. The same shape as systemctl_load_state_assignment_intent. The retire sweep is one argv through shell_command_text_of_argv. No string-assembled shell remains, so no dissolve-on marker is needed. The owner is a word: root in production, $(id -un) in the witness. - Floor budget (100982 > 72300): the separate dark-install render claim is deleted. Its three grant conjuncts join test.claim.live_deploy.emit the_dark_install_never_names_the_roadmap_unit, which already renders that script. - Floor route_gap: the real-execution readback witness is enrolled on the local-repo wet lane as the triple (floor_route_gap chunk_22, local_repo_wet_schedule, ci_layer_roots LocalRepoWetLane row with its dissolution), exactly as spark_snapshot_verify_wrapper_local_wet is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Review 70484 is addressed in 4832caf.
— sent from calm-deer-391 |
briansrls
left a comment
There was a problem hiding this comment.
APPROVE at exact head 4832caf4cab02cf8ea50fe87a278dfe862f8c212, rebinding the source approval from 454b5852c9 after the broader CI-driven delta.
The three changes are coherent.
1. Budget repair
Deleting the_dark_install_installs_this_releases_grant is the right repair. Its subject was not a second production interface: it rendered the same approval_broker_dark_install_script already rendered by test.claim.live_deploy.emit::the_dark_install_never_names_the_roadmap_unit. The three helper-grant conjuncts now inspect that one existing rendered value, so the floor pays for one real-route inhabitance render rather than two and no cost-debt row is needed.
This is not analogous to aggregating independent published vectors merely to cross a budget line. The facts are properties of one emitted dark-install transaction. The adjacent source comment makes the broadened inhabitance subject explicit. The existing identity name remains narrower than the whole conjunction, but I do not require a rename that would manufacture a new over-budget witness identity.
2. The LocalRepoWetLane executes in required CI
local_repo_wet_schedule is a roster, not a cron-only schedule.
The generated witnesses workflow invokes claim_executor --required-ci --required-lane witnesses. Inside that required-floor route, the host:
- decodes
v2.workflow.local_repo_wet_terminal.local_repo_wet_schedulewhile constructing the policy closure; - adds every scheduled entry module to the prepared-subject seeds;
- unconditionally calls
run_local_repo_wet_laneagainst that prepared repository; - builds each evaluation frame in
ExecutionMode::Wetand executes the scheduled functions' real local effects; - passes the resulting terminals to
finalize_local_repo_wet_laneagainst the same candidate digest.
A nonempty schedule with no invocation is the typed LocalRepoWetExecutorAbsent refusal. Missing, duplicated, foreign-entry, foreign-function, wrong-verdict, foreign-candidate, and unscheduled terminals also refuse. So these six identities execute co-resident on every required floor; they are not merely listed for a later cadence.
The cadence model confirms the same fact from the other side: LocalRepoWetLane can be enrolled on the PR floor and is explicitly excluded from both ScheduledFalsifier and WetFalsifier. The name local_repo_wet_schedule means the executor roster.
Therefore the triple is the correct home:
- hermetic required-floor execution reaches the exact declared
RunArgv / NoMockResponseroute gap and holds it; - the required floor's own local-repo wet executor produces the same-candidate real terminal;
ci_layer_rootsdeclares the file onLocalRepoWetLane.
That is stronger than a scheduled receipt and is the same mechanism used by the spark snapshot precedent.
3. Bash construction
The dashboard 70484 repair holds. Snapshot readback is now a list of bash_build statement nodes: two fail-fast assignment/cmdsubst observations followed by the grammar-built emptiness test. Each statement is serialized through bash_fold_serialize_node, with rejection producing the poison. The retire sweep is one argv rendered by shell_command_text_of_argv; !, {}, and + are arguments rather than hand-authored syntax. The owner remains a word—literal root in production and the modeled $(id -un) word in the scratch witness.
The reported 77/77 emit population and 6/6 real-execution population are coherent evidence for this delta. No new source finding remains.
Before landing
The PR body is stale and must be corrected without moving the SHA:
- it still cites deleted identity
the_dark_install_installs_this_releases_grant; - it still reports the old grant-test count (
19/19); replace that with the exact current count from the clean-tree run; - describe the six identities' hermetic route-gap + required-CI LocalRepoWetLane realization;
- state that the readback and retire programs now derive from grammar/argv authorities.
At this review, GitHub exposes no check runs for 4832caf4ca, and the PR reports non-mergeable. This is source approval, not authorization to land without an exact-head required floor. Land only after the exact-head required checks execute green and the body ledger is current. Do not apply anything on srv1 until this revision and #12077's helper body/process joins are both landed.
# Conflicts: # src/v2/workflow/floor_route_gap.dag
briansrls
left a comment
There was a problem hiding this comment.
APPROVE at exact head 18b7b5ffe36335d66ea3fa10123c598659a6a05a, rebinding review 5290652773 after the merge of current main.
The head is a two-parent merge commit: parent 1 is the source-approved 4832caf4ca, parent 2 is current main 8a979dbb75, and the commit records exactly one conflict, src/v2/workflow/floor_route_gap.dag.
The conflict resolution preserves the #12124 route-gap population at identity grain. The same six approval_broker_helper_readback_real_execution identities remain RunArgv / NoMockResponse; only their chunk declaration moves from floor_route_gap_expectation_chunk_22 to _chunk_24. Main's own _chunk_22 and _chunk_23 precede it, and _chunk_24 is appended to floor_route_gap_expectation_chunks, so no row was overwritten or dropped.
The PR-relative aggregate remains 15 files, +1606/-18, and the previously approved helper-grant, LocalRepoWetLane, grammar-built readback/retire, root-owned snapshot, atomic sudoers replacement and exclusive wire-codec source is unchanged by this merge resolution.
The clean merged-tree receipts reported on this head — emit_test 78/78, grant test 18/18, real-execution 6/6 and host_capacity_wet 8/8 — are coherent with that union.
No new source finding remains. At review time the PR is open, non-draft and mergeable; compiler, clippy and floor are running and emit-build is queued. Land only after the exact-head required checks finish green. The PR body still carries the pre-budget evidence ledger (deleted the_dark_install_installs_this_releases_grant, 19/19); correct that prose without moving the SHA before landing.
|
Review 70517's operator point: from this release on, every broker release installs a grant for a function that doesn't exist on main yet.
No code change is made here, so the side-chat approval on 18b7b5f stands. — sent from calm-deer-391 |
… to main's text Adding grant conjuncts to emit_test's the_dark_install_never_names_the_roadmap_unit made it a changed claim judged against the new-witness budget: 110041 eval steps vs 72300. emit_test.dag is restored byte-for-byte to main. The dark install's release half is now a named function, approval_broker_dark_install_release_steps (tree copy, then the helper snapshot and grant), which the intent consumes. the_dark_install_composes_this_releases_grant inspects that composition without rendering the orchestrated script: tree steps + exactly 17, with the mv -T rename and the retire sweep at their positions. The existing dark-install claims in emit_test still render the full script, which now carries these steps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
APPROVE at exact head f5104405b0bb7844e66a24fea5409c2ca531d612, superseding approval 5290813155 at 18b7b5ffe3.
The delta is the right budget repair, and it does not weaken the helper-grant walls.
One production composition, not a second render
approval_broker_dark_install_release_steps(spec, revision) is now the production-owned release-half composition:
approval_broker_tree_copy_steps
then
approval_broker_helper_grant_steps
approval_broker_dark_install_intent consumes that exact function. The new claim consumes the same function rather than reconstructing the dark install or rendering its Bash projection again. It derives the tree prefix length from approval_broker_tree_copy_steps, requires exactly seventeen following snapshot/grant steps, and pins the atomic mv -T and final old-snapshot retirement positions. The existing grant claims remain the detailed authority for the seventeen-step transaction.
This is a legitimate DESIGN §3 cut at one interface. It is not aggregation to evade the enrolment gate: the deleted work was a second inspection of a full projection already rendered by the existing dark-install witness module. The new claim's subject is the smaller production composition boundary itself.
Full-projection pairing is preserved
dag/test/claim/live_deploy/emit_test.dag is restored to main's bytes and is absent from the PR's current changed-file population. Its existing dark-install claims still render approval_broker_dark_install_script; on this branch that script reaches approval_broker_dark_install_intent, which reaches the new release composition. Thus the corpus retains the full-projection inhabitance while this PR's changed claim establishes the grant composition without paying another full render.
The delta from the approved head is exactly one commit over three files:
- production extraction/use in
dag/gunbc/live_deploy/emit.dag; - one composition claim in
approval_broker_helper_grant_test.dag; - removal of the eight PR-authored grant lines from
emit_test.dag, restoring it to main.
No sudoers, snapshot-readback, wire-codec, privileged-effect-census, route-gap, local-wet, or executor-grant source changed.
Evidence disposition
I accept the reported grant-file clean run and the prior same-tree six real-execution terminals. The six effectful readback identities remain enrolled through the previously approved hermetic-gap/local-repo-wet construction; this delta does not move those rows.
No further source finding remains. This is source approval, not permission to land before the new cost disposition is demonstrated: require the exact-head floor to finish green and show that the new composition claim is within the new-witness budget, while the six enrolled real-execution identities remain non-blocking through their same-candidate wet terminals.
At review time clippy is green; compiler, floor, and emit-build are still running.
One non-blocking ledger correction: after the main merge the emit witness population was reported as 78/78; the PR body still says 77/77. Keep the body on the exact current receipt before landing, without moving the SHA.
…ifact: #12124's helper contract has landed, and the gap retires only when the classifier makes the four joins Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…checks, run 35875508867) - privileged_effect_census: #12124's approval-broker helper-grant row gains witness_discharge: NoWitnessDischarge. Derived: the effect is reversible, resource-scoped and unbilled, so it owes no witness, and no standing ruling covers it. - bmc_wif_delegation_chain_witness_test (main's): the file carried no imports, so SecretAccessGrant resolved ambiguously between gunbc.auth.secret_access_grant and gunbc.auth.gcp_secret_access. It now names its imports, and list heads are matched as optionals rather than passed where a value is declared. Each claim's meaning is unchanged. - fleet-converge.yml and docs/design-rung-drops.md regenerated from the merged authority. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unblocks #12077's live-process joins. Each broker release install now does four things:
currentpointer and uses no wildcard.What
gunbc.executor_privileged_operation(the roster seam thatgunbc.runner_host_grantsrenders through), generalised rather than forked:RunGunbcEntryAs { run_as, binary, source_roots, entry, function }. Its argv isgunbc.cli_invoke's onegunbc runshape.OperationPrincipalmatch feeds both the sudoers(run_as)and thesudo -uelevation.gunbc.live_deploy.emit, in the broker's dark install, after the release tree:Snapshot (wall 1). Root copies the deploying checkout into
/opt/gunbc/approval-broker-helpers/<sha>withcp -R -P --no-preserve=all -T(root-owned, never follows a link), and the binary withinstall -o root -g root. A readback fails the stage unless the snapshot and every ancestor are root-owned, not group- or world-writable, and link-free. The readback captures eachfindinto its own assignment (a="$(find …)"), soset -estops the stage when an observation FAILS (a missing or unreadable path), and only then doestest -zdecide violation vs clean. The finds run BARE, not undersudo -n find: the snapshot is root-owned 0755, so the deploy principal can traverse it, and an entry it cannot traverse is itself a nonzero observation that fails the stage. The grant names only the snapshot.approval_broker_helper_grant_admission_atrefuses any locus whose owner is the grantee. The release dir is owned bybriansrls, the grantee, so a grant over it cannot be emitted.Grant (wall 2).
visudo -cfthe staged file.install -m 0440 -o root -g rootto/etc/sudoers.d/gunbc-approval-broker-helpers.new.visudo -c -O -P -fthe temp file (syntax plus owner, group and mode).mv -Tit over the final path (onerename(2)).visudo -c -O -P -fthe final path.install, thencmpit against the staged bytes.sudoers(5), Including other files from within sudoers: files in an
#includedirdirectory "that end in the '~' character or contain a '.' character" are skipped. So the temp file is never read by sudo, and an interruption before the rename leaves the previous valid policy in place.Retire. After the rename, the previous release's snapshot is removed (confined to the root-owned snapshot base; keeps exactly this sha).
The drop-in holds exactly one grant line, so the rename replaces the previous release's line.
approval_ntfy_runtime_observe_helper_argv(revision)is pure and exported for Repair #12013: ntfy delivery outcomes, 128-bit code; issuance gated on a live, bound ntfy ACL readback (refuses until #12124) #12077.approval_broker_release_revision_argument(revision)gives--arg release_revision=<sha>so the verb can name its own helper.gunbc.fleet_posix_accounts:SudoCp(/usr/bin/cp) andSudoMv(/usr/bin/mv) joinci_runner_sudo_binaries(parent decision on escalation). They add no privilege class: the deploy drop-in already grants whole-binaryinstallandvisudo, which is root-equivalent.mv -Tis the only atomic rename, andcp -R -Pmakes the root-owned snapshot in one call.gunbc.auth.approval_ntfy_runtime_observe(new) is the helper's contract:gunbc-ntfy;NtfyOpenFd { target, target_inode }record per fd and verbatim readlink targets;extdeps.languages.jsongrammar, with one key table per outcome read by both directions, so mixed-outcome objects refuse (review 70435).gunbc.auth.privileged_effect_censusgets a row forapproval_broker_helper_grant_steps. The selection's verdict isConforms(recurring automated effect, realized by the dispatched run's host principal, resource-scoped), and a claim pins it (review 70452).Declared frontier (§3c): the helper entry's body and the four joins land in #12077, in
gunbc.auth.approval_ntfy_runtime_observe. Until then the helper fails at resolve, which is loud.Evidence
gunbc run --source-root dag --source-root src/v2 --entry dag/test/claim/live_deploy/approval_broker_helper_grant_test.dag --claim-run: 19/19 PASS at f510440, plusdag/test/claim/live_deploy/approval_broker_helper_readback_real_execution_test.dag6/6 PASS (enrolled on the local-repo wet lane:floor_route_gapchunk_24,local_repo_wet_schedule, and aci_layer_rootsLocalRepoWetLane row).test.claim.live_deploy.emitis unchanged from main.installing_release_b_leaves_no_line_for_release_aa_grant_for_another_release_does_not_authorize_this_onea_sudoers_special_word_refuses_the_granta_grantee_writable_locus_gets_no_grantthe_root_owned_snapshot_locus_is_admittednothing_the_grant_executes_is_in_the_grantee_owned_release,the_snapshot_readback_covers_every_ancestor_and_refuses_linksthe_grant_replaces_atomically_and_reads_back_identity_and_bytes(root:root,-O -P,mv -T, privileged read, cmp)only_the_renamed_file_is_ever_included_by_sudothe_helper_argv_is_pinned_word_for_word,the_grant_line_is_pinnedthe_dark_install_composes_this_releases_grant, overapproval_broker_dark_install_release_steps, the composition the dark-install intent consumes: tree steps plus exactly the 17 snapshot and grant steps. The unchanged dark-install claims intest.claim.live_deploy.emitstill render the full script, which carries them. Rendering it inside a changed claim cost 100,982 and then 110,041 eval steps against the 72,300 new-witness budget, so this PR does not do that and admits no cost debt.the_helper_grant_site_conforms_in_the_privileged_effect_censusapproval_broker_helper_readback_real_execution_test, 6/6)helper_snapshot_readback_command_atand runsset -euo pipefail; <readback>; echo GUNBC-GRANT-REACHEDthroughbash_program_command+run_shell_command_observe+LocalExecover mktemp trees: clean → exit 0 + marker; missing snapshot / missing ancestor → nonzero, no marker; writable entry / snapshot symlink / symlinked ancestor → nonzero, no markerMutations:
test -z "$(find)$(find)"form: the enrolled real-execution claimsa_missing_snapshot_stops_before_the_grant_by_real_executionanda_missing_ancestor_stops_before_the_grant_by_real_executiongo red, which is the fail-open reproduced. The other four stay green, as they should.RunGunbcEntryAsrunning as root: 3 claims go red.a_grantee_writable_locus_gets_no_grantgoes red.Both were reverted.
Limits of this evidence: the snapshot readback runs for real (above), but no witness runs real
visudoorsudoagainst a host. The file-identity and atomicity controls pin the emitted argv;visudo -c -O -Pand the ownership readback are the walls on srv1.Regression:
emit_testoperations_witness_testfleet_converge_privilege_holdapproval_ntfy_convergeauthorization_pattern_selectionBaseline reds, identical on origin/main with the same binary:
executor_privileged_operation(1),runner_host_deploy(1),fleet_converge_plan(17), and agrounded_principalresolve error inextdeps/tailscale/acl.dag.Operator-applied steps (nothing is applied to srv1 by this PR)
gunbc.ci_deploy_sudoers deploy_sudoers_install_shell, which renders/etc/sudoers.d/gunbc-deployfromci_runner_sudo_binaries) now adds/usr/bin/cpand/usr/bin/mvforghrunneron srv1. It is applied by the first deploy that runs this change's preamble, which the operator dispatches. Confirm it withsudo -n -las ghrunner, which lists both./opt/gunbc/approval-broker-helpers/<sha>(root-owned; the stage fails if any entry or ancestor is not), installs/etc/sudoers.d/gunbc-approval-broker-helpers(root:root 0440) atomically, and reads back its identity and bytes. It removes the previous release's snapshot.briansrls, runsudo -n -l <helper argv>, which should list the command. Then run the enrolment-code verb.🤖 Generated with Claude Code