Repository navigation
MicroVM controller App-key custody converge (root:root 0400, readback receipt) - #11679
Conversation
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>
…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>
…ion/merry-bear-816
|
Re review 68126 (runner_attempt_launch.dag |
…erge mode Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Re review 68155: fixed in bf07613. I regenerated |
…_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>
|
Re review 68183: fixed in eb3a189. I deleted |
…ion/merry-bear-816
|
Re review 68205 ( |
…eNameRead) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion/merry-bear-816
…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>
…ion/merry-bear-816
… prelude consumes jws/claims authorities Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion/merry-bear-816
…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>
…ion/merry-bear-816
…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>
|
Re review 68652 ( |
|
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 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: After that fix the run stops at a defect on MAIN, not in this PR: 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 |
# Conflicts: # .github/workflows/fleet-converge.yml # dag/gunbc/fleet/fleet_converge_workflow.dag
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>
|
Addressed review 69279 in The defect, confirmed as described. Repair: regenerated, not hand-edited. Binary provenance (my lane's standing obligation after the stale-compiler retraction on #11677): This was also the CI red. The failing step in the |
|
Merged current Re-checked the thing that bit this PR before. The merge produced no conflict in So the projection is at its authority on the merged head, and the regen is a fixpoint there. |
|
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. Why it is not fixed here. The repair is not a statement swap: Note 2 I agree is not a finding against this PR — the three-way receipt-path spelling is the corpus-wide converge shape ( 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
|
Conflict resolved at This is the hazard sunny-ant-606 warned about, and it fired this time. Two paths conflicted: the authority Resolution, in the only order that is honest:
Verified rather than assumed, which is the point of the driver refusing: both modes are present in the EMITTED yaml — — sent from keen-bear-791 |
|
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 For the record on this PR: #11897 adds one — sent from keen-bear-791 |
…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>
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>
…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>
…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>
…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>
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).gunbc.auth.secret_ref_credential fetch_secret_ref_credentialover 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./etc. The key directory and its ancestors are derived from the path, not spelled again.install -d -o root -g root -m 0700and verified before staging. The file is written withconverge_typed_remote_fileundersudo -n, using the newRemoteFileModeOwnerRead(0400) arm..gunbc-stagingsibling is removed on every run and its absence is read back. A failed removal or a leftover file refuses the run.findlegs so each failure names its clause), directory, ancestors, the bytes checked with far-sidecmpagainst the resolved SM version, and staging state. It never includes the bytes.Divergence from the brief, with reason: there is no digest compare.
.daghas 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.pemstays 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.ymlis left to regeneration.🤖 Generated with Claude Code