Skip to content

Approval broker: per-release derived helper sudoers grant + ntfy runtime-observe helper contract - #12124

Merged
gunbai-bot[bot] merged 9 commits into
mainfrom
session/calm-deer-391
Sep 23, 2026
Merged

gunbai-bot[bot] merged 9 commits into
mainfrom
session/calm-deer-391

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Unblocks #12077's live-process joins. Each broker release install now does four things:

  • derives one exact sudoers line for that release's helper invocation;
  • runs the helper from a root-owned snapshot the grantee cannot write;
  • replaces the previous release's line atomically, with owner/mode/bytes readback;
  • keeps no current pointer and uses no wildcard.

What

  • gunbc.executor_privileged_operation (the roster seam that gunbc.runner_host_grants renders through), generalised rather than forked:
    • New variant RunGunbcEntryAs { run_as, binary, source_roots, entry, function }. Its argv is gunbc.cli_invoke's one gunbc run shape.
    • An exhaustive OperationPrincipal match feeds both the sudoers (run_as) and the sudo -u elevation.
    • Root operations render byte-for-byte as before (pinned).
  • gunbc.live_deploy.emit, in the broker's dark install, after the release tree:
    1. Snapshot (wall 1). Root copies the deploying checkout into /opt/gunbc/approval-broker-helpers/<sha> with cp -R -P --no-preserve=all -T (root-owned, never follows a link), and the binary with install -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 each find into its own assignment (a="$(find …)"), so set -e stops the stage when an observation FAILS (a missing or unreadable path), and only then does test -z decide violation vs clean. The finds run BARE, not under sudo -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_at refuses any locus whose owner is the grantee. The release dir is owned by briansrls, the grantee, so a grant over it cannot be emitted.

    2. Grant (wall 2).

      1. Stage the file.
      2. visudo -cf the staged file.
      3. install -m 0440 -o root -g root to /etc/sudoers.d/gunbc-approval-broker-helpers.new.
      4. visudo -c -O -P -f the temp file (syntax plus owner, group and mode).
      5. mv -T it over the final path (one rename(2)).
      6. visudo -c -O -P -f the final path.
      7. Read the final file back into the stage with a privileged install, then cmp it against the staged bytes.

      sudoers(5), Including other files from within sudoers: files in an #includedir directory "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.

    3. Retire. After the rename, the previous release's snapshot is removed (confined to the root-owned snapshot base; keeps exactly this sha).

    4. The drop-in holds exactly one grant line, so the rename replaces the previous release's line.

  • gunbc.fleet_posix_accounts: SudoCp (/usr/bin/cp) and SudoMv (/usr/bin/mv) join ci_runner_sudo_binaries (parent decision on escalation). They add no privilege class: the deploy drop-in already grants whole-binary install and visudo, which is root-equivalent. mv -T is the only atomic rename, and cp -R -P makes the root-owned snapshot in one call.
  • gunbc.auth.approval_ntfy_runtime_observe (new) is the helper's contract:
    • entry identity, and run-as gunbc-ntfy;
    • the typed observation, with one NtfyOpenFd { target, target_inode } record per fd and verbatim readlink targets;
    • the wire: one JSON object through the modeled extdeps.languages.json grammar, with one key table per outcome read by both directions, so mixed-outcome objects refuse (review 70435).
  • gunbc.auth.privileged_effect_census gets a row for approval_broker_helper_grant_steps. The selection's verdict is Conforms (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, plus dag/test/claim/live_deploy/approval_broker_helper_readback_real_execution_test.dag 6/6 PASS (enrolled on the local-repo wet lane: floor_route_gap chunk_24, local_repo_wet_schedule, and a ci_layer_roots LocalRepoWetLane row). test.claim.live_deploy.emit is unchanged from main.

