Skip to content

ntfy publisher token onto GCP custody: consolidate #11679 into one custody roster - #11935

Merged
gunbai-bot[bot] merged 18 commits into
mainfrom
session/proud-fox-175
Sep 21, 2026
Merged

gunbai-bot[bot] merged 18 commits into
mainfrom
session/proud-fox-175

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

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

  • One custody authority, renamed. gunbc.microvm_controller_app_key_converge → gunbc.host_credential_custody_converge, moved to dag/gunbc/fleet/. The old path is deleted.
  • One fleet-converge mode. It is host_credential_custody_converge, and the new credential dispatch input is a choice over the closed roster host_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.
  • The controller key is the first row. Its old root:root 0400 and 0700 directory constants are now that row's fields.
  • Each row carries a SecretRef, a path, a host scope, the file's owner/group and mode arm, and the directory's owner/group and mode arm. The ntfy row is root:gunbc-ntfy 0640 in a root:gunbc-ntfy 0750 directory, srv1 only. A witness holds that directory equal to approval_ntfy_converge NtfyConfigDirectory, so the two converges cannot fight over it.
  • Both find legs come from the arm, never from a caller octal. Files: exact 0400/0640 with forbidden bits /077//037. Directories: exact 0700/0750 with forbidden bits /077//027. The owner leg takes -user/-group from the row. A chown leg applies the row's owner and group after the typed write, because the write lands root:root.
  • The directory check was fixed in the row model, not relaxed. The old check refused any group bit on the directory. Now the directory's allowed bits come from the row's directory arm.
  • The HumanIntervention is deleted. What stays human is the mint (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 in gunbc.auth.approval_ntfy_deployment and is a new row in gunbc.auth.fleet_secret_accessor_roster.
  • Ported fixes from main (landed on the old module after this branch started):

Controls (on the real admission path, custody_row_placement, which the CI entry runs before any Secret Manager read)

  • Red: the real ntfy row with only its directory arm narrowed to 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.
  • Positive: the same ntfy row, unchanged, places in /etc/gunbc-ntfy, and every rostered row places.
  • Witness results: test.claim.host_credential_custody_converge_witness 37/37. Also re-run: gcp_secret_access 36/36, approval_ntfy_deployment 17/17, host_convergence_census 12/12, authorization_pattern_selection 12/12, workflow_dispatch_input 20/20, host_convergence_protocol 23/23, workflow_capability_closure 8/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_agreement names 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 its latest ref 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 with gh run list --repo gunb-ai/gunbc --workflow fleet-converge.yml rather than taking it from here. Two operator steps come first: minting the token into Secret Manager, and ensuring approval_ntfy_publisher_token_accessor_row. Until then the mode refuses at the fetch and names the secret.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 2 commits September 21, 2026 03:01
…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>
@gunbai-bot gunbai-bot Bot changed the title ntfy publisher token onto GCP custody ntfy publisher token onto GCP custody: consolidate #11679 into one custody roster Sep 21, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 21, 2026 03:45
…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>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review 69483 in 5178db2, taking option (a): the extdeps authority now has a name form.

  • extdeps.posix.identity adds PosixOwnerNames { user, group } and posix_owner_names_text. The : is still spelled only in that module; the note says why names are a separate fact from the numeric PosixOwnerSpec.
  • extdeps.tools.chown adds chown_names_argv.
  • The custody rows' ownership now is PosixOwnerNames: the local CustodyOwnership type is deleted rather than kept beside it. custody_file_chown_argv calls chown_names_argv.
  • The receipt's owner= wire had the same re-spelling, and now goes through posix_owner_names_text too.

Witness host_credential_custody_converge is 37/37 on the change. The two claims that pin the argv and the receipt text also pass after the last edit.

gunbc.auth.approval_keyring_converge keyring_chown_argv still has the old shape. That isn't in this PR's scope, and it can move to chown_names_argv in a follow-up.

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

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on review 69483 in 38b2d1f: chown_argv now handles both numeric and named owners, and the one existing duplicate is moved onto it.

  • extdeps.tools.chown chown_argv(owner: ChownOwner, path), where ChownOwner = ChownOwnerIds { spec: PosixOwnerSpec } | ChownOwnerNames { names: PosixOwnerNames }. The separate chown_names_argv from the previous commit is removed.
  • Both forms render through extdeps.posix.identity posix_owner_pair_text, which is now the only place the : is written. Before this, posix_owner_spec_text and posix_owner_names_text each wrote their own.
  • Callers:
    • gunbc.host.host_effect_realize and the srv3 witness wrap ChownOwnerIds.
    • The custody row and gunbc.auth.approval_keyring_converge keyring_chown_argv use ChownOwnerNames. The keyring argv also changes from a bare chown to the modeled /usr/bin/chown.
  • There is no "chown" literal left in any .dag argv.

Witnesses: host_credential_custody_converge 37/37, srv3_principal_privilege 18/18, approval_keyring_converge 10/10.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. Pinned delivery: the custody row names the numeric version to deliver; rotation advances that modeled version before convergence.
  2. Alias-aware delivery: model latest as 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.

Brian Searls and others added 3 commits September 21, 2026 10:28
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>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Three reviews (69522, 69535, 69542) independently reached the same blocking finding on version: "latest". I verified the mechanism in source and it is confirmed — but one piece of the corroboration is wrong, and correcting it makes the finding broader than any of the three stated. Flagging that before anyone fixes this, because it changes where the fix belongs.

The mechanism, confirmed

gcp_resource_names_this_version (extdeps/cloud/gcp/secret_ref.dag) builds both accepted spellings from ref.version, so a latest request is compared against .../versions/latest. secret_ref_credential.dag records, measured, that AccessVersion answers projects/<number>/secrets/<id>/versions/<n>. A numeric answer matches neither spelling, so resolved_identity_matches_request is false and SecretCredentialResolvedVersionMismatch fires.

Where the corroboration is wrong

Review 69542 says "every other production SecretRef pins a numeric version". That is not so. gunbai_ci_app_pem_secret (auth/github_apps.dag:56-60) is latest — and it is not a distant sibling, it is the ControllerAppKey row's own secret in this very roster (host_credential_custody_converge.dag:289), fetched through the same fetch_secret_ref_credential call at the same entry. The predecessor this module replaces did the same thing on main (microvm_controller_app_key_converge.dag:954).

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 ControllerAppKey row would dissolve it.

So the finding is one of two things, and they need different fixes

Either:

  1. The join really does refuse every latest — in which case the pre-existing controller-key delivery has never succeeded live either, and this PR is not introducing a dead row so much as inheriting one. The fix then belongs in gcp_resource_names_this_version (alias-aware resolution, proving same project and secret before accepting a resolved numeric answer, carrying that numeric identity into the receipt) — one place, correct for both rows and every other latest ref in the tree.

  2. Something about the real response matches — in which case both rows are fine and all three findings dissolve together.

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 runner_host_deploy and ci_spec. That is the hand-enumerated-roster stall this PR exists to remove.

What settles it

One fact, and it is not in the repository: has the controller-key converge (old mode microvm_controller_app_key_converge) ever produced a HELD receipt on a live dispatch? I searched for a recorded receipt and there is none either way. I am not guessing at it — asserting GCP's alias behaviour from the join's shape would be the same "deduced from inferred" move this lane keeps catching.

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

Brian Searls and others added 2 commits September 21, 2026 14:49
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>
Brian Searls and others added 8 commits September 21, 2026 17:57
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>
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 briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • SecretVersionSelector lives beside the SecretRef resource-name builder;
  • resolve_returned_version proves 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 latest are refused;
  • the credential fetch consumes that resolver, while verify_app_key_version explicitly refuses alias semantics as its exact-version policy.

The further repairs also hold:

  • file readback octal derives from remote_file_mode_octal rather 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.

@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit ec22a19 Sep 21, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/proud-fox-175 branch September 21, 2026 23:20
gunbai-bot Bot pushed a commit that referenced this pull request Sep 21, 2026
…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>
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