Skip to content

R2 mint custody cells granted by iam-converge (grant sets); bootstrap creates bare custody containers (B') - #13298

Merged
gunbai-bot[bot] merged 32 commits into
mainfrom
r2-custody-iam-reach
Oct 5, 2026
Merged

gunbai-bot[bot] merged 32 commits into
mainfrom
r2-custody-iam-reach

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Order: #13291, then this PR. Stacked on #13291 (gcp-iam-bootstrap-land). #13286 is closed as superseded: its commit is merged in here, so this PR carries the six workspace cells, and the diff stands on #13291 alone. Retarget to main once #13291 lands.

Operator decision B' (2026-10-04, relayed by warm-badger-442 and still-dove-673): the R2 mint custody cells move from an operator gcloud session (GcloudPrintToken) to iam-converge, under the existing gcp-iam-converge approval mode. The bootstrap creates the custody containers bare, so no recurring identity holds secretmanager.secrets.create.

What changes

  • gunbc.auth.gcp_iam_converge: grant sets.
    • GrantSet / gcp_iam_converge_grant_sets add a second estate population beside the federation targets. A grant set is secret cells for an existing member, with no federation of its own.
    • The member is resolved by gcp_secret_access grant_member_resolution, the same fleet-cloud-convergence principal the gcloud ensure granted. So only who grants changes, never who holds.
    • plan_grant_set is pure. It plans per secret, because one secret carries viewer, versionAdder and accessor, and it emits one policy add per secret. It refuses on an absent secret, a partial reading set, or an unresolved member.
    • observe_and_plan_estate plans targets plus grant sets under one approval. It refuses when a label is shared between a target and a grant set, because diffs execute by label.
    • execute_grant_set_diff applies policy adds only, and refuses any create.
    • grant_set_bindings gives iam-converge's gunbcIamConvergeSecretPolicy on each grant-set secret, resource-local. iam_converge_bootstrap_readback adds versions.access and versions.add as ExpectDenied on each grant-set secret.
  • gunbc.cloudflare.r2_mint_secret_access.
    • The 13 r2_*_access_converge entries (GcloudPrintToken) are deleted: a replacement migration that cuts over at the root. They had no other consumers.
    • r2_mint_custody_grants is the one roster. The custody secret population is derived from it, never listed twice.
    • The head annotations no longer claim an operator session or a create on the mint path.
  • gunbc.auth.gcp_iam_bootstrap{,_model} (Bootstrap the approval-gated GCP IAM workflow (supersedes #12565) #13291's files; I told snappy-wolf-887 beforehand).
    • The identities step creates each grant-set secret bare when absent: observe, then create, then read back.
    • Create journal (side-chat NO-LAND fix). It reuses this module's election journal: iam_authority_record, iam_authority_pin, and WriteCreateNew under target/. Per secret and per operation (the bootstrap receipt), an election is written before CreateSecret.
      • A definite create that reads back present is closed.
      • A definite refusal is journaled refused, and a new operation may elect again.
      • An outcome-unknown create leaves the election unresolved. That blocks every later automatic create of the secret, even on a later absent GET, because a 404 after an ambiguous create is not evidence.
      • Observing the container present settles the election.
      • Otherwise only iam_bootstrap_custody_create_dispose (an explicit operator disposition) retires it.
      • Marks are read, never believed from a name (second NO-LAND fix). The target/ listing only discovers candidate operations (custody_candidate_operations). Every election and terminal mark is read through iam_authority_record, and must carry exactly custody_journal_wire(secret, operation) (iam_authority_record_matches). Any of the following makes the journal unreadable, so the step refuses:
        • a record that cannot be read;
        • a record that is empty or carries other bytes (for example a disposition interrupted after WriteCreateNew, a corrupt marker, or one restored from elsewhere);
        • a terminal mark beside an absent election.
          Only a valid terminal record resolves an election.
      • The decision is a pure fold over typed record readings (CustodyOperationRecords, as Bootstrap the approval-gated GCP IAM workflow (supersedes #12565) #13291's authority journal takes them), then custody_container_step and custody_after_create, which the effectful step consumes.
    • The capabilities step binds iam-converge's secret-policy role and iam-observe's secret reader per secret. No new role and no project-level binding.
    • The negative probes add versions.access and versions.add expected not-held on each custody secret. The deny policy is unchanged.
    • The printed plan names the bare containers.
  • gunbc.cloudflare.r2_token_mint_run. The container-create refusal now points to the bootstrap and a grant-set row, instead of an operator-identity run.
  • gunbc.auth.privileged_effect_census.
    • The r2_mint_bootstrap_read_access_converge row (GcloudInteractive) is removed, because its site is deleted.
    • Those effects are now covered by the existing gcp_iam_converge_apply_wet row (RealizedFederatedGrant), whose subject now names the grant sets.
  • fleet-converge. No YAML change. The R2 cells join the estate the existing gcp-iam-converge request and apply modes already plan and apply, so a workflow_dispatch plus the recorded approval is the route.

Operator: one run, after #13291 lands

iam_bootstrap_apply (gunbc.auth.gcp_iam_bootstrap) is run as documented in docs/plans/gcp-iam-bootstrap.md, with a fresh GUNBC_IAM_BOOTSTRAP_RECEIPT. It is idempotent: existing cells read back, and drift refuses adoption. It creates cloudflare-r2-workspace-{read,write}-token bare and adds the per-secret bindings. After that, a gcp-iam-converge dispatch and one tap grant all 13 cells.

Witnesses (claim_batch on BuildBuddy, full rosters, head c3967a8)

file result
test.claim.gcp_iam_converge_witness_test 25/25
test.claim.auth.gcp_iam_bootstrap_witness_test 23/23
test.claim.r2_mint_secret_access_witness_test 6/6
test.claim.authorization_pattern_selection_witness_test (the census executes and agrees) 25/25

Memory, pre-existing: gcp_iam_converge_witness_test is OOM-killed (rc=137) at a 16 GiB bind on #13291's own base as well (0/21 there), so this does not come from this PR. Its verdict needs CTRL_BUILD_RUNNER_EXEC_PROPERTIES=EstimatedMemory=60GB with GUNBC_BIND_MEMORY_CGROUP_BYTES=51539607552 (48 GiB), where it passes 25/25.

Discriminating REDs (§4b rung honesty)

Each wall was removed alone, on top of c3967a8: copy dag/, apply the single removal, run only the named claim, then restore dag/ from the copy. All 11 REDs failed at this head. At d601416 the earlier ten also failed in two identical passes, after BuildBuddy retried the dispatch. Restored PASS is the full-roster run above at the same head.

wall removal claim with wall removed restored
per-secret custody binding (bootstrap) custody dropped from iam_bootstrap_capability_bindings grant_set_secrets_are_bound_per_secret_never_on_the_project rc=1 FAIL PASS (23/23)
per-secret custody binding (iam-converge reach) grant_set_bindings() replaced by [] custody_secrets_are_bound_per_secret_and_their_payload_stays_denied rc=1 FAIL PASS (25/25)
versions.access/versions.add ExpectDenied the custody ExpectDenied probes removed custody_secrets_are_bound_per_secret_and_their_payload_stays_denied rc=1 FAIL PASS (25/25)
cross-invocation: an unresolved create blocks run 2 run 2 ignores the journal on an absent observation an_ambiguous_create_blocks_every_later_automatic_create (run 1: absent, unknown create; run 2: absent plus run 1's election, so it must refuse and never reach the create) rc=1 FAIL PASS (23/23)
outcome-unknown is not a refusal CustodyCreateOutcomeUnknown mapped to CustodyCreateRefusedDefinitely, which would release run 2 an_ambiguous_create_blocks_every_later_automatic_create rc=1 FAIL PASS (23/23)
settle by observed present a present observation ignores the unresolved election an_unresolved_create_is_settled_by_observing_the_container rc=1 FAIL PASS (23/23)
a definite refusal releases a new operation (positive control) the refused mark is not honored resolved_journals_admit_a_new_create_and_unreadable_ones_refuse rc=1 FAIL PASS (23/23)
a terminal mark settles only with its exact wire custody_record_valid answers true without comparing bytes a_terminal_mark_settles_only_with_its_exact_wire: a valid election, a disposed marker that is empty, carries another operation's wire, or is unreadable, and an absent container, must refuse and never create; a terminal mark with no election refuses. Positive control: the exact wire lets a new operation create rc=1 FAIL PASS (23/23)
one secret's journal never blocks another the journal prefix separator . weakened to - (candidate discovery) resolved_journals_admit_a_new_create_and_unreadable_ones_refuse rc=1 FAIL PASS (23/23)
grant member is fleet-cloud-convergence@, never iam-converge plan_grant_set grants to iam-converge's member custody_cells_name_the_resolved_convergence_principal_never_iam_converge (planned through the real grant_member_resolution) rc=1 FAIL PASS (25/25)
census row agrees the deleted r2_mint_secret_access GcloudInteractive row restored the_r2_custody_cells_are_realized_by_the_federated_iam_route rc=1 FAIL PASS (25/25)

The journal and container decisions are pure folds over closed carriers and the target/ listing, so their claims are driven by supplied observations and journal names built by the same naming functions the effect writes. The real GetSecret, CreateSecret and journal files are exercised by the operator's iam_bootstrap_apply run.

🤖 Generated with Claude Code

briansrls and others added 25 commits September 28, 2026 21:44
…en secrets

Run 37211074479 (r2_workspace_object_read_mint) refused at the precondition GET: the fleet
convergence identity held no secretmanager.secrets.get on cloudflare-r2-workspace-read-token.
Adds viewer / versionAdder / accessor cells for cloudflare-r2-workspace-read-token and -write-token,
mirroring the bucket-admin cells, with operator converge entries. Witness 6/6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…are requires Network like main's

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ant set; bootstrap creates bare custody containers
…fold-residue gate refused 16 wildcard sites)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… gunbc.auth.approval_run_identity's; same fix quick-moth-82 verified 24/24)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal added 3 commits October 4, 2026 16:59
…absent, never retried), grant member is the convergence principal, census realizes R2 cells federated
…automatic create until observed present or operator-disposed (reuses #13291's election journal)
… the listing only discovers candidate operations
Base automatically changed from gcp-iam-bootstrap-land to main October 4, 2026 22:12
gunbc-ci-auto-heal added 4 commits October 4, 2026 22:15
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 3f575ba Oct 5, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the r2-custody-iam-reach branch October 5, 2026 04:08
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