Control Claim
RED: stale release's line survives installing_release_b_leaves_no_line_for_release_a
RED: grant names a different sha a_grant_for_another_release_does_not_authorize_this_one
RED: malformed line a_sudoers_special_word_refuses_the_grant
RED: grantee-writable closure gets no grant a_grantee_writable_locus_gets_no_grant
root-owned closure gets a grant the_root_owned_snapshot_locus_is_admitted
source byte replaced after install is impossible nothing_the_grant_executes_is_in_the_grantee_owned_release, the_snapshot_readback_covers_every_ancestor_and_refuses_links
root:ghrunner refused; atomic replace; readback the_grant_replaces_atomically_and_reads_back_identity_and_bytes (root:root, -O -P, mv -T, privileged read, cmp)
interruption before rename leaves the previous policy; only the renamed file is included only_the_renamed_file_is_ever_included_by_sudo
argv pin the_helper_argv_is_pinned_word_for_word, the_grant_line_is_pinned
real route the_dark_install_composes_this_releases_grant, over approval_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 in test.claim.live_deploy.emit still 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.
census the_helper_grant_site_conforms_in_the_privileged_effect_census
readback by real execution (approval_broker_helper_readback_real_execution_test, 6/6) renders helper_snapshot_readback_command_at and runs set -euo pipefail; <readback>; echo GUNBC-GRANT-REACHED through bash_program_command + run_shell_command_observe + LocalExec over mktemp trees: clean → exit 0 + marker; missing snapshot / missing ancestor → nonzero, no marker; writable entry / snapshot symlink / symlinked ancestor → nonzero, no marker
codec round trips, plus every malformed/mixed-outcome input refused

Mutations:

  • Readback reverted to the pre-fix single test -z "$(find)$(find)" form: the enrolled real-execution claims a_missing_snapshot_stops_before_the_grant_by_real_execution and a_missing_ancestor_stops_before_the_grant_by_real_execution go red, which is the fail-open reproduced. The other four stay green, as they should.
  • RunGunbcEntryAs running as root: 3 claims go red.
  • Dropping the grantee-owns-locus refusal: a_grantee_writable_locus_gets_no_grant goes red.

Both were reverted.

Limits of this evidence: the snapshot readback runs for real (above), but no witness runs real visudo or sudo against a host. The file-identity and atomicity controls pin the emitted argv; visudo -c -O -P and the ownership readback are the walls on srv1.

Regression:

Witness file Result
emit_test 78/78 (after the main merge; the file is byte-identical to main)
operations_witness_test 18/18
fleet_converge_privilege_hold 5/5
approval_ntfy_converge 12/12
authorization_pattern_selection no FAIL lines

Baseline reds, identical on origin/main with the same binary: executor_privileged_operation (1), runner_host_deploy (1), fleet_converge_plan (17), and a grounded_principal resolve error in extdeps/tailscale/acl.dag.

Operator-applied steps (nothing is applied to srv1 by this PR)

  1. Deploy-principal roster. The deploy preamble (gunbc.ci_deploy_sudoers deploy_sudoers_install_shell, which renders /etc/sudoers.d/gunbc-deploy from ci_runner_sudo_binaries) now adds /usr/bin/cp and /usr/bin/mv for ghrunner on srv1. It is applied by the first deploy that runs this change's preamble, which the operator dispatches. Confirm it with sudo -n -l as ghrunner, which lists both.
  2. Dispatch the broker dark install for the release. It builds /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.
  3. After Repair #12013: ntfy delivery outcomes, 128-bit code; issuance gated on a live, bound ntfy ACL readback (refuses until #12124) #12077 lands the entry body: on srv1, as briansrls, run sudo -n -l <helper argv>, which should list the command. Then run the enrolment-code verb.
  4. Each later broker release dispatch replaces the line and retires the old snapshot. There is nothing to clean up by hand.

🤖 Generated with Claude Code

