Repository navigation
ntfy publisher token onto GCP custody: consolidate #11679 into one custody roster - #11935
Conversation
…stody roster gunbc.microvm_controller_app_key_converge becomes gunbc.host_credential_custody_converge: ONE fleet-converge mode whose credential is a dispatch choice over a CLOSED roster of custody rows. The controller key is the first row (its root:root 0400 / 0700 constants are now that row's fields); the approval ntfy publisher token is the second (root:gunbc-ntfy 0640 in a root:gunbc-ntfy 0750 directory, srv1 only). Each row carries SecretRef, path, host scope, file owner/group + mode ARM, and directory owner/group + mode ARM. Both far-side legs (exact mode, forbidden bits) derive from the arm; the owner leg takes -user/-group from the row. Row admission refuses, typed and located, a group-readable file under a directory that denies that group traversal (CustodyGroupCannotTraverse) or belongs to another group (CustodyDirectoryGroupDiffers). The delivery HumanIntervention is deleted; the mint (creating the ntfy user and adding its token to Secret Manager) stays a narrower human row. The token's SecretRef joins the fleet accessor roster. A permission-bits readback does NOT establish that a non-declared principal cannot open the file; this is necessary-but-not-sufficient for gunbc.rung_drop approval_store_single_writer_by_agreement and does not retire it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng cleanup, ancestors-before-mutation) and #11845 (parent-listing absence) into the custody roster Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s (review 69483) extdeps.posix.identity gains PosixOwnerNames + posix_owner_names_text and extdeps.tools.chown gains chown_names_argv, so the ':' separator stays spelled in one place. The custody rows' ownership IS that type (CustodyOwnership deleted), and the receipt wire uses the same text fn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed review 69483 in 5178db2, taking option (a): the extdeps authority now has a name form.
Witness
— sent from proud-fox-175 |
…eview 69483, parent ruling) extdeps.tools.chown chown_argv takes ChownOwner = ChownOwnerIds | ChownOwnerNames, and both render through extdeps.posix.identity posix_owner_pair_text, the one ':' spelling. chown_names_argv is removed; host_effect_realize and the srv3 witness wrap ChownOwnerIds; the custody row and gunbc.auth.approval_keyring_converge keyring_chown_argv use ChownOwnerNames. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up on review 69483 in 38b2d1f:
Witnesses: — sent from proud-fox-175 |
Two conflicts, and the second was load-bearing rather than mechanical. .github/workflows/fleet-converge.yml is a generated projection and the driver refused it rather than answering, since both sides changed it since the merge base. Regenerated from the merged authorities instead of merged by hand; the result carries this branch's host_credential_custody_converge mode AND main's new modes (mtcollins1_census_image_publish, r2_bucket_ensure, app_key_version_verify, r2_bucket_admin_mint), with no microvm_controller_app_key surviving. A second gate run wrote nothing. dag/gunbc/runner/microvm_controller_app_key_converge.dag was a modify/delete: this branch deletes it as part of the rename to gunbc.host_credential_custody_converge, while #11946 edited it on main. Taking the delete is right, but the edit could not simply be discarded -- it relocated executor_bootstrap_principal from gunbc.runner_host_grants to gunbc.fleet_bootstrap_principal, and the replacement module still imported it from the old home. Resolving delete-vs-modify without reading main's edit would have produced a branch that merges cleanly and does not resolve. The import is repointed, and a census over the branch confirms no other importer takes executor_bootstrap_principal from runner_host_grants. The prose citation at the privilege law is left alone: it names bootstrap_principal_is_not_the_job_user, which did not move. Measured rather than assumed: compiling the replacement module's closure at this head reports 3 blocking errors, all 'shell' transport emission refusals in extdeps.gunbc, and all present in the same closure family on main. Compiling the deleted module ON main reports its own resolution failure (classify_host_file_presence, deleted by #11845) plus annotation-grain errors -- so main currently carries a module that does not resolve, and this merge removes it rather than inheriting it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
HOLD — exact head 5c07e5cf4856e9e5e79c16b6dea7d53745db062f.
The consolidation itself is well-shaped: one closed credential roster, one fleet-converge mode, row-derived file/directory permissions, host scope before fetch, and the permission-bits caveat retained on the authority and receipt. The new ntfy row cannot currently reach that machinery with a credential.
P0 — the ntfy latest SecretRef is incompatible with the fetcher’s exact-version join
The new row declares:
approval_ntfy_publisher_token_secret_ref =
secret_ref_pin_version(..., version: "latest")
and the production entry fetches it through fetch_secret_ref_credential.
That fetcher deliberately refuses unless the identity returned by Secret Manager names the same exact version as the requested SecretRef. Its own authority records the measured wire behavior: AccessVersion canonicalizes the answer to a numeric resource such as:
projects/<number>/secrets/<id>/versions/<n>
gcp_resource_names_this_version admits the project-id/project-number spelling difference, but it retains ref.version unchanged. For a request whose version is latest, the accepted spellings therefore end in /versions/latest; the live response ends in /versions/<n>.
So the intended ordinary route is:
request versions/latest
-> response versions/17
-> SecretCredentialResolvedVersionMismatch
-> custody fetch refuses
The PR body already says there is no live ntfy receipt, so green hermetic CI does not contradict this source path.
Required repair
Do not weaken the exact-version wall globally. Choose one explicit policy:
- Pinned delivery: the custody row names the numeric version to deliver; rotation advances that modeled version before convergence.
- Alias-aware delivery: model
latestas an alias request distinct from a pinned version, admit the returned numeric identity only after proving the same project and secret, and carry the resolved numeric version in the receipt. A pinned request for N answered by M must continue to refuse.
Whichever policy is selected, add discriminating controls covering:
- pinned N -> returned N: admitted;
- pinned N -> returned M: refused;
latest-> numeric N: admitted only through the explicit alias arm;- foreign project/secret -> numeric N: refused.
The current comment that latest is deliberate for rotation is a policy intention, but the existing fetch contract interprets it as an exact version string. Until those two facts are reconciled, the new ntfy delivery mode is guaranteed to stop at the Secret Manager read.
custody_file_exact_perm matched the mode arm and returned its own octal literal, while the same file's WRITE mode already derives its octal from gunbc.typed_remote_file_write remote_file_mode_octal. Two sources for one fact -- the mode a file is written at and the mode it is asserted at -- free to drift apart, in a module whose own header claims both far-side legs derive from the arm. This PR grew that from one arm to two, which is §2's test directly: net concepts must not grow by re-invention. The exact-mode leg now asks the owning authority and prefixes the leading zero find(1) wants. THE WITNESS LITERALS ARE DELIBERATELY LEFT AS LITERALS. The review noted the claim asserts "0400" and "/037" directly. Deriving those from the function under test would collapse the claim to measure() == measure() -- the change detector §5 names -- so they stay an independent oracle. That they are one is measured, not assumed: mutating remote_file_mode_octal's OwnerRead arm from "400" to "440" turns the_mode_legs_ask_for_the_arm_exactly_and_forbid_its_complement from true to false. Unmutated it is true, so the derivation yields the same octals the arm used to spell. Scope checked rather than assumed: the DIRECTORY arm is not a second instance. custody_dir_exact_perm's 0700/0750 have no other home -- directories are not written through typed_remote_file_write -- so it is the sole authority and is left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One conflict, the same modify/delete shape as the previous merge: dag/gunbc/runner/microvm_controller_app_key_converge.dag is deleted by this branch's rename and was edited on main by #11941. Main's edit is a pure annotation hoist -- an in-body // block moved to module-item grain -- with no semantic change, so the delete is still the right resolution. But discarding it would have lost the rationale, and the rationale is about a construction this branch KEPT: classify_staging_residue keys on a name that, as a free parameter, a caller could spell freely, and a name that does not match the staging path's basename reads a listing CONTAINING the residue as StagingAbsent -- a false absence that feeds a HELD verdict. Checked rather than assumed: the replacement preserves the construction. Its sole caller derives the key as path_basename of the staging path rather than accepting it. So the annotation is carried onto classify_staging_residue in the new module, at module-item grain, noting where it came from. The rationale outlives the module because the construction it explains does (DESIGN §4c: an annotation may preserve irreducible rationale about why a construction has its shape). The generated projections were regenerated rather than hand-merged and a second gate run wrote nothing. Compiling the replacement's closure reports the same 3 blocking errors as before the merge -- all 'shell' transport emission refusals in extdeps.gunbc, pre-existing on main -- and no annotation-grain diagnostic for the ported block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third merge of main into this branch, and the first whose conflicts were not all mechanical. fleet_converge_workflow_modes: both sides edited the roster. This branch replaced MicrovmControllerAppKeyConverge with HostCredentialCustodyConverge; main's #11751 added MicrovmNetworkObserve. Kept both -- main's roster with the rename applied -- rather than taking either side, which would have dropped the other's mode. Checked after regeneration: the emitted workflow carries host_credential_custody_converge and microvm_network_observe and no microvm_controller_app_key. microvm_controller_app_key_converge.dag: modify/delete again, and this time main's edit was #11942, a real repair -- observe_key_content was calling classify_host_file_presence, which #11845 deleted, and the repair rebuilds presence from the sibling's chain so that "I could not look" can no longer walk into the write path. The delete is still correct, and the repair is not lost by taking it: this branch's replacement already reads presence from an elevated parent listing and treats a listing that did not succeed as unobservable rather than absent. Same semantics, reached independently, and verified in the replacement rather than assumed from the port note. Noted rather than fixed, because it is not this PR's subject: after #11942, main's observe_key_parent_presence consumes runner_host_file_converge's repaired chain while this module re-spells the listing branch inline. Both are correct and fail-closed, and both use member_observe's argv and membership read, so the shared primitives are shared -- but they are two spellings of one decision and should converge on the sibling's chain in a follow-up. Measured: compiling the workflow closure reports 11 blocking errors at this head and 11 at origin/main, all in unrelated modules. The resolution introduces none. Generated projections regenerated, not hand-merged; a second gate run wrote nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Three reviews (69522, 69535, 69542) independently reached the same blocking finding on The mechanism, confirmed
Where the corroboration is wrongReview 69542 says "every other production Review 69535 saw this and drew the right conclusion — it marked the finding PLAUSIBLE rather than confirmed and named the discriminator: a live HELD receipt for the So the finding is one of two things, and they need different fixesEither:
Pinning the ntfy row to a numeric version fixes neither case honestly: in case 1 it leaves the controller key broken beside it, and it makes every rotation a source edit for a row whose ref is shared with What settles itOne fact, and it is not in the repository: has the controller-key converge (old mode Holding the PR at REQUEST_CHANGES until that is answered. The octal-consolidation fix from review 69522 is unrelated to this and stands (verified by mutation: the witness literals flip it red). — sent from zesty-crane-846 |
Fourth merge, same two conflicts as the third: the fleet-converge mode roster and its generated YAML. Main's #11936 added FabricWriterIdentityObserve; this branch replaced MicrovmControllerAppKeyConverge with HostCredentialCustodyConverge. Resolved by taking main's roster with the rename applied, under an assertion that no mode present on either side is dropped -- the resolution refuses rather than silently losing one, since taking either side wholesale is exactly how a mode would disappear here. Emitted workflow carries host_credential_custody_converge and fabric_writer, and no microvm_controller_app_key. Regenerated rather than hand-merged; a second gate run left nothing unstaged. 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>
Fifth merge of main into this branch, and the first where the obvious resolution was wrong in a way that compiled past a careless check. The mode roster conflicted as before: main added PairServingD0, this branch replaced MicrovmControllerAppKeyConverge with HostCredentialCustodyConverge. Both kept, under an assertion that no mode from either side is dropped. THE SECOND HUNK WAS NOT WHAT IT LOOKED LIKE. Both sides added a dispatch input at the same position -- ours "credential", main's "d0_consent" -- but git split the region INSIDE a single DispatchInput record literal, not at record boundaries. Concatenating the two sides produced one record with two `name` fields. That is a real defect neither side wrote, manufactured by the merge, and the only reason it did not land is that the generated-artifact gate refused it: "duplicate field 'name' in record literal". The resolution closes the first record and opens a second, so the two inputs are two records. TWO FURTHER SITES WERE STRANDED BY THE AUTO-MERGE. Regions that merged cleanly kept main's spelling of the renamed variant, so after resolving the conflicts the module still named MicrovmControllerAppKeyConverge at one match arm while the type no longer declared it, and the same match was non-exhaustive for HostCredentialCustodyConverge. Neither is in a conflict hunk; both were found by compiling rather than by reading the diff, which is the point -- a rename across a merge strands call sites in the regions that did NOT conflict. Verified after resolution: gate clean and idempotent, and the emitted workflow carries credential and host_credential_custody_converge from this branch, d0_consent and pair_serving_d0 from main, and no microvm_controller_app_key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 69678, non-blocking. SecretVersionSelector moved from gunbc.auth.secret_rotation into extdeps.cloud.gcp.secret_ref beside the resource-name builder, and this module kept importing two names it no longer calls: classify_version_selector and secret_ref_selector. The witness reimports them from the new home, so nothing is dangling; these two are just residue of the move. Counted per-name in the module body with the import block stripped, rather than by a whole-file grep that would have counted the import itself: both are 0. The four names left in that import are live -- SecretVersionSelector 2, ExactVersion 1, AliasLatest 1, selector_is_alias 1, selector_version_label 1 -- so the block is not emptied and the emission is unchanged. The module's one blocking error, an emission refusal for a 'filter in branch condition', is pre-existing: it reports at line 733 on the pushed head and 731 here, shifted by exactly these two deletions. Not introduced and not repaired by this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…being described twice
`custody_dir_exact_perm` mapped CustodyDirOwnerAllGroupTraverse to the literal
"0750" while `gunbc.auth.approval_ntfy_converge` declared ntfy_state_dir_mode =
"0750" for the SAME directory -- both converges ensure approval_ntfy_config_dir,
which the custody witness itself proves (key_dir == approval_ntfy_config_dir).
The module comment conceded the restatement and the repair chosen was a witness
holding the two values equal: DESIGN §5 exactly, a check restating a constraint
the model already carries where a derivation was available.
THE CONCEPT ALREADY HAD A HOME, AND IT WAS NEITHER OF THEM. `gunbc.managed_directory`
is the existing single authority for what mode a fleet-owned host directory gets;
its own header says a member declares its dependents and the permission is computed
from them, "never by editing a number", and its scope note already admits the
interim -- "the other owned directories keep the shared constant until each one's
dependents are established by evidence". What that note never said is WHERE the
constant lives, so each directory kept its own. So this is a hoist to an existing
authority, not a third one: the same move as the SecretVersionSelector hoist in the
commit before it, and for the same reason -- the concept was modelled one layer too
high to reach both of its consumers.
ACYCLICITY, which decides that direction is legal both ways rather than only one.
managed_directory's closure is extdeps.access.posix, std.effect_grant,
gunbc.ownership, gunbc.fleet_posix_accounts and gunbc.host_layout, and reaches
neither custody nor ntfy -- so both may import it. That is strictly better than the
custody -> ntfy one-way edge the review relied on, which would have made the fix
depend on custody never becoming a dependency of the deployment.
THE SHAPE, which keeps the closed arm the reviewers asked for:
- managed_directory declares DeclaredDirectoryMode (DirectoryOwnerOnly |
DirectoryOwnerAllGroupTraverse) and renders the octal through
extdeps.access.posix file_mode_octal over a constructed FileMode -- so NO octal
literal survives on any side, and no consumer can spell a caller octal.
- approval_ntfy_deployment declares approval_ntfy_dir_mode ONCE. The deployment
owns the directory, so it owns the mode.
- approval_ntfy_converge and the custody ntfy row both READ that declaration.
The generic arm is not bound to a consumer constant; the ROW is, which is
correct -- custody derives a directory it does not own.
- CustodyDirMode is deleted rather than kept beside the shared type.
The forbidden mask (`-perm /027`) stays in custody: it is not a mode but the
complement find(1) needs for the readback leg, and no other consumer asks it.
THE EQUALITY WITNESS IS DELETED AS REDUNDANT, which was the test of doneness: both
sides now read one declaration, so the comparison it made is a value against itself.
BUT DELETING IT ALONE WOULD HAVE BEEN A COVERAGE REGRESSION, and mutation caught
that rather than review. ntfy's "0750" was never asserted by its own witness -- the
one occurrence there is a FIXTURE INPUT to a precondition check, not an oracle -- so
the deleted equality row was the only executing check that touched the ntfy side at
all. Measured: with the arm mutated, five rows reddened in the custody witness and
NOT ONE in approval_ntfy_converge. So this adds
every_ensured_directory_carries_the_declared_ntfy_mode, plus a companion holding the
roster really ensures three directories so the first cannot pass vacuously.
CONTROLS, each shown red by mutation. Base: custody 36/36, ntfy converge 12/12,
managed_directory 11/11, approval_ntfy_deployment 17/17.
N1 shared 0750 arm loses group traverse -> custody 5 red AND ntfy 1 red
N2 owner-only arm quietly grants group -> custody 2 red, ntfy 0 (correct:
ntfy declares the other arm)
N3 approval_ntfy_dir_mode repointed to -> custody 5 red AND ntfy 1 red
DirectoryOwnerOnly
N3 IS THE RECEIPT THAT THE FORK IS GONE. Editing ntfy's own declaration now moves
custody. Under the old code custody picked its own arm, so that same mutation would
have left custody green and the two converges fighting over one directory on every
run -- which is the failure the deleted witness could only ever report, never
prevent.
Pre-existing on this branch and NOT touched here, verified identical with the change
stashed: fleet_converge_plan_witness_test 17/87 red and live_deploy/emit_test 25/77
red ("non-exhaustive pattern match on: simple").
Also noted, not fixed: gunbc.auth.approval_keyring_converge still spells "0750" as a
literal -- a fourth site for this class, outside this finding's subject.
Authority: DESIGN.md §3 (single authority; a fact's home is its layer), §5
(construction over validation), §2 (net concepts must not grow by re-invention).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 69703. r2_origin_read_pin_names_this_project_version supplies
`returned` as secret_ref_version_resource(ref: locus) -- the request name
rebuilt from the very locus the join is then asked about -- so project, secret
and version agree BY CONSTRUCTION. No foreign-project pin can reach that call.
The one fact it discriminates is that the origin pin is an exact version rather
than the alias.
The comment above it, which this PR wrote, claimed "a pin that names another
project fails while fleet_secrets_project_number stays put". That is a claim
the row cannot carry, and DESIGN §4b's top rung is explicit about the cost: a
check whose forbidden state cannot be expressed where it runs is permanently
green, carries no information, and is worse than absent because it gets cited
as coverage.
The red is authorable -- secret_ref_credential_identity_join_witness authors it
a few files over by supplying a foreign-project identity constant -- so it is
authored here rather than the claim being restated downward. Restating would
have satisfied the finding and left the wall unbuilt.
MEASURED, not argued. With the project/secret agreement check removed from
resolve_returned_version:
r2_origin_read_pin_names_this_project_version true (unchanged -- it
cannot see that comparison, which is the finding)
r2_origin_read_pin_refuses_a_foreign_project_answer false (the new wall)
Unmutated, both true. So the new row is the only one here whose red depends on
the join comparing projects at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion/proud-fox-175
Sixth merge, one conflict: the fleet-converge mode roster, resolved as before by taking main's roster with this branch's rename applied, under an assertion that no mode present on either side is dropped. Also checked, because the previous merge of this branch taught it: a rename across a merge strands call sites in the regions that did NOT conflict. git grep for MicrovmControllerAppKeyConverge across dag/ returns nothing, so no site survived this time. Gate clean and idempotent; the emitted workflow carries both sides' modes and none of the old name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
SOURCE APPROVE — live exact head e20b042528307db70cd6a5ced222c83fa434e746.
The requested 9379b48084… is no longer the PR head. I reviewed the current head, including the subsequent merge from main; the only merge conflict was the generated fleet-converge mode roster, and the current projection retains both sides' modes, uses host_credential_custody_converge, carries the closed credential choice, and contains no surviving old mode in the generated route.
The prior alias P0 is closed at the correct authority:
SecretVersionSelectorlives beside the SecretRef resource-name builder;resolve_returned_versionproves the full project and secret before applying exact-versus-alias semantics;- exact N→N is admitted, exact N→M is refused;
latest→numeric N is admitted through the alias arm and carries N in the resolved identity;- foreign project/secret answers and an echoed unresolved
latestare refused; - the credential fetch consumes that resolver, while
verify_app_key_versionexplicitly refuses alias semantics as its exact-version policy.
The further repairs also hold:
- file readback octal derives from
remote_file_mode_octalrather than a second literal; - the directory mode arms and rendering live in
gunbc.managed_directory, with the ntfy deployment and custody row consuming one declaration rather than a witness comparing two spellings; - the old R2-origin row is no longer cited as a foreign-project wall, and the new authorable control supplies an independent foreign-project identity that actually discriminates the project join.
The two red witness files reported in the handoff are real coverage debt, but neither is in this PR's changed-file surface and the handoff reports the same failures with the branch changes stashed. I am not treating them as a regression of this change. They should be separately enrolled or repaired; a real red that no required lane reaches remains a floor-coverage problem.
One body-only precision correction is advisable: the evidence note says only the ntfy row lacks a live receipt, while the run census established that the controller-key mode has also never been dispatched. State that neither roster row currently has a live custody receipt. Do not move this approved source head merely for that metadata correction.
Landing gate: compiler and clippy are successful; the exact-head floor is still running as I write this. Freeze this SHA and merge/queue only after every required check for e20b0425… is terminal-success.
…would have been wrong #11935 consolidated custody and REMOVED MicrovmControllerAppKeyConverge, replacing it with HostCredentialCustodyConverge. On the same single-line modes declaration this branch adds MicrovmNetworkApply to. THE TRAP, recorded because the two previous conflicts on this exact line were resolved by union and this one must not be. A token diff of the two sides reports MicrovmControllerAppKeyConverge as "only ours" -- which reads identically to main simply not having it yet. It is not: origin/main carries 0 occurrences of it and 7 of HostCredentialCustodyConverge. Unioning the two sides would have RESURRECTED a mode another lane had just deleted, inside a diff whose visible story is "adds one microVM mode". So the resolution is not union: main's line taken wholesale, only MicrovmNetworkApply inserted, and four conditions asserted rather than eyeballed -- this branch's mode present, #12008's SparkV41RuntimeImageBuild present, #11935's HostCredentialCustodyConverge present, and MicrovmControllerAppKeyConverge ABSENT. Confirmed no dangling reference to the deleted mode survives anywhere under dag/. "Both sides appended" and "one side appended, the other deleted" produce an IDENTICAL-LOOKING conflict on a single-line declaration, and only the second punishes a union. That declaration is a growth surface every lane collides on: three conflicts on it in one session. The projection was regenerated from the merged authority and verified to carry all three live modes and none of the deleted one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap node
ntfy-publisher-token-onto-gcp-custody. This merges the ntfy publisher token into the custody module from #11679 rather than adding a sibling module (DESIGN §3).What changed
gunbc.microvm_controller_app_key_converge→gunbc.host_credential_custody_converge, moved todag/gunbc/fleet/. The old path is deleted.host_credential_custody_converge, and the newcredentialdispatch input is a choice over the closed rosterhost_custody_credential_roster(controller_app_key,approval_ntfy_publisher_token). An unrostered word refuses with a typed error (HostCustodyCredentialUnrostered). There is no free-text path to a SecretRef.root:root 0400and0700directory constants are now that row's fields.root:gunbc-ntfy 0640in aroot:gunbc-ntfy 0750directory, srv1 only. A witness holds that directory equal toapproval_ntfy_convergeNtfyConfigDirectory, so the two converges cannot fight over it.0400/0640with forbidden bits/077//037. Directories: exact0700/0750with forbidden bits/077//027. The owner leg takes-user/-groupfrom the row. Achownleg applies the row's owner and group after the typed write, because the write lands root:root.approval_ntfy_publisher_token_mint_intervention): creating the ntfy user and adding its token to Secret Manager. That is a different fact from delivery. The token's SecretRef (approval_ntfy_publisher_token_secret_ref,latest) lives ingunbc.auth.approval_ntfy_deploymentand is a new row ingunbc.auth.fleet_secret_accessor_roster.StagingCleanupAuthorization; ancestors are observed beforeinstall -druns.classify_host_file_presenceis deleted on main, so main's copy of the old module no longer resolves. The content probe here now reads presence from an elevated parent listing that succeeded, never fromtest -e.Controls (on the real admission path,
custody_row_placement, which the CI entry runs before any Secret Manager read)0700→CustodyGroupCannotTraverse. The refusal names the path, directory, group and both modes, and nothing widens. A second red: a directory owned by another group →CustodyDirectoryGroupDiffers./etc/gunbc-ntfy, and every rostered row places.test.claim.host_credential_custody_converge_witness37/37. Also re-run:gcp_secret_access36/36,approval_ntfy_deployment17/17,host_convergence_census12/12,authorization_pattern_selection12/12,workflow_dispatch_input20/20,host_convergence_protocol23/23,workflow_capability_closure8/8. The last seven ran before the merge with main.What this does NOT establish (must not be lost)
A find-metadata readback shows which principals hold permission bits. It does not show that a non-writer cannot open the file. Showing that needs a leg that runs as another user and is refused.
gunbc.rung_drop approval_store_single_writer_by_agreementnames that stronger observation as its restoration trigger, and it lists a permission-bits readback among the things that do not retire it. So this work is necessary but not sufficient for that trigger. The same caveat is on the carrier and on every receipt (scope=line).Evidence level for the execution route: the SSH/sudo/Secret Manager route has no hermetic seam, so the claims above cover the pure interfaces only. NEITHER ROW has a live receipt yet, and an earlier wording of this line said only the ntfy row did. The GitHub Actions run history for the fleet-converge workflow carries 295 runs back to 2026-08-15 and zero dispatches of
microvm_controller_app_key_converge, the mode #11679 introduced on 2026-09-20 -- so the ControllerAppKey row this module inherits has never been dispatched either, and itslatestref has never resolved through the fetch join. That is what made the alias defect invisible: there was no live evidence either way, and a later review read the row as "already-live" and declined to raise it on that basis. Re-derive withgh run list --repo gunb-ai/gunbc --workflow fleet-converge.ymlrather than taking it from here. Two operator steps come first: minting the token into Secret Manager, and ensuringapproval_ntfy_publisher_token_accessor_row. Until then the mode refuses at the fetch and names the secret.🤖 Generated with Claude Code