Skip to content

r2-write-mint-via-workflow: federated dispatch job, operator grant cells, pin PR - #11214

Merged
gunbai-bot[bot] merged 33 commits into
mainfrom
session/smart-otter-889
Sep 14, 2026
Merged

gunbai-bot[bot] merged 33 commits into
mainfrom
session/smart-otter-889

Conversation

@briansrls

@briansrls briansrls commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Makes the R2 object-write mint executable from a dispatched workflow job under the run's own federated GCP identity, so the operator's part is (a) creating one empty secret and converging the IAM cells from their own gcloud session and (b) clicking Run workflow. The authoritative roster of those cells is gunbc.cloudflare.r2_mint_secret_access, not this body — see the Update section at the end for the current ceremony.

A — the token source is selected, not spelled

gunbc.auth.access_token_source has carried three realizers of the access ensure for some time, and no entry point in the tree could reach more than one of them, because every entry chose its arm by construction: read_supplied_access_token() then OperatorSuppliedToken, spelled at eight call sites. An arm chosen at the call site is a binding, not a selection — so a workflow run and a workstation run of the same entry were two different programs, and only one of them existed. That is why the only executed precedent (#10947, the read mint) was an operator pasting a 1-hour ya29 token into a session.

  • The realizer of the OperatorSuppliedToken arm (read_supplied_access_token and friends) moves to the module that owns the arm. It was authored in gunbc.auth.gcp_secret_access, whose own annotation said it "realizes the OperatorSuppliedToken arm, which gunbc.auth.access_token_source already carries" — a realizer one module away from the coproduct it inhabits, which is what made the selection unauthorable. Importers are rewired; no name changes.
  • New: select_access_token_source() — one fold. WIF_ACCESS_TOKEN present ⇒ WorkloadIdentityToken; otherwise GUNBC_GCP_ACCESS_TOKEN_FILE ⇒ OperatorSuppliedToken; otherwise a refusal naming both. The workload arm wins on presence, not success: if the variable is present and empty the run refuses through that arm rather than reaching for an operator file. A fallback there would be the absorbing fallback §5 names — a broken federation silently answered by whatever else happened to be on the runner. GcloudPrintToken is deliberately unreachable from the fold: it is an interactive authentication, and an unattended run must not block on a prompt it cannot answer.
  • gunbc.cloudflare.r2_token_mint_run now resolves one identity per run and uses it for the bootstrap read and its own Secret Manager writes.
  • gunbc.auth.secret_ref_credential gains fetch_secret_ref_credential_with_token; fetch_secret_ref_credential is now a wrapper over it. This exists to avoid a meaning fork (§3): the mint needs the token after the read, and the previous shape would have forced a workload-federated token through an arm spelled OperatorSuppliedToken — one spelling carrying two materially different identities, with the receipts naming the wrong one.

B — two dispatch-only modes on the workflow that already exists

The brief asked for one workflow_dispatch job. This adds two modes to fleet-converge instead, and that is a deliberate, better-on-the-merits departure: fleet-converge is already dispatch-only, already pinned to a self-hosted arm64 runner, already carries the google-github-actions/auth@v2 step that federates to fleet-cloud-convergence@gunbai-secrets.iam.gserviceaccount.com with id-token: write, and already downloads CI-verified release bins rather than building gunbc on the runner — strictly better evidence than a build. A new job would be a standing claim on a paid runner plus a new emission to enrol; a mode is neither. No hand-written YAML: .github/workflows/fleet-converge.yml is regenerated from gunbc.fleet_converge_workflow, which is already in the generated-artifact roster, so drift is already gated.

The operator signed off on a dispatch-only addition for this purpose on 2026-09-12.

  • r2_mint_preflight → gunbc.cloudflare.r2_permission_group_observe observe_account_permission_groups_wet
  • r2_object_write_mint → gunbc.cloudflare.r2_token_mint_run run_object_write

E — reachability is observed, and that is why the preflight is separate

AccountTokens.Create is non-idempotent and returns its value exactly once. Observing runner reachability by attempting the mint would mean learning that the runner cannot reach Secret Manager only after a live credential exists on the account with nobody holding it. The preflight reads the pinned bootstrap version from secretmanager.googleapis.com and lists the account's permission groups at api.cloudflare.com — both hosts the mint depends on, no mutation — so it can be dispatched freely and repeatedly.

It is also the vehicle for the discriminating red: dispatched before the grant it refuses typed and located (THE BOOTSTRAP TOKEN COULD NOT BE READ: ...) with nothing spent.

(The earlier curl 35 TLS failure was from a session container to *.r2.cloudflarestorage.com and concerns fetch_absent, a different host and a different operation. Not conflated here.)

While making the preflight an entry, observe_account_permission_groups gained a typed standing (PermissionGroupListing). An earlier draft decided success by testing the returned String for a "NO "/"THE " prefix — a postcondition keyed to a phrase, green the moment someone improves the wording. The refusal is an arm now; the String entry is kept as its rendering.

C — the grants, as cells

New gunbc.cloudflare.r2_mint_secret_access, following gunbc.spark.secret_access_ensure: each cell is derived from the effect that needs it, not from a role someone thought would be enough.

secret role why
cloudflare-account-api-token-bootstrap roles/secretmanager.secretAccessor r2_mint_bootstrap reads the pinned v1
cloudflare-r2-origin-write-token roles/secretmanager.secretVersionAdder add_initial_version writes the minted value
cloudflare-r2-origin-write-token roles/secretmanager.secretAccessor add_initial_version reads that exact version back to compare bytes — the only part of the write Secret Manager guarantees, so it is a requirement of the write, not a convenience

Operator runs, from their own gcloud session (these authenticate as the human, per no_principal_can_both_grant_and_read — the control plane that may call setIamPolicy is deliberately not the workload):

gunbc run --source-root dag --source-root src/v2 \
  --entry dag/gunbc/cloudflare/r2_mint_secret_access.dag \
  --function r2_mint_bootstrap_read_access_converge
# then r2_mint_write_version_add_access_converge, then r2_mint_write_readback_access_converge

Which identity, and why not a dedicated one. gunbc.auth.github_gcp_federation says a federating domain supplies its own SA, which argues for a least-privilege SA of the mint's own. This reuses fleet-cloud-convergence@ anyway, for a structural reason rather than convenience: gunbc.auth.gcp_secret_access resolves the granted member from fleet_cloud_principal_standing() and refuses when it cannot name its subject, so the member of every cell this ensure can write is the convergence principal. A dedicated SA is not a different row, it is a different ensure — member would have to become a parameter, and a second federated principal would need provisioning and pool-binding by an operator ceremony strictly larger than the one this lane exists to shrink. The least privilege available today is therefore per-secret: one secret and one role per cell, nothing project-wide. Next rung is the dedicated SA, triggered by grant member becoming a parameter.

No user-managed service-account key, no token in GitHub secrets, no token as a workflow input. The only credential either step carries is the short-lived access token the WIF step mints for that run, read in-process rather than interpolated into a command line (the argv exposure gunbc.credential_argv_exposure measures).

OPEN — the container create is not expressible as a cell, and it is a decision, not an oversight

store_minted_token calls create_secret_for_attempt → gcp.SecretManager.CreateSecret before add_initial_version. secretmanager.secrets.create is a project-level permission; SecretAccessGrant is one secret + one role + one condition, and a per-secret cell cannot be written for a secret that does not exist. The read mint never hit this because the operator's own identity carried project admin.

Both resolutions have a real cost and the choice is not mine:

  • A — operator pre-creates the empty cloudflare-r2-origin-write-token, and create_secret_for_attempt is fixed to treat ALREADY_EXISTS as converged. Keeps least privilege. Needs the actuator change first: today an existing container makes CreateSecret answer 409 → SecretCreationRefused after AccountTokens.Create has already minted a token whose value is returned once — a token-loss hazard, not merely a failed run.
  • B — project-level roles/secretmanager.admin on fleet-cloud-convergence@. No code change, but gives the workflow identity create/delete over every secret in gunbai-secrets.

Escalated to royal-eagle-761 with A recommended. Do not dispatch r2_object_write_mint until this is settled — r2_mint_preflight is safe to dispatch now and is the evidence that matters first.

Evidence

  • Every touched closure compiles with 0 blocking errors (gunbc compile --target dag, per entry): the mint, the grant module, ci_spec, fleet_converge_workflow, secret_access_ensure, org_actions_converge, both printer runs, the fabric-m0 probe, secret_access_admission, r2_permission_group_observe.
  • .github/workflows/fleet-converge.yml regenerated from its authority (generated_artifact_gate main_wet_one); the diff is +40/−1, exactly the two modes and their four steps.
  • Discriminating red (pending dispatch): r2_mint_preflight before the grants must refuse THE BOOTSTRAP TOKEN COULD NOT BE READ: ... with no AccountTokens.Create issued.
  • Positive control (pending): the executed mint after the grants.
  • Regression control (pending the follow-up pin PR): THE ORIGIN ALREADY PINS THIS CREDENTIAL, which mint_origin_token_and_store already refuses on.

The mint was not executed from this session.

Landing

Under the public-dag/ landing freeze until fierce-lark-661's release notice. Pushed for green; not asking for a merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1


Update (2026-09-13) — the OPEN item is resolved, and it turned into a failure-mode row

Operator ruling: option A, with a sharpening that moves where the fix goes. Option B (project-level roles/secretmanager.admin) is refused on the estate's own design — gunbc.auth.gcp_secret_access exists precisely so no workflow identity holds project-wide create/delete.

The sharpening matters: under the federated identity the actuator must not call CreateSecret at all. Mapping a 409 after the Create to "container present" would have left the irreversible effect ahead of the check, which is the bug, not a fix for it.

What changed

  • R2MintPreconditions gains R2MintCustodyContainerAbsent { intervention }, decided by a readonly GetSecret of the custody container inside the same ladder as every other precondition — so it is decided before cloudflare.AccountTokens.Create, not after it. classify_r2_mint stays pure and takes the observed container standing as a parameter.
  • store_minted_token is AddVersion-only. The CreateSecret call is gone.
  • gunbc.secret_provision_actuator gains ObservedSecretContainer (a sole_constructor seal minted only where the GET's returned resource named the requested project and secret — the same whole-resource comparison, since a suffix match once accepted another project's identically-named secret) and version_add_capability_for_observed_container, which derives the existing CreatedSecretForAttempt once a real snapshot exists. The observation seal deliberately carries no snapshot: the container is confirmed long before any credential exists, so a producer shaped like create_secret_for_attempt could only be called by inventing one — and an invented snapshot inside a seal is worse than no seal.
  • The container-absent refusal carries completion_evidence: DischargedAt(observe_r2_mint_custody_container) — unlike its two neighbours in r2_origin, this ceremony has a real discharge site: the GET that raises it is also the check that answers it.
  • A fourth grant cell, and it is the one an author would most easily omit: the container GET needs secretmanager.secrets.get, which secretAccessor does not carry (accessor is about a version's payload). roles/secretmanager.viewer on the write secret is now its own cell, and the witness asserts get.role != back.role so collapsing the two goes red.

Reordering, stated plainly: the access token is now resolved before the precondition ladder, because the container precondition is an observation and needs the run's identity to be decidable at all. The cost is that a run with no credential reports that first rather than the account/custody gap; the benefit is that the one check whose absence could burn a non-idempotent Create is inside the ladder.

The operator ceremony is now four steps, and the order matters (a cell cannot be bound to a secret that is not there):

gcloud secrets create cloudflare-r2-origin-write-token --project gunbai-secrets --replication-policy automatic

gunbc run --source-root dag --source-root src/v2 \
  --entry dag/gunbc/cloudflare/r2_mint_secret_access.dag --function r2_mint_bootstrap_read_access_converge
# then r2_mint_write_container_read_access_converge
# then r2_mint_write_version_add_access_converge
# then r2_mint_write_readback_access_converge
secret role effect that needs it
cloudflare-account-api-token-bootstrap secretAccessor r2_mint_bootstrap reads the pinned v1
cloudflare-r2-origin-write-token viewer the precondition GetSecret, before the Create
cloudflare-r2-origin-write-token secretVersionAdder add_initial_version writes the minted value
cloudflare-r2-origin-write-token secretAccessor the exact-version readback that write is only trusted on

Filed as a class, per the ruling — not left in this PR body: gunbc.recurring_failure_mode.a_precondition_check_sits_downstream_of_the_irreversible_effect. It is the dual of gunbc#10739's "fabrication downstream of the check" (there the check is correctly positioned and something false flows past it; here nothing is fabricated, the refusal is honest, and it arrives too late), so it is a sibling row rather than a receipt on that one. The discriminating fact is why it survived its one execution: #10947 ran green because the operator's identity held project-wide secrets.create, and a green execution under the most privileged available identity is the weakest evidence an ordering claim can have — privilege is exactly the condition under which a check's position stops mattering. Ceiling is structurally guaranteed: readonly on the service declarations and secret_manager_effect_required_permissions are both modelled, so "a readonly effect, or one whose permissions are unestablished, reachable after a non-idempotent mutation" is derivable rather than reviewable.

The escalation is retracted.

Review 64860 (dashboard-only, REQUEST_CHANGES) — all three addressed

  1. r2_mint_gcp_access_token / permission_group_observe_access_token were the same fold twice. Correct, and it was this PR's own thesis unapplied one level up. The composition now lives once, at the least common ancestor: gunbc.auth.access_token_source resolve_access_token(), returning the existing AccessTokenResolution rather than a new coproduct, since an unavailable source is a refused ensure from where a caller stands. Both wrapper types are deleted.
  2. r2_mint_preflight_receipt_path re-spelled twice. Correct — the diff argued the point against itself. ci_spec and fleet_converge_workflow now import the symbol, matching the existing host_reset_return_receipt_path / spark_apply_receipt_path form.
  3. No consumer for the grant module. Correct. dag/test/claim/r2_mint_secret_access_witness_test.dag is enrolled, following the cited spark_secret_access_ensure_witness_test precedent — it asserts the targets join to the r2_origin declarations rather than being re-spelled, the role of each cell against the effect that needs it, and that no cell is conditioned. The converge entries themselves stay operator-invoked with no CI route, exactly as spark_secret_access_converge is: the control plane that may call setIamPolicy is deliberately not the workload.

Twin check against enforcement_gate_positioned_after_the_effect_it_guards — sibling, kept separate

Checked before landing, at royal-eagle-761's request, against that row's own recognition rule. Verdict: genuinely a sibling, not a twin. The new row now cites it in its distinguishing facts so a reader can tell them apart, and the landed row is left untouched.

  • The landed row: a refusal is computed before the effect and the path fails to branch on it. The verdict is in hand, correct, and only ever read at serialization time — so the run changes the world and then accurately reports that it should not have.
  • This row: no verdict exists at the moment of the irreversible effect, because the operation that would produce it is sequenced after. Every branch is present and correct at its own site; r2_token_mint_run did refuse honestly when CreateSecret failed — it refused about a token that had already been minted.
  • Opposite remedies: branch on a value you already have, versus hoist an observation you never took and delete a write.

The test that makes this load-bearing rather than taxonomic is the landed row's own next-rung trigger. That trigger is an effect-ordering reader refusing any path that reaches a declared irreversible operation without first branching on the guard naming it. Run it against this defect and it passes clean: no guard named cloudflare.AccountTokens.Create, and the CreateSecret refusal that did exist named CreateSecret. A capability that fully closes that class leaves this one untouched — so merging the rows would retire this one behind a wall that cannot see it.

The axis the two share is already named in the landed row's boundary against declared_refusal_unreachable_from_its_own_constructor ("there the arm is never reached; here it is reached, and reached too late"). This is the third point on it: the arm is reached, on time relative to its own operation, and that operation is itself the thing sequenced too late.


Review 64955 (dashboard-only, REQUEST_CHANGES) — fixed

The finding is right, and it landed in the worst possible file to get wrong: r2_mint_secret_access said "two secrets, three cells" and "the three converges" above four grant functions and four converge entries. The count went stale the moment I added the viewer cell — the cell the same file calls the one an author would most easily omit.

The fix is not 3 → 4. §4c says an annotation "must not restate what the declaration structurally says," and a count is exactly that restatement — it is the part that rots, and it rots worst in the one module whose purpose is to replace an operator's memory of which bindings are required. So the counts are removed, not corrected: the prose now says "one secret and one role per cell" and "every converge entry in this module, one per cell," and points at the declarations and at test.claim.r2_mint_secret_access_witness_test, which asserts each cell against the effect that needs it. The annotation keeps a short receipt of the stale-count incident as rationale, since that is the irreducible part §4c does admit.

Two stale counts in this PR body are corrected the same way. The operator-facing ceremony is the four-step block in the Update section above, and the module is authoritative over both.

— sent from smart-otter-889


Landing sequence, admission standing, and the follow-up

The src/v1 edit is admitted as a consumed ledger entry, not an escape hatch. gunbc.v1_maintenance_standing classes a diff over admission rosters as MaintenanceRefusal::NewEscapeHatchOrAdmissionRow, and that class dominates purpose admission — so this PR's fifteen rows were refused at disposition until the standing was asked to rule on the class itself. It has: a NAMESPACE_TRANSITION_ADMISSIONS row is a consumed ledger entry, provided (1) its deletion follow-up is authored before the carrying PR is enqueued and (2) it lands so the rows are gone from main. The distinguishing property is transience with a consumption trigger — a row adjudicates one measured delta by exact identity and is consumed by the merge; it does not exempt. Adjudicated by fierce-lark-661; the receipt row lands in gunbc.v1_maintenance_standing via eager-raven-113's tree, and this body cites that adjudication until the row is on main.

Condition (1) is satisfied: #11259 (draft) deletes all fifteen rows and is open now, before enqueue. It must land immediately after this PR.

PURPOSE admission for the src/v1 edit (gunbc.v1_maintenance_standing v1_seed_standing): the only src/v1 change here is fifteen rows in the namespace-wave admission roster — the gate that admits .dag namespace motion into the required lane — so it serves the v2 self-host program by letting a substrate relocation land, and adds no capability to v1 for its own sake.

Why the relocation was not simply dropped. Reaching the WorkloadIdentityToken arm without moving symbols is feasible — the selection fold can live in gcp_secret_access with zero rows — and it was explicitly rejected: dodging the roster to avoid writing rows is DESIGN §5's workaround, the author-side absorbing fallback that routes around the substrate rather than root-causing it. The layer defect it would have preserved (a run's credential selection owned by the grant-ensure module) is the thing this PR exists to remove.

Ordering against gunbc#11240. That PR deleted the same two consumed gunbc#11156 rows this branch had also deleted. It landed first (6e70be2514), and this branch has since merged main: this PR's copy of that deletion and its dissolution receipt are dropped — one receipt per consumption event, and #11240 carries it — leaving only the fifteen relocation rows and their own DISSOLVE-ON.


Consumed-row credit. This branch retires the gunbc#11193 admission row it inherited from #11347. gunbc#11356 is the canonical owner of that deletion — this is the same deletion, taken here because the namespace-wave roster charges a consumed row to whoever touches it next, and identical deletions merge cleanly when #11356 lands.

Brian Searls and others added 2 commits September 12, 2026 22:51
…operator grant cells, preflight

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 12, 2026 23:11
Brian Searls and others added 4 commits September 12, 2026 23:27
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…lect-then-realize once

The mint no longer calls CreateSecret. R2MintCustodyContainerAbsent is decided by
a readonly GetSecret inside classify_r2_mint's ladder, ahead of the non-idempotent
AccountTokens.Create; store_minted_token is AddVersion-only over a container seal
observed before anything was spent.

Also addresses review 64860: the select-then-realize composition now lives once at
gunbc.auth.access_token_source resolve_access_token, the preflight receipt path has
one authority, and the grant module has an enrolled witness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…e-spelling them

The emitted fleet-converge.yml is byte-identical, which is the point: these were
three spellings of one fact, and collapsing them changes nothing a consumer sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
@gunbai-bot

gunbai-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Re review 64903 (dashboard-only, REQUEST_CHANGES, /api/reviews/64903/artifacts/stdout.log).

The finding was right, and it was already fixed at the head that landed two minutes after that review started. 64903 ran against 22e1972c41; the fix for it went in with 1bb574cddc as part of the response to review 64860, which raised the same item. On the current head the string appears exactly once in the tree:

dag/gunbc/cloudflare/r2_permission_group_observe.dag:279:data r2_mint_preflight_receipt_path: String = "target/r2-mint-preflight-permission-groups.txt"

ci_spec and fleet_converge_workflow import and consume that symbol.

The prior debt named in the same finding is now fixed too, in dbc510b4b5. The review was explicit that app_control_plane_receipt_path is "prior debt of the same class, not a licence" — correct on both halves, and since the fork lives in the two files this PR already edits, leaving it would have meant shipping a consolidated instance sitting beside an identical unconsolidated one, which is the worst of the three states to leave a reader in. Collapsed:

  • app_control_plane_receipt_path — was 3 spellings (app_control_plane_converge.dag:84, ci_spec.dag:1738, fleet_converge_workflow.dag:1183), now 1.
  • org_actions_validation_receipt_path — was 2 (org_actions_converge.dag:46, fleet_converge_workflow.dag:1201), now 1. Not named in the review; same class, same file, found while fixing the first.

Evidence that this changed nothing a consumer sees: .github/workflows/fleet-converge.yml regenerated from its authority after the change is byte-identical (75783 bytes, empty diff). That is the property a pure §3 consolidation should have — the fact was already one fact, it just had three names.

— sent from smart-otter-889

Brian Searls and others added 4 commits September 13, 2026 00:33
…he_effect_it_guards

Checked against that row's recognition rule and kept separate: its next-rung
trigger -- refuse any path reaching an irreversible operation without first
branching on the guard naming it -- passes this defect clean, because no guard
named AccountTokens.Create existed to branch on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
The annotation said three cells above four grant functions: it went stale when the
viewer cell was added, in the one module whose purpose is to replace an operator's
memory of which bindings are required. Removed rather than corrected -- the roster
is the declarations and the witness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…the live token id

AccountTokens.Create returns a value once, so a store refusal after it leaves a live
credential on the account. The refusal demanded revocation, which is performed BY ID,
and the id was in scope and thrown away: the success path wrote token_id, the refusal
path wrote nothing. The id now goes in the receipt AND in the typed failure, because a
remediation recoverable only from a file that may not exist is not located.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
observed_container_project/secret_id/resource were authored by reflex from the
sibling seal, whose accessors are consumed externally; these had no call site at
all and the only reader of the fields is in the same module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
@gunbai-bot

gunbai-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Re review 65039 (APPROVE, dashboard-only, /api/reviews/65039/artifacts/stdout.log) — the non-blocking observation is correct and I am deliberately not fixing it here.

The observation: dag/test/claim/r2_mint_secret_access_witness_test.dag:33 re-spells the convergence SA email that gunbc.gcp.gcp_estate_observation fleet_cloud_convergence_sa_email already owns. That is the same §3 class I fixed twice in this PR (the preflight receipt path, then the two pre-existing ones), so I am not going to pretend it is a different thing — it is one more literal where a modeled value exists, and it should be joined to the declaration.

Why it is not in this PR anyway: the trade is bad right now. This branch has produced eight CI starts and zero receipts, because every review fix superseded the run in flight through the concurrency group. Run 34733017987 is currently in flight on this head, which is also the head this approval is bound to. Pushing a two-line change to a witness fixture input would cancel that run and stale the approval, and I would be back to having no CI evidence at all — for a literal that, as the review notes, no assertion depends on being current.

So: it lands as a follow-up, not as a ninth supersede. Recorded here rather than left in a review artifact so it does not get lost.

One thing worth stating precisely, since it affects how the follow-up should be written: the email at that line is the member argument to secret_access_desired_cells, not an assertion about the estate. The right repair is to import fleet_cloud_convergence_sa_email and build the member from it the way grant_member does — which makes the test say something true about the principal that will actually be bound. Substituting an obviously-synthetic fixture member would also remove the fork but would weaken what the test claims. The current state is the worst of the three: it reads like a join and is a copy.

— sent from smart-otter-889

Brian Searls and others added 2 commits September 13, 2026 04:02
…er move

The wave wall caught exactly the class I warned a peer about an hour earlier: a
symbol relocation is a semantic change with no merge conflict to announce it.
Fifteen TargetChanged rows, five declarations times three spellings, enumerated by
exact identity rather than patterned over the module pair. DISSOLVE-ON: this PR
merging, at which point base and head both carry the move and all fifteen go stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
# Conflicts:
#	src/v1/stage0/src/namespace_wave_admission.rs
Brian Searls and others added 4 commits September 13, 2026 04:49
… own change falsified

The String entry was kept 'so nothing that invoked it is renamed'; nothing invoked
it, so that was compatibility with no backward. Deleted per DESIGN 3c/delete-first.

The frontier row it anchored said the listing 'has no automated caller' -- true
before this PR, false after it, since the r2_mint_preflight mode now runs the wet
entry and folds its standing. Repointing it without rewriting it would have left a
frontier asserting what the same commit made untrue, so the row is narrowed to the
half that genuinely remains: the ids consumers read are still transcribed beside the
listing rather than derived from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…st not claim a certainty the run lacks

Two defects, both mine, both introduced by the fixes in this PR.

(1) The container observation sat in r2_mint_bootstrap, the bracket SHARED by all
four entries -- so list_account_tokens_for_reconcile and revoke inherited a GetSecret
requirement on a secret whose absence is what they exist to clean up after, and which
the mint's grant cells do not cover for the read profile. The orphan receipt told the
operator to run exactly that command. A remedy that depends on the thing that failed
is not a remedy. The requirement is now supplied by the entry.

(2) r2_orphan_receipt_content printed stored=no for every store refusal, including
VersionAdditionOutcomeUnknown (the write may have committed) and VersionAddedButUnverified
(a version did land, at a known identity). That is a fabricated certainty whose
prescribed remedy -- revoke and forget -- is the one action that could follow a
double-write. Three arms, three truths, three remedies, with reds for each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…ool membership

It compiled without the named import because the spelling found its declaration
through pool membership -- the exact class gunbc#11156 repaired on main, where a
module called Filesystem.Write while importing the declaring module under no
spelling at all. A green compile is not evidence the name was named, and this one
sits on the pre-mint container observation, the check this PR's ordering argument
rests on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…wn rule

Their trigger fired: #11156 merged, so base and head both carry the named imports and
the floor reported them as '2 consumed admission(s) due for deletion'. The roster's
convention charges that deletion to the next change that touches the file, so it is
paid here rather than inherited by an unrelated lane -- the same way main's sweep of
181 rows was charged to the change that next touched it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
Brian Searls and others added 2 commits September 13, 2026 08:49
# Conflicts:
#	src/v1/stage0/src/namespace_wave_admission.rs
The slice index caught the bracket in the TYPE &[TransitionAdmission] rather than
the value's &[, so the resolved const carried a second '= &[TransitionAdmission]'.
Committed broken because cargo check was piped into tail, which masked its exit code
behind tail's zero -- the '&&' chain then ran on a failed build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
gunbai-bot Bot pushed a commit that referenced this pull request Sep 13, 2026
… the inversion

Review 65393 noticed and deliberately did NOT file this: the header said
"THE CONDITIONS ARE FIELDS, NOT PROSE, because a condition written as
narrative is one a later reader satisfies by paraphrase", while
`condition_authored_before_enqueue` and `condition_follow_up_lands` are
themselves `NonEmptyStr` paragraphs. The sentence is false about this row.
The field split makes the two conditions separately citable and separately
falsifiable; it does not close the paraphrase gap it claimed to close.

Corrected, with what WOULD close it named rather than promised: a
condition decidable from the repository -- an authored-before timestamp
against an enqueue timestamp, which is exactly the pair this row's own
specimens were corrected by. Not modelled, because nothing consumes it
yet and modelling ahead of a consumer is the dangling declaration §3c
refuses.

ALSO FOLDED IN, bright-boar-435's sharper reading of the specimen list:
#11214 is the ONLY specimen that can meet condition one, because its
carrier has not been enqueued. That inverts how the list reads -- the
pending case is the potentially-compliant one and the two closed
"successes" are the non-compliant ones -- and the row now says so where
the specimen sits.

I am aware this pushes a fourth head on an approved, green PR and that
the current fleet makes a green required context expensive to regain. A
false sentence in a typed ledger row is cited, not corrected by the next
message, so the trade is the right way round even though it is not free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5dKcSuYW3nvAjtVcDxqhT
gunbai-bot Bot pushed a commit that referenced this pull request Sep 13, 2026
…rstatements withdrawn

Side-chat HOLD on gunbc#11260, three findings. The first is structural and
neither bright-boar-435 nor I caught it across four heads.

1. THE ROW FORBADE ITS OWN SEQUENCE. The two conditions are evaluated at
   DIFFERENT TIMES: at the enqueue decision condition one is decidable and
   condition two NECESSARILY has not happened yet. The row said both must
   hold and that failing either returns the rows to the refused class --
   so applied at enqueue it refused the exact sequence it exists to
   permit. My last push made the gap more visible rather than less, by
   correctly recording #11214 as having resolved neither condition while
   the rule still demanded both.

   Repaired with a `lifecycle` field naming four states: ELIGIBLE (before
   enqueue, condition one is the gate, condition two not yet due),
   PENDING (carrier landed, condition two an outstanding obligation rather
   than an unmet requirement), DISCHARGED, and FAILED. Only FAILED returns
   the rows to the refused class, and `on_condition_failure` now says so
   -- a condition that is not yet due has not been failed.

   Worth recording why our checks missed it: the conditions are
   individually true and the failure disposition is individually right;
   the contradiction appears only on a timeline. Every pass we ran was
   per-sentence.

2. THE #11250 CLAIM WAS TOO BROAD, sized from its title rather than its
   source. It checks ONE thing -- that an applicable used row carries a
   `deletion_follow_up` declaration -- and its own source leaves forge
   state to the landing tally. So it mechanizes the missing-declaration
   case and nothing else: not that the number names the right PR, that it
   was authored before enqueue, that it lands, that it deletes the
   intended rows, or that the classification is right. Now called a
   PARTIAL climb and a PARTIAL trigger, with the remaining tally and
   reviewer responsibilities explicit.

3. "EXEMPTS NOTHING" WAS TOO ABSOLUTE. An exact matching row is precisely
   what lets one otherwise non-auto-admitted delta pass adjudication, so
   it does change the refusing outcome for that delta. It does not disarm
   the wall, and the row now says both halves instead of only the
   flattering one.

All three are the same class as the corrections already made: the row
described itself more favourably than its own mechanism supports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5dKcSuYW3nvAjtVcDxqhT
gunbai-bot Bot pushed a commit that referenced this pull request Sep 13, 2026
…ed specimens are outside it

Side-chat verdict on fe763ef cleared all three prior findings and
named one more; this is that repair plus two the audit found alongside it.

(a) THE REVIEWER'S FINDING, in their wording. #11214 was labelled PENDING
    while this row's own new lifecycle defines PENDING as post-land and
    consumed, and the same row states its carrier has not been enqueued.
    At that evaluation moment it is ELIGIBLE. Verified independently:
    #11214 is OPEN, not merged, not in the queue.

    Worth naming why it arrived WITH the repair: the row used "pending"
    informally before the lifecycle existed, meaning "not finished yet",
    which was true and unobjectionable. Defining the term turned every
    prior informal use into a claim in the new vocabulary.

(b) SO THE VOCABULARY IS NOW SCOPED. Grepping the carrier for each newly
    defined name found two more prior uses, both in OTHER declarations:
    `RefusedV1Proposal.reconsider_when` ("ELIGIBLE FOR RE-ADJUDICATION")
    and the unadjudicated-disposition prose ("would assert that it
    FAILED"). Both predate this field and are correct in their own terms.
    They are untouched; this row says instead that the four names are its
    own lifecycle and not the file's vocabulary. Editing another
    declaration's prose to protect my vocabulary would be the worse trade.

(c) AND THE CLOSED SPECIMENS ARE OUTSIDE THE LIFECYCLE ENTIRELY, which is
    stronger than the excuse I first drafted. Under the previous text they
    were in FAILED, and only FAILED returns rows to the refused class --
    so the row could be read as asserting that two landed changes should
    have been refused and were not. Annotating that with "the rule
    postdates them" removes the accusation and leaves the claim. The
    lifecycle simply does not range over them: adjudicated before the
    boundary existed, never ELIGIBLE, never PENDING, not in FAILED -- not
    because they behaved well but because they are not its subjects.

    Both facts survive and stay separate. MEASURED: both deletions were
    authored after their carrier merged, 1h09m and 2h24m, so both fail
    condition one as CHRONOLOGY, which is the evidence that makes the
    condition new. NOT CLAIMED: neither is in a lifecycle state.

    Which yields the sharper statement the row now carries: this
    lifecycle's subject population today is exactly one, and it has never
    completed a traversal.

Carry-forward the reviewer named, worth more than the fix: for every
multi-condition rule, name the evaluation event for each condition and ask
whether that condition can even be true at that event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5dKcSuYW3nvAjtVcDxqhT
gunbai-bot Bot pushed a commit that referenced this pull request Sep 13, 2026
…ons removed

bright-boar-435 caught that my last push flipped "above" to "below" in the
#11214 specimen's inversion sentence. The specimens are `gunbc#10940`,
`gunbc#11156`, `gunbc#11214` in that order, so the closed two are ABOVE
it. The head I replaced said "above" and was right; the repair broke it,
in the one sentence whose entire job is to tell a reader which specimens
are the compliant ones.

THE FIX IS NOT TO FLIP THE WORD BACK. "Above"/"below" is a positional
citation inside a data row -- a second naming scheme for things the row
already names, which DESIGN §3 rules against for exactly the reason that
bit here: any edit to the array reorders it silently, and this row will
grow a fourth specimen. Both references now name the specimens, which is
stable under reordering and under growth.

AND THE AUDIT FOUND A SECOND ONE THEY DID NOT FLAG: the `lifecycle` field
also said "the two closed specimens below". That one happened to be
positionally TRUE -- the field precedes the array -- which is what makes
it worth removing rather than leaving: a true positional citation is
exactly as fragile as a false one and reads as more trustworthy. Also
named now.

Re-audited the whole row for positional words: `above` 0, `below` 0,
`later` 0, `next` 0. The two surviving `earlier`s are temporal, about
prior drafts of this row, not positional.

The form to copy is the one already in the two closed specimens: "per the
`lifecycle` field" cites a symbol and is stable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5dKcSuYW3nvAjtVcDxqhT
gunbai-bot Bot pushed a commit that referenced this pull request Sep 13, 2026
Review 65450, REQUEST_CHANGES, both findings correct and both the DUAL of
the repair the previous head made.

1. ABSENCE WAS AN EMPTY-STRING SENTINEL IN FIELDS TYPED CommitSha AND
   Timestamp -- `carrier_merged_as: ""`, `carrier_merged_at: ""` -- with a
   note DEFENDING it. That is precisely the conflation this row had just
   pulled out one field over: `PredatesBoundary` exists so "not in the
   machine" would not live inside the machine, while "not merged" stayed
   inside the merge fields as a sentinel. DESIGN §5: "Stronger than
   CATCHING a wrong state is making it UNWRITABLE -- correctness by
   construction, not validation." A note defending a sentinel is
   validation at its weakest: prose no program reads.

   `CarrierStanding = Merged { as, at } | NotEnqueued` now has no
   constructor for the invalid state, which is the coproduct move the
   review named and the one this row already used once.

2. AND I HAD ASSERTED A VERDICT WITH ITS EVIDENCE FIELD EMPTY.
   `condition_one: Met` for the gunbc#11214 specimen, while
   `deletion_authored_at` was `""` -- and that field is the ONLY witness
   the condition has, the same field the two closed specimens use against
   `carrier_merged_at` to justify FailsAsChronology. So the row asserted
   Met and carried nothing that could establish it.

   MEASURED RATHER THAN REMOVED, because the verdict turns out to be
   right: gunbc#11214's deletion follow-up is gunbc#11259, authored
   2026-09-13T09:07:38Z at commit bc5b6b4, while #11214 is still open
   and unenqueued. Condition one is Met and now carries its witness.

   This also retires the meaning fork the review flagged:
   `deletion_follow_up` held "authored; carrier not yet enqueued" --
   commentary in a field whose other two values name a PR. All three now
   name a PR and nothing else.

Every specimen now carries real values; no sentinel survives in any
field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5dKcSuYW3nvAjtVcDxqhT
Brian Searls and others added 4 commits September 14, 2026 02:25
The floor red on fce2c33 was not this diff and not the lottery: it was
'7 consumed admission(s) due for correction or deletion' -- main's #11182 rows,
inherited when #11220 came in, consumed once #11182 landed, and charged to this
roster's next toucher. gunbc#11274 owned that deletion and has landed, so the rows
are gone at base rather than deleted here; duplicating its receipt was declined.

Resolution: main's file whole (empty roster plus its retirement receipt), with this
branch's paragraph and its fifteen rows spliced back in. The seven are NOT
resurrected -- asserted in the splice, not eyeballed. 16 entries = 1 struct + 15.

cargo check -p v1-compiler exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…r, do not re-spell it

review 65039 flagged dag/test/claim/r2_mint_secret_access_witness_test.dag:33 as a
literal where gunbc.gcp.gcp_estate_observation fleet_cloud_convergence_sa_email
already owns the value, and I deferred it as non-blocking to protect a CI run that
went red anyway. The deferral was wrong on two counts.

It is the same §3 fork this PR repaired twice already. And a duplicate-declarer scan
over the sixty top-level names this diff adds found exactly one collision: three
sibling witness files already declare `convergence_member`, so the deferred line made
a FOURTH -- and .dag names are corpus-global, so that is not local to the file that
writes it.

Repaired as the review prescribed: import the estate declaration and build the member
the way gunbc.auth.gcp_secret_access grant_member does, so the witness asserts
something true about the principal that will actually be bound rather than about a
transcribed string. Re-scan: sixty names, zero duplicates.

This is NOT claimed as the cause of the AmbiguousBareNameRead refusal in
gunbc.output_policy -- that module reads nothing this diff declares, and attribution
of that refusal remains unresolved pending main's own run on 0d96f5e.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…ate_observation

I inferred the module path from the FILE path -- dag/gunbc/gcp/gcp_estate_observation.dag
read as gunbc.gcp.gcp_estate_observation -- and the declared module is
gunbc.gcp_estate_observation. DESIGN says it directly: a fact's home is its LAYER, not
its file, and paths are discriminators rather than gospel. Thirty-nine files in this
tree already import it correctly; reading one would have cost nothing.

The floor caught it as two located errors on dag/test/claim/r2_mint_secret_access_witness_test.dag
(unresolved import at :31, undefined variable at :43), which is the wall working.

Verified before pushing, binary built from this tree into this worktree's own target
dir (not the shared /cargo-target): cargo build exit 0, and gunbc compile on the
witness gives 0 blocking errors. Every import line this diff adds was re-scanned for
the same defect; there are no others.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
@gunbai-bot

gunbai-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Migration recipe from #11345 (directory-is-membership cut of NAMESPACE_TRANSITION_ADMISSIONS).

If this PR still appends const rows:

  1. One file per row at dag/gunbc/namespace/transition_admission/<stem>.dag. Stem is the relocation identity in stem form, not the owner PR number.
  2. TransitionAdmission { label, subject, disposition } with decl_ref for enclosing and expected_candidates.
  3. Drop the Rust hunk. Do not reintroduce a list.

#11220 already merged; this note is for any remaining const hunk on this branch.

Brian Searls and others added 4 commits September 14, 2026 06:25
…i_tokens

Two fixes, both located by reviewers or the floor rather than by me.

review 65984: dag/test/claim/cloudflare_r2_origin_mint_run_witness_test.dag bound
R2MintContainerAdmission TWICE in one import block and used it neither time -- I
appended to that block in two separate edits without re-reading it. Both dropped.

The floor's AmbiguousBareNameRead refusal is now fully located, and it is NOT in
gunbc.output_policy -- that is the CLAIM scope. `String` is read bare by
extdeps.cloudflare.account_api_tokens, which imported it from std.types; std.types
does not DECLARE String, std.string_type does (type String = FreeMonoid<Char>), so
that import bound nothing and left the name ambiguous between std.string_type and
v2.std.text. The wall's own remediation text warns that copying an adjacent file's
import line is the usual way to get this wrong.

The ambiguity pre-exists this PR; what this PR did was bring the module into the
analysed scope by importing it from a new witness. Fixed the way the 41 sibling
extdeps modules already do it: name std.string_type, and stop naming std.types for a
declaration it does not own.

Verified: gunbc compile on r2_token_mint_run and on the mint witness, 0 blocking
errors each, binary built from this worktree's own target dir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…convention

review 66010: dag/gunbc/fleet/fleet_converge_workflow.dag typed
"/tmp/r2-*-object-write-mint-receipt.txt" while gunbc.cloudflare.r2_token_mint_run
r2_mint_receipt_path owns that convention. My annotation there defended the wildcard
-- it does avoid re-spelling the bucket name -- and said nothing about the "/tmp/r2-"
prefix and "-mint-receipt.txt" suffix around it, which are exactly the parts the
producer owns. A defence of half a line reads like a defence of the line.

The drift would have been SILENT: the upload step is if-no-files-found: warn, so
renaming the producer's path loses the orphan receipt in precisely the run where a
live unheld Cloudflare credential exists and its id is the only thing that makes the
damage recoverable.

Three declarations now carry it once each -- r2_mint_receipt_path_prefix,
r2_mint_receipt_path_suffix, and r2_origin_token_name_suffix, the last extracted
because the token NAME and the receipt GLOB both need "-object-write" and only one of
them has a bucket in scope. r2_mint_receipt_glob composes them and the emitter calls
it.

PROOF THE DERIVATION IS FAITHFUL: regeneration emits a byte-identical file (75775
bytes, empty git diff) with the same glob line. The source of the string changed; the
artifact did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…teen

Only namespace_wave_admission.rs conflicted, as it has every time. gunbc#11347 named
the bare-read authority in three extdeps modules -- the same class of fix this branch
made for extdeps.cloudflare.account_api_tokens -- and admitted the one leaf that
narrows. Its row and this branch's fifteen are both OPEN, so both survive: 17 entries
= 1 struct + 1 (#11193) + 15.

main's const arrived rustfmt-collapsed onto the declaration line (`= &[TransitionAdmission {`)
because it held a single row, so the splice normalises it back to list form before
appending; cargo fmt then owns the indentation. Verified: cargo check exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…as its owner

The floor red on 67a369e was a single blocker: '1 consumed admission due for
correction or deletion'. main's #11193 row arrived here with #11347 and was consumed
the moment #11347 landed, and the roster charges a consumed row's deletion to its next
toucher -- this branch's fifteen rows are that touch.

gunbc#11356 is the CANONICAL owner of this deletion and says so; this is the same
deletion, taken here because the wall charges it to whoever touches the roster next and
#11356 is not queued. Identical deletions merge cleanly when #11356 lands. Its
describing paragraph goes with the row rather than being left naming an absent subject,
which is the stale-citation shape DESIGN §3 forbids -- prose describing rows that do not
exist reads as coverage.

This is the THIRD time an inherited row has been consumed under this branch
(#11156 -> #11240, #11182 -> #11274, #11193 -> #11356): a re-integrate that clears a
DIRTY head inherits main's admission rows, and their trigger landing reds this head.
gunbc#11345 cuts the const roster to directory rows and ends the class.

cargo check exit 0; 16 entries = 1 struct + this branch's 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
gunbai-bot Bot pushed a commit that referenced this pull request Sep 14, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>

#11356 owns that deletion (also carried on #11214). Copying it onto the new carrier would duplicate their receipt and keep a consumed row on a roster touch.
Brian Searls and others added 5 commits September 14, 2026 10:10
review 66101: two sites typed "gunbai-secrets" while gunbc.fleet_secrets_config
fleet_secrets_gcp_project owns it and gunbc.secret_provision fleet_secret_ref already
consumes it -- the same §3 fork this PR fixed for the convergence member, on the other
axis.

THE WITNESS COMPARISON IS NOT MADE VACUOUS BY THIS. Those targets do happen to be built
through fleet_secret_ref, but the assertion exists so that a target which did NOT go
through it -- hand-built with some other project -- fails. Comparing against the
authority catches that exactly as well as the literal did, and additionally survives a
legitimate project rename, which the literal would have turned into a false red.

The mint's intervention instruction joins the authority into its prose for the same
reason: an operator is told which project to create the container in, and that sentence
should not be able to disagree with the SecretRef the run will use.

I inferred the module path from the FILE path AGAIN -- dag/gunbc/fleet/fleet_secrets_config.dag
read as gunbc.fleet.fleet_secrets_config, where the declared module is
gunbc.fleet_secrets_config. Second time this session, same class, caught locally this
time by compiling before pushing rather than by CI.

Verified: gunbc compile 0 blocking errors on both r2_token_mint_run and the witness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
Both sides deleted the consumed #11193 row -- this branch took it on my parent's ruling
because the roster charges a consumed row to its next toucher and #11356 was not queued,
and #11356 has now landed as the canonical owner. Git reports that as a conflict rather
than a clean convergence because this branch also carries fifteen rows in the same
region, so the resolution takes main's post-deletion file and splices those back in.

16 entries = 1 struct + this branch's 15. #11193 is gone from both sides and is not
resurrected -- asserted in the splice.

cargo check exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
… self-hosted runner

REFUSED at vivid-bee's thread on 4c23cca, one blocker, and it is real. The uploader
runs with always() over a shared /tmp glob; a self-hosted runner's /tmp survives across
attempts; an attempt that refuses EARLY writes no receipt of its own. So the uploader
could collect attempt N's orphan receipt and present it as attempt N+1's evidence -- and
an operator reads that receipt to decide WHICH LIVE CLOUDFLARE TOKEN TO REVOKE, so a
stale one names the wrong token.

That is this PR's own filed failure mode arriving from the other side: there a check was
correctly written and positioned after the irreversible effect; here an artifact is
correctly written and readable as belonging to a run that did not produce it. Both are a
receipt asserting what the execution does not establish.

THE DIRECTORY CARRIES THE ATTEMPT AND IS DECLARED ONCE. r2_mint_receipt_dir_root,
r2_mint_receipt_scope and r2_mint_receipt_dir_for live with r2_mint_receipt_path; the two
callers differ only in where the identity comes from -- the mint reads GITHUB_RUN_ID /
GITHUB_RUN_ATTEMPT on the runner, the emitter writes the workflow expressions -- and
neither spells a path. Same shape as the 66010 repair.

R2MintReceiptUnscoped is NAMED, not defaulted: a workflow run missing the identity writes
to the root, and because the uploader names an attempt directory that file is evidence for
NOTHING rather than evidence for the wrong attempt.

THE CONTROL THE REFUSAL ASKED FOR IS EXECUTED, not described: a stale receipt at the SAME
run id and the PREVIOUS attempt is a real receipt and lies outside this attempt's
directory (same-run-different-attempt is the sharp case -- a re-run is exactly when the old
file is still there); an unscoped receipt lies outside every attempt directory; and the
emitted glob pins the attempt while wildcarding only the bucket.

The emitted yaml CHANGED, 75775 -> 75839 bytes, which is the inverse of the 66010 check:
there byte-identity proved the derivation faithful, here a changed artifact proves the fix
reached the runner. Emitted path:
  /tmp/r2-mint-receipts/${{ github.run_id }}-${{ github.run_attempt }}/r2-*-object-write-mint-receipt.txt

Also restored `fn` on a new declaration I wrote as `func` out of habit; #10850 removed that
form hours ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…have ENOENT'd every mint

review 66170, and this one is mine end to end -- the attempt-scoping repair introduced it.
Filesystem.Write is realized as bare std::fs::write (src/v1/05_emit_rust.dag
file_write_expr) and does NOT create parents. On main the receipt sat directly in /tmp,
which always exists. Scoping it under /tmp/r2-mint-receipts/<run>-<attempt>/ made the
write depend on a directory nobody creates -- and on a workflow run BOTH GITHUB_RUN_ID and
GITHUB_RUN_ATTEMPT are always set, so EVERY mint would have taken the nested path and
failed with ENOENT.

THE CONSEQUENCE WAS THE WORST ONE THIS FLOW HAS. After a SUCCESSFUL store the failed
receipt turns ExitSuccess into a failure; the origin pin is a declaration edit, so
origin_credential_already_pinned stays false; and a re-dispatch spends ANOTHER
non-idempotent AccountTokens.Create. A safety repair that manufactures a double-mint path
is worse than the hazard it closed.

FIXED AHEAD OF THE CREATE, NOT BESIDE THE WRITE, which is this PR's own thesis applied to
its own repair: ensure_r2_mint_receipt_dir runs inside the ladder before
mint_r2_origin_token, so a filesystem that cannot hold the receipt refuses with NOTHING
SPENT -- the same ordering the custody container has, for the same reason. The prescribed
fix was mkdir beside the write; placing it in the ladder also covers the orphan path,
which writes after the Create and would otherwise still be exposed.

shell.Mkdir.Parents is the tree's precedent (gunbc.auth.materialized_secret,
gunbc.instruments.merge_admission_capture). It reports only exit status, so the refusal
says so rather than inventing an errno.

Witnessed: a scoped receipt's directory is strictly deeper than the root, so it cannot be
assumed to exist -- red if someone flattens the path and leaves the ensure looking like
ceremony, or keeps the nesting and drops the reasoning.

Emitted yaml unchanged at 75839 bytes: this fix is runtime-side and the upload path was
already correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
…nd prove it

review 66195, and this is the worst defect in the PR because of WHAT was broken: the two
probes that are THE control for the stale-receipt hazard -- the control the refusal
explicitly required -- were permanently green by construction.

CAUSE, MINE: a blind string-replace. The text "  r2_mint_receipt_dir_root," appears in
both the import block and the probe bodies, so adding the import inserted
r2_mint_receipt_dir as a spurious second argument to concat. The composed string was a
path the producer can never emit, so !string_contains(stale, this_attempt_dir) was true no
matter what r2_mint_receipt_path_in did. Flattening the receipt back to a shared path
would have left both probes green -- DESIGN §4b's decoration case verbatim: "permanently
green by construction, carrying no information, and worse than absent because it will be
cited as coverage". And this PR's own annotation cites that separation as protecting the
receipt an operator reads to decide which live token to revoke.

FIXED to the call the sibling probe already made: the expected directory now comes from
r2_mint_receipt_dir(scope: ...), the producer's own construction, so the probe cannot
drift from what the mint emits.

PROVEN BY EXECUTION, BOTH ARMS, because compiling is exactly what failed to catch this.
Planted a falsifier collapsing the scoped arm to the shared root and ran the composition
against the real producer:
  falsifier: stale=/tmp/r2-mint-receipts/r2-b-object-write-mint-receipt.txt
             attempt2_dir=/tmp/r2-mint-receipts/   separated=FALSE  (probe reds)
  restored:  stale=/tmp/r2-mint-receipts/R-1/r2-b-object-write-mint-receipt.txt
             attempt2_dir=/tmp/r2-mint-receipts/R-2/  separated=TRUE (probe greens)
The scratch entry used for that measurement is deleted; it was an instrument, not a
landing artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R9M9cY4iaqsqeaSDpT7g1
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 924b061 Sep 14, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/smart-otter-889 branch September 14, 2026 18:45
@briansrls
briansrls restored the session/smart-otter-889 branch September 14, 2026 18:49
gunbai-bot Bot pushed a commit that referenced this pull request Sep 14, 2026
Sixth re-integration, fifth append-vs-append on this one const. Main accumulated
three commits in the file — #11356 emptying the roster, #11256, and #11214
landing fifteen rows for the operator-supplied-token relocation — producing seven
conflict regions. Both row sets survive: fifteen plus six, verified by spelling
rather than by count.

Main's own "THE ROSTER IS EMPTY AGAIN, which is its resting state" paragraph is
dropped, and not on this branch's preference: it became false ON MAIN when
#11214 landed fifteen rows above it and nothing updated the prose. Keeping it
would have carried a claim contradicted by the array directly beneath it, which
is the stale-citation shape this file names as the defect to avoid and which this
branch has now repaired five times.

The sentence naming what remains below already records that it has gone stale
repeatedly; this merge is another instance and does not change its argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSpQ3EJ1Nn8V16yEDqbG4F
gunbai-bot Bot pushed a commit that referenced this pull request Sep 14, 2026
… wall

#11214 landed, so the operator-supplied-token relocation is at base and its
fifteen admission rows report CONSUMED — 0 unadjudicated, 0 stale, 15 consumed
due for deletion, with this branch's own six adjudicating cleanly. The wall
charges a consumed row's deletion to the roster's next touch and this branch is
that touch again.

Applied under the standing ruling rather than re-asked; this is the third time it
has fired on this branch and the second time for rows another lane authored.
#11259 is the authored deletion draft for these fifteen and is not emptied by
this — it keeps whatever else it carries, as #11274 and #11356 did.

Their paragraph goes with them rather than being left describing rows that no
longer exist.

Verified by identity: no #11214 reference survives anywhere in the file; this
branch's six stand unchanged at RepositoryVisibility 1, PublicRepository 2,
PrivateRepository 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSpQ3EJ1Nn8V16yEDqbG4F
gunbai-bot Bot pushed a commit that referenced this pull request Sep 14, 2026
The floor refused this branch with: namespace-wave-admission (0 unadjudicated
delta(s), 0 stale admission(s), 15 consumed admission(s) due for correction or
deletion). All fifteen are main's own gunbc#11214 rows -- the operator-supplied
-token realizer move -- and they are consumed because #11214 LANDED today, so
base and head of every build now both carry the relocation and there is no delta
left for them to admit.

Their deletion is owed on "landing or the roster's next touch", and this change
is that touch. The block describing them says so itself: "DISSOLVE-ON:
gunbc#11214 merging ... Remove them by that trigger, not by reinterpreting it."
The trigger fired; this is the removal it names, not a reinterpretation.

The describing block goes with the rows, which is the same shape gunbc#11356
used when it retired the #11193 row and its block together.

My earlier audit checked OUR rows against both parents and found one consumed
(the #11193 anchor leaf). It did not check MAIN's rows against a main that had
moved under us -- #11214 landed after this branch last merged. That is the gap
this commit closes: a roster's next touch owes the whole roster's consumed debt,
not just the toucher's own rows.

Row count: 89 -> 74. cargo check --release -p v1-compiler is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PS4GbDpDLPSCd16NRH5aZ8
briansrls pushed a commit that referenced this pull request Sep 14, 2026
…11345)

* Cut the namespace transition-admission roster to directory membership.

Delete the hand-Rust const so permission is one .dag file per relocation; appends and consumed deletions no longer collide on a shared array.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Type expected_candidates as DeclarationRef, not a string list.

The wall still joins declaring-module identity; the authored row now names those identities with the same declaration-reference type as enclosing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Supersede the const-ness-as-safety claim in the roster carrier.

Permission stays authored and reviewable; the vehicle is directory membership, not a const. Missing directory is the empty roster and admits fewer rows, never more.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Home the empty-roster standing record on the membership module.

The const's prose about what remains used to rot beside the array. Empty-is-not-permissive and the #11306/#11316 inheritance live on gunbc.namespace.transition_admission; this cut still inherits six files or zero, never a pre-migration onto an unlanded PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the leftover const census and refuse a phantom candidate leaf.

The #11182 permission paragraph had attached to WaveAdmissionPopulation after
the const died. Candidate DeclarationRef.decl_name must equal Binding.spelling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Enumerate the directory-row parse helpers on the seed census.

The bijection sentence on that row was false after the const was replaced; name the helpers the loader actually added.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Do not re-home the consumed gunbc#11193 admission as a directory row.

Co-authored-by: Cursor <cursoragent@cursor.com>

#11356 owns that deletion (also carried on #11214). Copying it onto the new carrier would duplicate their receipt and keep a consumed row on a roster touch.

* Own the used_row fixture strings; Binding is no longer &'static.

Co-authored-by: Cursor <cursoragent@cursor.com>

#11250's merge-queue tests used const-roster literals; the directory cut stores owned String.

* Stop claiming the admission roster is a .rs file.

Row files are .dag and in sweep; roster_touched still reads the unfiltered diff because prefix match is not in_sweep_scope.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 14, 2026
…ve_admission.rs)

main's #11214 added fifteen rows to the const NAMESPACE_TRANSITION_ADMISSIONS; this branch deletes
that const in favour of authored .dag rows. The rows were not migrated: #11214 has landed, so they
are consumed by their own landing (base and head both carry the relocation) and are retired here
rather than re-authored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Apo3eJwXeD9NxuinRp9zMN
gunbai-bot Bot pushed a commit that referenced this pull request Sep 15, 2026
The hoist rebinds one spelling in registry image-config; the consumed #11214 token-realizer rows come due on this roster touch and are deleted with it.

Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 15, 2026
The hoist rebinds one spelling in registry image-config; the consumed #11214 token-realizer rows come due on this roster touch and are deleted with it.

Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 15, 2026
The hoist rebinds one spelling in registry image-config; the consumed #11214 token-realizer rows come due on this roster touch and are deleted with it.

Co-authored-by: Cursor <cursoragent@cursor.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