Repository navigation
C9b: bind the rotation kernel to the GitHub App key, with an exact-version verifier - #11732
Merged
Merged
Conversation
…rsion verifier The gunbai-ci App private key (ci-github-app-private-key) becomes the first real subject of gunbc.auth.secret_rotation: - gunbc.auth.ci_app_key_rotation: the intent (ReceivedOutOfBand, DisableOnly, one consumer pinned to the exact new version), the protection contract (rotate_by, loss remedy at the App settings page, restore test bound to the rostered consumer), and the verifier: typed verdict over what an exact-version mint observed, refusing the alias, unreadable versions, a response about another version, and a non-201 installation-token exchange. The verdict is the kernel's consumer evidence; the contract's deadline is read on every verify run. - fleet-converge mode app_key_version_verify: mints one installation token with versions/N through the SAME prelude the org-admin step uses (now parameterized by key ref and an observation record); org_actions_observe emits unchanged. Mutates nothing: no add-version, disable or destroy. - Kernel: plan_rotation_retirement orders read-back before census/proof; authorize_retirement now refuses an empty roster as CensusReachedNoOne (it previously authorized DisableOnly retirement over zero consumers). - doc_graph_roots: the frontier row states the kernel has NOT actuated and names the missing capability: the operator's first live rotation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/gunbc/ci/ci_spec.dag
… prelude #11547 generalized the mint into github_app_installation_token_prelude(spec). The verifier becomes one more AppInstallationTokenMint (exact-version key ref, observation record) instead of a second parameterization; org-admin and heal-publisher steps emit byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # .github/workflows/fleet-converge.yml # dag/gunbc/fleet/fleet_converge_workflow.dag
…hind Review 68730: the header cited gunbc_ci_app_key_version_verify_invoke, which the re-seating commit replaced with a mint script plus a verdict invoke. DESIGN §3 — a stale symbol name is decidable, so it is a citation defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # .github/workflows/fleet-converge.yml # dag/gunbc/fleet/fleet_converge_workflow.dag
Ledger-Repair-Judged: docs/design-rung-drops.md Ledger-Rows-Repaired: docs/design-rung-drops.md namespace_wave_admission_wall_removed Heal-Candidate-Run: 35485965960
Both conflicts were generated artifacts (fleet-converge.yml, design-rung-drops.md); resolved by taking main's bytes and re-deriving from the authorities, never by hand-editing a projection. The rung-drops projection now matches main exactly (#11812 fixed the sentence terminator in the projector). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main added the site_pxe_edge_converge mode in the same two places this cut touches. Authority resolved by keeping both mode sets; the .yml re-derived from it rather than hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main added the site_pxe_edge_converge and spark_native_serving_apply modes in the same declarations this cut extends. Mode list resolved as a union with main's order first; the dispatch description keeps main's text with this cut's clause inserted; the .yml re-derived from the authority. NOTE for whoever hits this next: the merge first reported 'refusing to merge unrelated histories'. The clone is SHALLOW (.git/shallow) and the graft boundary had moved past the real merge base, so git could not see one while GitHub's own compare reported diverged/11-ahead/4-behind. git fetch --deepen=100 origin main restored it. A missing merge base here is a fetch-depth artifact, not a rewritten main -- do not conclude history was force-pushed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 69158: the observation-record path lived in both the mint step's env (which writes it) and the verdict step's env (which reads it) -- two sources for one fact, and a split that fails closed while pointing the reader at the wrong step. The dispatch-input expression was the same lapse. Both are now one data row consumed by a shared env builder. The regenerated .yml is byte-identical, so this is single authority and not a behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five conflict regions, all 'both sides added disjoint content at one spot': main's mtcollins1_fan_observe mode against this cut's app_key_version_verify. Mode list resolved as a union with main's order first; every other region keeps both sides. Region 3 needed care -- both sides end MID-STEP and shared the continue_on_error/timeout tail that followed, so concatenating them left one step unterminated (the regen refused: 'heads-only parse: unterminated function body'). The first copy gets its own tail. The .yml is re-derived, not hand-edited, and carries both modes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Sep 20, 2026
At e0c453f it had one approval, no change requests and four green checks by name; the eighth review cleared the capability-grained frontier row. It was stopped by queue position on the contended pair, not by a defect. The consequence is recorded rather than left implicit: the App private key still has no rotation deadline, no restore test and no rehearsed compromise answer, and nothing in the corpus can answer whether the key in a given version works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One conflict region: the dispatch mode list. Resolved as a union with main's order first (main's approval_broker_dark_install plus this cut's app_key_version_verify). The .yml is re-derived from the authority. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflict regions: the dispatch mode list (union, main's order first, picking up main's microvm_controller_app_key_converge) and the dispatch description (main's text with this cut's clause inserted). The .yml is re-derived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
C9b: the gunbai-ci GitHub App private key (
ci-github-app-private-key) becomes the first real subject of the rotation kernelgunbc.auth.secret_rotation, with a verifier that checks one exact key version on demand.The kernel has NOT actuated. This PR adds nothing that performs add-version or disable. The frontier row in
gunbc.doc_graph_roots(secret-version-lifecycle) now says so and names the missing capability: the operator's first live rotation, with a human in the loop.What lands
gunbc.auth.ci_app_key_rotation(new module):ci_app_key_rotation_intent:ReceivedOutOfBand(GitHub only creates App private keys in the App settings page; no REST endpoint does),DisableOnly, and a one-consumer roster pinned to the exact new version.ci_app_key_contract: the protection contract, attached to the real subject. Each field is read by something:rotate_byis read byrotation_deadline_standingon every verify run. A passed or unorderable deadline refuses.loss_remedy(reissue at the App settings page) is rendered into the unreadable-key and 401 refusals.restore_testbinds the rostered consumer, and the intent's roster is built from it. The retirement plan refuses unless that consumer is proven on the exact new version.verify_app_key_versionreturns a typed verdict over what the mint observed. It refuses:latest, an empty selector, or anything that is not a version number;resolved_identity_matches_request, so project-number spellings are accepted);app_key_verdict_evidenceturns the verdict into consumer evidence for the kernel:ResolvedVersionObservedwhen the key mints,ConsumerUnreachablewith the cause when it doesn't. It is never success-only.app_key_version_verify, with a newapp_key_versiondispatch input:versions/Nthrough the same prelude the org-admin step uses. The verifier is one moreAppInstallationTokenMintspec on main'sgithub_app_installation_token_prelude(Heal publishes through a credential-isolated boundary, as an App that already existed #11547), with the key ref at the exact version and a new optionalobservation_recordfield, so there is still one place key material is written, with the same umask 077, chmod 600 and trap cleanup.org_actions_observeand heal-publisher steps are byte-identical.extdeps.cloud.gcp.secret_manager sm_version_id_is_number).always(), so a refused mint still gets a typed verdict and a receipt. The receipt is uploaded as theapp-key-version-verifyartifact.plan_rotation_retirementputs the read-back first; consumer proofs are joined against the identity that read-back confirmed.authorize_retirementauthorizedDisableOnlyretirement over an empty roster. A census over zero names is vacuously complete. It now refuses asRetirementCensusReachedNoOne { CensusReachedNoOne { roster_size: 0 } }.Evidence
test.claim.ci_app_key_rotation_witness, 16 witnesses, each run with the locally built interpreter viagunbc run --function; all returnedtrue.CensusReachedNoOne.false. The other reds call symbols that don't exist before this change, so they fail to compile there.For the operator
rotate_by: 2026-12-18T00:00:00Zis a proposed policy value, 90 days out. Please set it. A completed rotation should move it forward.app_key_version_verifywith the currently enabled version number. That is the first restore rehearsal, and it answers "does the key in version N actually mint", which nothing could answer before this PR.Out of scope, not touched: the org-admin credential deletion (
org_admin_credentialis still imported by the shared prelude for its two message functions; that lane owns their move), #11689, #11678.🤖 Generated with Claude Code