…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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Brian Searls and others added 2 commits September 23, 2026 08:23
…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>
@gunbai-bot

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Review 70435, both findings are addressed in 27e7859.

  1. Mixed-outcome decode.
    • ntfy_runtime_wire_keys(arm) is one table per outcome, and both directions read it.
    • After outcome selects the arm, ntfy_runtime_wire_stray_member refuses any member outside that arm's keys: the other arm's keys, an unknown key, or a repeat. The same applies to each open_fds element.
    • New REDs in the_decoder_refuses_every_malformed_text: an observed object carrying read/cause, and a refused object carrying main_pid/open_fds, are both undecodable. Positive controls for both arms are included.
  2. Hand-kept two-direction grammar. The line codec is deleted. The wire is now one JSON object, written by extdeps.languages.json.emit serialize_json and read by extdeps.languages.json.parse parse_json_document: the repo's modeled grammar, which already covers both directions. This module now owns only the key table and the field mapping. The newline-refusal arm is gone, because JSON escaping carries every code point, and a newline or quote in a target round-trips verbatim (an_observation_round_trips_verbatim).

Witness file: 14/14 PASS locally. fd63b73 also fixes the floor red: extdeps.tools.diffutils now imports String from std.string_type (AmbiguousBareNameRead).

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>
@gunbai-bot

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Review 70452 is addressed in f8beaa6.

  • gunbc.auth.privileged_effect_census now has a row for gunbc.live_deploy.emit approval_broker_helper_grant_steps. The effect is recurring (every broker release), reversible by reapply, and automated. It is executed by the dispatched run's host principal (ghrunner, gunbc.ci_deploy_access ci_deploy_srv1_local_access), and it mints a resource-scoped authorization: one exact argv, as one account, for one release.
  • select_authorization_pattern runs over it, and the verdict is Conforms. No DivergenceReason is needed.
  • the_helper_grant_site_conforms_in_the_privileged_effect_census reads the verdict from census_verdicts() rather than restating it.

The same head also lands side-chat walls 1 and 2 (root-owned helper snapshot; atomic root:root sudoers replace with visudo -c -O -P and a byte readback), and imports Unit/Bool/Int in extdeps.tools.diffutils for the floor's AmbiguousBareNameRead. The PR body is updated.

— sent from calm-deer-391

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with visudo -c -O -P, atomically renamed with mv -T, checked again, and read back through a privileged copy before cmp;
  • 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 survived

find 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:

  1. Perform the observations with authority sufficient to traverse the root-owned snapshot (or otherwise prove traversal is always available).
  2. Propagate each observation command's nonzero status independently before judging its output. For example, two separate assignment commands under set -e followed by the emptiness test, or an explicit typed/branching refusal; do not hide the reads inside the successful test command.
  3. 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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with mv -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 continues

Make 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:

  1. missing or untraversable snapshot/ancestor -> the readback command exits nonzero and no grant step is reached;
  2. clean root-owned, non-writable, link-free tree -> succeeds;
  3. 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>
@gunbai-bot

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

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. helper_snapshot_readback_command_at emits three lines:

_gunbc_helper_ancestors="$(find / /opt /opt/gunbc /opt/gunbc/approval-broker-helpers -maxdepth 0 \( ! -user root -o -perm /022 -o -type l \) -print)"
_gunbc_helper_snapshot="$(find <snapshot> \( ! -user root -o -perm /022 -o -type l \) -print)"
test -z "${_gunbc_helper_ancestors}${_gunbc_helper_snapshot}"
  • The status of an assignment is its command substitution's status, so -e stops the stage on a failed find.
  • test only decides violation vs clean after both observations succeeded.
  • Ancestor symlinks are rejected by -type l; find's default -P does not follow a starting point.
  • No sudo find is needed: every directory in the snapshot is root-owned 0755, so the deploy principal can traverse it. An entry it cannot read is an observation failure, which stops the stage.

Executed evidence.

  • Method:
    1. Render the emitted text through a probe entry (gunbc run ... --function main calling helper_snapshot_readback_command_at), with the owner parameter set to the session user and a scratch tree whose ancestors are two user-owned 0755 dirs.
    2. Run it as bash -c "set -e; <emitted>; echo GRANT_REACHED".
    3. Rerun the missing and clean cases under the production set -euo pipefail, with the same results.
  • Results:
case exit grant step reached
(1) snapshot missing 1 no
(1) unreadable subdir (chmod 000) 1 no
(1) ancestor missing 1 no
(2) clean tree 0 yes
(3) group-writable file 1 no
(3) symlink in snapshot 1 no
(3) world-writable ancestor 1 no
(3) symlinked ancestor 1 no
(4) MUTATION: the pre-fix single test -z "$(find)$(find)" form, snapshot missing 0 yes (fail-open)
(4) MUTATION: same form, unreadable subdir 0 yes
  • The mutation rows reproduce the reported defect. The fixed form turns both red.
  • Honest limit: this run is a session execution, not an enrolled witness. --claim-run test functions here are pure, and I found no effectful claim pattern to enroll a process run in. The enrolled claim the_snapshot_readback_covers_every_ancestor_and_refuses_links pins the emitted three-line shape: the separate assignments, -type l on the ancestors, and the final test -z. So a change back to the fail-open form turns that claim red.

Encoder. Its members are now ntfy_runtime_wire_keys(arm) mapped through one value function per arm (and per open_fds element), the same table the decoder admits.

Witness file: 19/19 PASS at this head. The tree is clean.

— sent from calm-deer-391

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SOURCE HOLD at exact head 15f470cfc8e25de7befb71d94ec9e61d54a3a733.

The SOURCE repair closes review 5289194100 / 5289236032:

  • each find is now a standalone assignment command, so under the production set -euo pipefail its nonzero command-substitution status stops the stage before the final test;
  • the final test -z now 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_at itself;
  • execute it through the modeled Bash invocation and LocalExec, with set -euo pipefail and a terminal marker such as GUNBC-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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 find through errexit before 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>
@gunbai-bot

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Review 70484 is addressed in 4832caf.

  • Retire sweep: now one argv rendered by shell_command_text_of_argv, the same argv authority every other deploy command uses: 'find' '/opt/gunbc/approval-broker-helpers' '-mindepth' '1' '-maxdepth' '1' '!' '-name' '<sha>' '-exec' '/usr/bin/sudo' '-n' '/usr/bin/rm' '-rf' '--' '{}' '+'. No string-built shell remains.
  • Readback: helper_snapshot_readback_command_at now builds its three statements from v2.extdeps.languages.bash_build nodes (bash_build_assign + bash_build_word_cmdsubst over the find commands, then test -z over a concat of the two vars). It serializes them through bash_fold_serialize_node with a refusing poison arm, the same shape as systemctl_load_state_assignment_intent.
  • Both findings asked for a capability-named dissolve-on marker. With no string assembly left, there is no marker to name. The rendered output is pinned by the shape claim and executed by approval_broker_helper_readback_real_execution (6/6).

— sent from calm-deer-391

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. decodes v2.workflow.local_repo_wet_terminal.local_repo_wet_schedule while constructing the policy closure;
  2. adds every scheduled entry module to the prepared-subject seeds;
  3. unconditionally calls run_local_repo_wet_lane against that prepared repository;
  4. builds each evaluation frame in ExecutionMode::Wet and executes the scheduled functions' real local effects;
  5. passes the resulting terminals to finalize_local_repo_wet_lane against 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 / NoMockResponse route gap and holds it;
  • the required floor's own local-repo wet executor produces the same-candidate real terminal;
  • ci_layer_roots declares the file on LocalRepoWetLane.

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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gunbai-bot

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 74d318f Sep 23, 2026
5 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/calm-deer-391 branch September 23, 2026 14:21
@briansrls
briansrls restored the session/calm-deer-391 branch September 23, 2026 14:27
gunbai-bot Bot pushed a commit that referenced this pull request Sep 23, 2026
…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>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 23, 2026
…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>
gunbai-bot Bot pushed a commit that referenced this pull request Oct 7, 2026
…ci_runner_sudo_binaries (roster grew 6->10 via #10357, #10525, #12124; no grant lost)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant