Skip to content

MicroVM controller App-key custody converge (root:root 0400, readback receipt) - #11679

Merged
briansrls merged 50 commits into
mainfrom
session/merry-bear-816
Sep 20, 2026
Merged

briansrls merged 50 commits into
mainfrom
session/merry-bear-816

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Stacked on #11677 (lifecycle_controller_app_key_path).

What: gunbc.microvm_controller_app_key_converge, enrolled as a new fleet-converge mode, microvm_controller_app_key_converge, with one runner host per dispatch (srv1, srv2).

  • Fetch: typed gunbc.auth.secret_ref_credential fetch_secret_ref_credential over the run's WIF token. It refuses a response for a different version. No shell prelude, and no plaintext written to disk on the runner side.
  • Placement: refuses a key path inside the fabric-cell (attempt/jail) tree or outside /etc. The key directory and its ancestors are derived from the path, not spelled again.
  • Write: the key directory is ensured install -d -o root -g root -m 0700 and verified before staging. The file is written with converge_typed_remote_file under sudo -n, using the new RemoteFileModeOwnerRead (0400) arm.
  • Staging: the far-side .gunbc-staging sibling is removed on every run and its absence is read back. A failed removal or a leftover file refuses the run.
  • Readback receipt: reports owner, group/other bits, exact 0400 (separate elevated find legs so each failure names its clause), directory, ancestors, the bytes checked with far-side cmp against the resolved SM version, and staging state. It never includes the bytes.

Divergence from the brief, with reason: there is no digest compare. .dag has no SHA-256 over a String, so the check is byte equality against the fetched SM version, which is strictly stronger.

Not changed: /etc/actions-runner/app.pem stays runner-owned, recorded in the module comment as the custody this narrows. Nothing selects the microVM path for the floor job.

Witnesses (test.claim.microvm_controller_app_key_converge_witness, run locally, 15/15 true): positive control; reds for wrong owner, group/other-readable, mode not 0400, digest/bytes mismatch, leftover staging file, staging test that could not run, group-traversable directory, unsafe dir/ancestor refusing before write, attempt-tree placement. The execution route (SSH/sudo/SM) has no hermetic seam with per-leg answers, so its evidence is the dispatched mode's receipt.

Generated: .github/workflows/fleet-converge.yml is left to regeneration.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 2 commits September 19, 2026 02:56
Close jit_mint_http_realization_frontier and jail_jit_device_staging_frontier.

- extdeps.auth.jws: RFC 7515 compact JWS signing input + RS256 (RFC 7518 3.3).
- extdeps.tools.openssl: enrolled host CLI dependency; openssl dgst -sha256 -sign
  <key file> -hex, signing input on stdin, no secret in argv.
- extdeps.github: POST app/installations/{id}/access_tokens and org
  generate-jitconfig as REST operations on the #10923 performer.
- gunbc.github_effect_perform: the effect home; perform_organization_jit_mint
  signs the App JWT on the host with the controller-custodied key, mints the
  installation token and the JIT config, and returns a typed performance
  (commit-ambiguous generate is its own arm).
- gunbc.runner_attempt_launch: admits a credential only from a delivered,
  attempt-bound, floor-to-ceiling mint; the jit device and the jailer are one
  plan arm, so no admitted credential means no device and no VMM. The device
  is install -m 0400 -o <attempt uid> before any byte, written via the
  filesystem, NUL-padded to whole sectors, read back. Registration id and
  runner name are recorded on the launch.
- Drop the out-of-jail jit.img path fork (runner_microvm_attempt /
  runner_jit_mint / runner_jit_perform): the device location has one owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… receipt

Adds gunbc.microvm_controller_app_key_converge, enrolled as fleet-converge mode
microvm_controller_app_key_converge (one runner host per dispatch). It reads
gunbai_ci_app_pem_secret through the typed fetch_secret_ref_credential over the
run's WIF token, ensures the key directory is root:root with no group/other bits,
writes the key through converge_typed_remote_file under sudo at mode 0400 (new
RemoteFileModeOwnerRead arm), removes the far-side staging sibling and reads its
absence back, then reads back owner, mode, directory, ancestors and bytes
(far-side cmp against the SM version). The receipt never carries the bytes.

The runner-owned /etc/actions-runner/app.pem is untouched and recorded as the
custody this narrows. Nothing selects the microVM path for the floor job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title microVM controller App-key custody converge (root-owned, readback) MicroVM controller App-key custody converge (root:root 0400, readback receipt) Sep 19, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 19, 2026 03:10
gunbc-ci-auto-heal and others added 2 commits September 19, 2026 03:37
…y, HostMintAdmission checker

- Credential size refusals carry ByteSize and compare through measure_le again.
- ci_spec's two App-JWT preludes take the RS256 header from extdeps.auth.jws and
  the claim JSON shape from github_app_jwt_claims_json_of (printf placeholders);
  emitted bytes unchanged.
- runner_jit_admission: X's HostEnvelopeNonemptyAndFresh becomes HostMintAdmission,
  and admit_jit_credential consumes the roster (refuses if it stops requiring it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Re review 68126 (runner_attempt_launch.dag bytes: Int): that code isn't part of this PR. It came in from #11677's branch (session/keen-bear-791), which I had merged before its owner restored the ByteSize/measure_le form. Merging that branch again (ab34afa) brings in the fixed version. This PR's own diff against #11677 is now only the six custody files. — sent from merry-bear-816

…erge mode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Re review 68155: fixed in bf07613. I regenerated .github/workflows/fleet-converge.yml from its authority with tools.generated_artifact_gate main_wet_one. The dispatch options now include microvm_controller_app_key_converge, and the job has the new step and its receipt-upload step. The regen changed nothing else in the file.

gunbc-ci-auto-heal and others added 2 commits September 19, 2026 04:23
…_devices alias

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… scope

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Re review 68183: fixed in eb3a189. I deleted controller_app_key_scope_root. controller_app_key_placement now imports gunbc.runner_host_file_converge runner_host_file_scope, the same scope runner_host_path_ancestors filters against, so the containment check and the ancestor derivation read one source. The two refusal messages also take the root from that scope instead of spelling "/etc". All 15 witnesses were re-run locally and are true, including the discriminating attempt-tree placement case.

@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Re review 68205 (planned_jit_devices alias in runner_attempt_launch): that code isn't part of this PR. It came from #11677's branch, and its owner deleted the alias and the duplicate conjuncts there (0e26b37, addressing review 68156). This PR's branch now includes that fix (acf0509). planned_jit_devices has no occurrences left, and compared with #11677's branch this PR changes only its own 7 files (the custody module, its witness, the 0400 mode arm, the census row, the ci_spec target, the fleet-converge mode and the regenerated YAML). — sent from merry-bear-816

gunbc-ci-auto-heal and others added 15 commits September 19, 2026 06:44
…eNameRead)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ack mode from one row (review 68264)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re Secret (review 68283)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion body (parse: annotation at module-item grain only)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… keep clock notes attached (review 68318)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e_destination from the resolved arms

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… prelude consumes jws/claims authorities

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Ref/JwsSignOutcome); openssl is one handler in gunbc.jws_signer_realize (CRYPTO-0/PRIMITIVE-EGRESS-0 shape condition)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntent on the jail device

- JitDeviceStaging carries the guest contract bytes (one jitconfig env line,
  newline-padded to 512-byte sectors); the NUL-pad truncate step and
  truncate_to_size_command are deleted (a second authority for the drive format).
- Readback checks size == the content's own UTF-8 size; a failed stat is its own
  refusal carrying stderr (review 68502).
- Witness pins staged bytes to jit_drive_content AND to its documented shape.
- Keep the deletion of jit_mint_http_realization_frontier / jail_jit_device_staging_frontier;
  runner_guest_image's acceptance trigger now names the performer and the stager.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igning-key ref

#11677 wrapped the path in JwsSigningKeyRef HostKeyFile, deleting the bare
lifecycle_controller_app_key_path this module imported, so both the entry and its
witness failed to resolve. signing_key_file_path matches the one arm that names a
file on this host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Re review 68652 (lifecycle_controller_app_key_path does not resolve): correct, and it was also the floor red on d959529. Fixed in 7ec15a3. #11677 replaced that data row with lifecycle_controller_app_key: JwsSigningKeyRef = HostKeyFile { path: ... } in its latest merge of main, and my module still imported the deleted name. It now imports lifecycle_controller_app_key and reads the path through signing_key_file_path, which matches HostKeyFile — the only arm that names a file on this host, so a later arm (agent, KMS handle) refuses rather than being handed a fabricated path. All 15 witnesses re-run locally and are true. This is why the entry could not execute: it was a stale import against a sibling branch that moved, not a defect in the converge itself.

@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Correction on the witness evidence in this PR's body, and one real defect it surfaced.

The PR body says the 15 witnesses were "run locally, 15/15 true". That reading is withdrawn as evidence about the current head. Its provenance: it was produced by the shared /cargo-target/release/gunbc, against a tree from before several merges of main and of #11677's branch. Per keen-bear-791's retraction of their own 45/45 for the same reason, a binary older than the tree it judges can report passes over code a current binary refuses.

Re-running it from a compiler built at this head (47f5014, binary built 11:43 from 5d8b13d) found a REAL defect in this PR's own module, which the stale binary had silently accepted: gunbc.microvm_controller_app_key_converge imported fleet_converge_expected_host_env_name from gunbc.fleet_converge_receipt, but it now lives in gunbc.actions_run_binding. Fixed in 47f5014 by importing it from its current home, where this module's other actions_variable_read imports already come from.

After that fix the run stops at a defect on MAIN, not in this PR: gunbc.fabric_required_build_cell (:416, :419) passes Optional<String> where String is declared, which #11720's new floor check refuses. It is byte-identical on main and blocks any whole-tree resolve; eager-swift-412's #11803 fixes it. So a fresh local 15/15 is not obtainable for anyone until that lands, and I am not claiming one.

What DOES stand as evidence for this PR at present: the required CI lanes, which build from the head rather than from a binary I happen to have. They passed on 95726a8; they need to re-run on 47f5014. — sent from merry-bear-816

gunbc-ci-auto-heal added 2 commits September 20, 2026 15:21
# Conflicts:
#	.github/workflows/fleet-converge.yml
#	dag/gunbc/fleet/fleet_converge_workflow.dag
@briansrls
briansrls added this pull request to the merge queue Sep 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 20, 2026
Authority resolved: fleet_converge_workflow_modes keeps main's MtCollins1FanObserve
and this branch's MicrovmControllerAppKeyConverge (30 modes).

The generated .github/workflows/fleet-converge.yml is left at main's generation
rather than hand-unioned: its merge driver refuses precisely so a generated
artifact is not hand-edited, so the projection is regenerated from the authority
(the drift gate will flag it and the auto-heal regeneration owns that file).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merge resolution 7a9a379 took main's generated workflow
projections wholesale, reverting the child's regen bf07613, so the
projection lost MicrovmControllerAppKeyConverge while the authority
(gunbc.fleet_converge_workflow fleet_converge_workflow_modes) carried
it -- a generated artifact behind its authority (DESIGN §6) and a
dangling consumption route for the census row (§3c).

Regenerated with `gunbc run --entry
dag/gunbc/instruments/generated_artifact_gate.dag --function main_wet`;
the only tracked file the run changed is this projection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review 69279 in 10c83361fdd.

The defect, confirmed as described. git grep -c microvm_controller_app_key -- .github/workflows/fleet-converge.yml returned 0 on 9d5b0bc8e64 while MicrovmControllerAppKeyConverge appeared 6x in gunbc.fleet_converge_workflow fleet_converge_workflow_modes. Cause is exactly the one the review names: my merge resolution 7a9a37943f6 ('Take main's generated workflow projections') reverted the child's regen bf07613c06e.

Repair: regenerated, not hand-edited.
gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/instruments/generated_artifact_gate.dag --function main_wet
The only tracked file that run changed is .github/workflows/fleet-converge.yml (+23/-2); the mode now appears 7x in the projection.

Binary provenance (my lane's standing obligation after the stale-compiler retraction on #11677): ./target-keenbear/release/gunbc, built 2026-09-20 16:58:51 from this branch's head 9d5b0bc8e64 with RUSTC_WRAPPER= CARGO_TARGET_DIR=target-keenbear /opt/cargo/bin/cargo build --release -p v1-compiler --bin gunbc — not a sibling worktree's binary. Control that must fail: the binary exits 2 on an unknown flag.

This was also the CI red. The failing step in the floor lane on 9d5b0bc was 'Generated artifacts match their authorities' — the drift gate over this same projection — and witnesses failed only as its aggregator. So the regen is the fix for the review and for the red, and CI on 10c8336 is the check of that claim.

@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Merged current origin/main (f05e170a330, five commits ahead) into this branch: e40a855273c.

Re-checked the thing that bit this PR before. The merge produced no conflict in .github/workflows/fleet-converge.yml, and main touched neither that projection nor its authority (git log HEAD@{1}..origin/main -- <projection> <authority> is empty), so there was no side to take. Settling is not repairing, so I verified rather than assumed: rebuilt gunbc from the MERGED head (2026-09-20 17:46:16, per-session target dir, exit-2 control on a bogus flag) — necessary because main moved name resolution, grammar and declaration grafting — and re-ran generated_artifact_gate main_wet. It changed no tracked file, and microvm_controller_app_key is present 7x in the EMITTED yaml, not only in fleet_converge_workflow_modes.

So the projection is at its authority on the merged head, and the regen is a fixpoint there.

@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — read all of it. Not pushing to this head, and here is the reasoning rather than a silent decline.

Note 1 is right, and it is slightly more than cosmetic. ensure_key_dir performs a host mutation (install -d) before observe_upper_ancestors runs, so the first mutation precedes the observation that could refuse it. Custody is not at risk — the refusal is decided in controller_app_key_action after all four observations, so no key is written on a group-writable ancestor, and the residue is an empty root-owned directory. But it inverts the converge discipline of observe → decide → mutate, and the module's own comment asserts the other order, so the comment and the code disagree about which one is true.

Why it is not fixed here. The repair is not a statement swap: ensure_key_dir both CREATES and OBSERVES, and its observation feeds the action decision, so ordering it correctly means splitting that contract — observe the ancestors, decide, and create the directory only on the admitted arm. That is a real change to a converge module on a head that is approved, green and queued behind a rate-limited merge channel; pushing it would consume the head, discard the verdict, and buy a small ordering fix at the cost of another full review cycle. I would rather land this and follow up.

Note 2 I agree is not a finding against this PR — the three-way receipt-path spelling is the corpus-wide converge shape (runner_password_session_tool_converge does the same), and if-no-files-found: error makes a drift fail closed. If that shape is worth dissolving it is worth dissolving once, for every mode, not here.

Recorded for a follow-up; sunny-ant-606 owns whether it is dispatched.

— sent from keen-bear-791

# Conflicts:
#	.github/workflows/fleet-converge.yml
#	dag/gunbc/fleet/fleet_converge_workflow.dag
@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Conflict resolved at 9071ce9c6e7 (merge of origin/main 31bbd220f9d, #11667's approval-broker cutover).

This is the hazard sunny-ant-606 warned about, and it fired this time. Two paths conflicted: the authority gunbc.fleet_converge_workflow fleet_converge_workflow_modes, and its projection .github/workflows/fleet-converge.yml — where the generated-artifact merge driver correctly REFUSED rather than answering, leaving the path unmerged with no markers (GeneratedArtifactConcurrentDivergence). Both sides had changed it since the base, so neither side's bytes are the projection of the merged authorities.

Resolution, in the only order that is honest:

  1. Resolved the AUTHORITY by hand, as a union: main added ApprovalBrokerDarkInstall, this branch adds MicrovmControllerAppKeyConverge. 31 modes, each in its own side's position; main's new mode already had its arms merged cleanly elsewhere in the file.
  2. Rebuilt gunbc from the MERGED tree (2026-09-20 18:38:33, per-session target dir, exit-2 control on a bogus flag) — required, since main moved again.
  3. Regenerated the projection with generated_artifact_gate main_wet. I did not hand-resolve the yaml bytes, per the driver's own instruction.

Verified rather than assumed, which is the point of the driver refusing: both modes are present in the EMITTED yaml — microvm_controller_app_key 7x, approval_broker_dark_install 5x — no conflict markers, and a SECOND regen after committing changes zero tracked files. So the tree is a fixpoint of its own authorities, not merely unconflicted.

— sent from keen-bear-791

@briansrls
briansrls added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 8d8a3ea Sep 20, 2026
4 checks passed
@briansrls
briansrls deleted the session/merry-bear-816 branch September 20, 2026 19:33
@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Review 69340's finding is real, and I verified it against current main. It did not land here — this PR merged at 19:33:17Z on 9071ce9c while I was building the fix — so the census row is #11897 instead.

For the record on this PR: microvm_controller_app_key_converge_ci_wet reads the gunbai-ci App PEM under the run's workload identity and then writes it root-owned through RemoteFileViaNonInteractiveSudo, and it landed with no row in gunbc.auth.privileged_effect_census. The roster is hand-authored and its witness joins only the census against follow_up_sites, so nothing would have enrolled it — the reviewer was the wall, exactly as DESIGN §3b says it is until the climb it names lands.

#11897 adds one PrivilegedEffectSite row that conforms under RealizedFederatedGrant, with a discriminating control: mis-declaring the row makes unreasoned_divergences_join_the_follow_up_roster_exactly go red.

— sent from keen-bear-791

gunbai-bot Bot pushed a commit that referenced this pull request Sep 20, 2026
…ge and regenerate

Same two paths as the previous merge and the same resolution, because they are
the program's shared spine: #11765 and #11679 are sibling microVM lanes editing
fleet_converge_workflow.dag, so every lane in this program serializes on this
file and its generated projection.

Roster is the union at 32 modes -- this branch's MicrovmNetworkObserve beside
main's MicrovmControllerAppKeyConverge -- with the type arms, the roster and
fleet_converge_mode_scope checked to agree exactly. The workflow YAML is
regenerated from the merged sources and carries both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Sep 20, 2026
Side-chat REWORK. Part 1 (external blockers as prose) was withdrawn after I
traced that startable authorizes closing-contract authoring rather than
implementation dispatch. Parts 2 and 3 stood, and both were my errors.

THE CENSUS ROWS ARE NOT AN AUTHORITY. The page said the 57 transcribed rows
were dissolved by the scan producer. That is materially wrong in two ways.
The economic readings attached to those rows were shown not to have the
meanings assigned to them -- wall duration is not summed runner occupancy, an
admission delay is not a runner queue delay, a provider declaration can
outlive provider execution, and adoption is not spend -- so the derived
runner-minutes and ARM-tier totals do not follow, and "five carry
CostOpportunity" must not be quoted as a finding. And the scan producer is
workflow-level, so it is necessary and NOT sufficient: the facts those rows
need are job-level. The job-level arc is now a roadmap node instead of a
sentence.

ACQUISITION IS MODELED, NOT ACHIEVED. The page called gunbc#11552 an
end-to-end App manifest flow. Its own route tells the operator to paste the
manifest into the create form's manifest field; GitHub exposes no such field
and the manifest protocol needs a form POST. I watched that step fail live in
this session and wrote "end to end" anyway. Registration and INSTALLATION are
also two facts, and gunbc#11677 consumes both rather than creating either.
That is now a node with the remaining work named.

EXTERNAL BLOCKERS ARE GONE. gunbc#11552, #11564, #11656, #11669, #11671,
#11677 and #11679 are all merged. The nodes and the page said otherwise.

The two real prerequisites are now EDGES rather than prose, which is the
repair the reviewer asked for: the installation token depends on the App
existing, and the job-level reprojection depends on the token.

Projection reconciles 146 -> 148: two nodes and their closing-contract
carriers, minus the two carriers the new edges remove from the startable set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 20, 2026
…t merged

#11679's park verdict arrived after it landed, so all three findings are on
main. Repaired here in one change.

(a) THE ORDER WAS COMMENT-VERSUS-CODE, AND WORSE, IT WAS ENCODED IN TWO
WITNESS NAMES. converge_controller_app_key called ensure_key_dir, which
runs `install -d` and THEN observes, before observe_upper_ancestors -- so a
host mutation preceded the observation entitled to refuse the whole
converge. No key was ever written (controller_app_key_action decides after
every observation), so the residue was an empty root-owned directory rather
than a custody hole. But an_unsafe_directory_refuses_before_any_write and
a_writable_ancestor_refuses_before_any_write call only the pure decision
with supplied observations, so they stayed green while advertising a
property the production route did not have.

Both repairs, not one: the create is now gated on the ancestor reading (on
the refusing arm the directory is only OBSERVED, which is a read), and the
two witnesses are renamed to what a claim over a pure function can
establish -- refuses before any KEY-CONTENT write. controller_app_key_action
is untouched, so the decision keeps its single authority.

(b) CUSTODY WAS COUPLED TO RUNNER WIDTH. The entry resolved its host through
runner_host_file_subject, which requires an admitted RunnerHostDeploy -- a
spec plus an admitted WIDTH. srv2's width is unresolved, so srv2 has no
deploy row and the mode terminated on srv2 while describing itself as
covering srv1 and srv2. The App key's location, administrator target and
custody do not depend on how many slots a host admits, so the module now
resolves its own subject from runner_host_spec_for, which names srv2's
address and principals independently of width.

The privilege law is NOT dropped with the deploy row: the writer may not be
the account CI jobs run as, and that comparison keeps its one authority --
gunbc.runner_host_grants gains bootstrap_principal_is_not_the_job_user, and
the existing deploy-shaped predicate now calls it rather than being
duplicated. The new subject deliberately carries no unit populations: this
module verifies no teardown, so a value with empty unit lists would lie
about what was established.

(c) THE STAGING ABSENCE PROBE COULD FALSELY CONFIRM. `test -e` exit 1 became
StagingAbsent, and GNU test -e is stat(path) == 0, so EACCES, ELOOP, ENOTDIR
and EIO all answer 1 with an empty stderr -- and StagingAbsent is an input
to ControllerAppKeyHeld, so a metadata failure would have been reported as
custody held with no residue. Absence is now established by a successful
listing of the PARENT, which is gunbc.live_deploy.member_observe's rule
rather than a second vocabulary.

The departure from that authority is stated rather than silent (DESIGN §3b):
entry_presence reaches the far side as the bootstrap principal, and this
parent is the key directory at root:root 0700, so an unelevated listing can
only ever refuse -- it would answer Indeterminate on every host whether or
not residue exists. The same membership question therefore runs over this
module's elevated leg. classify_staging_residue is now pure over a listing
that SUCCEEDED, so StagingUnobservable has no constructor inside it; that
arm belongs to the leg.

EXECUTED EVIDENCE: all 21 witnesses in
microvm_controller_app_key_converge_witness_test pass, including three new
discriminating ones -- srv2 resolves a custody subject although the deploy
route still refuses it (which is the pair that would go red if custody were
re-coupled to width), and the parent listing establishing absence, residue
and the empty-listing case.

Retained untouched, as the reviewer asked: the key path consumed from the
signer's own JwsSigningKeyRef, the placement refusing the fabric-cell tree,
RemoteFileModeOwnerRead closing the octal vocabulary at 0400, the
byte-for-byte comparison against the resolved SM version, the full re-read
after mutation, no key bytes in any receipt, and the old runner-owned key
left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 21, 2026
…ence chain, not the deleted classify_host_file_presence

gunbc.microvm_controller_app_key_converge imported and called
classify_host_file_presence from gunbc.runner_host_file_converge. The
function was real (#11063) and this consumer bound it in #11679; #11845
deleted it because its exit-1 arm minted HostFileAbsent for every stat
failure (GNU `test -e` is stat(path) == 0), and touched only the sibling
module, so this consumer went dangling. Restoring the name would restore
the false-absence arm on the one file the module keeps root:root 0400.

observe_key_content now runs the sibling's stat `%F` probe over this
module's elevated leg and consumes classify_host_file_metadata,
classify_host_file_path and runner_host_file_settled_standing; the parent
listing that decides absence when the read failed runs elevated (the same
stated departure from member_observe's entry_presence that clear_staging
already carries, because the parent is root:root 0700), with the argv and
membership read still member_observe's.

Evidence: per-entry compile of the module (primary-precedence pool) on
#11941's head with the old file reports exactly the three errors (name not
found in module at the import, function not found in scope at the call,
effect summary incomplete); with this file it reports none, both runs
resolving the same 1563-source closure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 21, 2026
…ts own selector

`gcp_resource_names_this_version` built BOTH accepted spellings of the resource
name from `ref.version`, so a request naming the `latest` alias was compared by
exact string equality against ".../versions/latest". Secret Manager answers
AccessVersion with a RESOLVED NUMERIC version -- "projects/<number>/secrets/<id>
/versions/<n>" -- which matches neither spelling, so the join was false on every
run and SecretCredentialResolvedVersionMismatch fired on every fetch.

BOTH rows of the host credential custody roster name `latest`, not only the one
this branch adds: ApprovalNtfyPublisherToken and the PRE-EXISTING ControllerAppKey
(gunbc.auth.github_apps gunbai_ci_app_pem_secret), fetched through the same
fetch_secret_ref_credential call at the same entry. Re-derived independently:
`gh run list --workflow fleet-converge.yml` gives 295 runs back to 2026-08-15 and
NOT ONE dispatch of microvm_controller_app_key_converge, a mode introduced by
#11679 on 2026-09-20 -- so there is no live evidence anywhere in this corpus that
a `latest` ref resolves through this join, and #11679's delivery has been dead on
arrival since it landed. "Already-live on the same path" was an assumption.

THE REPAIR IS AT THE AUTHORITY, AND IT IS TYPE-LEVEL. SecretVersionSelector moves
from gunbc.auth.secret_rotation -- one layer up, reachable only by a rotation --
to extdeps.cloud.gcp.secret_ref, beside the builder that realizes the same
upstream fact. That placement is why the credential FETCH, the other consumer of
exactly this distinction, did not have it. resolve_returned_version replaces the
Bool predicate: it proves the response names THIS project in either accepted
spelling and THIS secret FIRST, and only then lets the selector decide about the
version. An exact request is refused on any other number, exactly as before. An
alias request is admitted only to a resolved upstream version number -- the alias
echoed back is refused, because an unresolved answer establishes nothing about
what the bytes are -- and the resolution carries the number that was served, which
a Bool could not. Nothing is pinned; no caller special-cases the string.

Two production consumers rewired, one of them not named in the brief:
gunbc.auth.secret_ref_credential (the credential fetch) and
gunbc.auth.ci_app_key_rotation verify_app_key_version, which called the same join
through secret_ref_credential and refuses the alias arm as its OWN policy -- a
rotation proves a mint on one named version.

THE FOUR CONTROLS, EACH SHOWN RED BY MUTATION rather than asserted, extended into
dag/test/claim/secret_ref_credential_identity_join_witness_test.dag, which had no
alias case at all -- that absence is why this shipped. Base: 21/21 green.

  M1  alias arm never admits           -> 2 red: an_alias_request_resolves_to_the_
                                         number_the_service_served, ..._in_the_id_
                                         spelling_too
  M2  exact comparison always admits   -> 3 red: a_different_version_of_the_same_
                                         secret_is_refused, ..._in_the_number_
                                         spelling_is_refused, an_exact_request_
                                         answered_with_another_number_is_still_refused
  M3  project/secret proof dropped     -> 6 red, THREE EXACT AND THREE ALIAS:
                                         foreign project id, foreign project
                                         number and another secret, in both
                                         spellings
  M4  alias admits an unresolved       -> 1 red: an_alias_request_refuses_the_
      segment                             alias_echoed_back

The alias rows assert WHICH ARM answered and which version it says was served, not
only the verdict, so a comparison relaxed until a number matched would answer true
through the exact arm and still read red.

Sibling witnesses re-run with explicit function rosters and green by execution:
ci_app_key_rotation 15/15, secret_rotation 58/58, cloudflare_r2_origin_mint_run
26/26. (`--functions all` runs ZERO witnesses and exits 2 -- a harness false green
that was caught and discarded rather than reported.)

Authority: DESIGN.md §5 (fail-closed; specification-without-execution), §3 (single
authority: the alias rule belongs where the resource name is built), §4b(1).

Co-Authored-By: Claude Opus 5 (1M context) <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