Skip to content

A2 reader: snapshot take requires the custody export receipt - #13858

Merged
gunbai-bot[bot] merged 20 commits into
mainfrom
a2-snapshot-take-requires-export-receipt
Oct 14, 2026
Merged

gunbai-bot[bot] merged 20 commits into
mainfrom
a2-snapshot-take-requires-export-receipt

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Summary

  • dispatch_credential_snapshot_take requires the root-published export receipt (no fallback). Directory authority is established first (publisher-owned, no group/other write). The receipt is lstat'd as a regular file, then opened once; owner, mode and bytes come from that descriptor (extdeps.posix.sh_invocation posix_sh_receipt_object_command / posix_sh_receipt_object_body_program: wrapper exec < "$0", body stat -L of /dev/stdin then cat). Residual: after the directory is admitted, only the publisher can swap the path between lstat and open.
  • Version join admits declared.secret.project == fleet_secrets_project_id first (SnapshotReceiptSecretProjectMismatch), then resolve_returned_version against fleet_secrets_project_number unchanged: only SecretRefVersionResolvedExact admits.
  • Metadata remains a coproduct (SnapshotReceiptObservedNotRegular | SnapshotReceiptObservedRegular { … }). Claude guard yields the declaration. Custody leaf is data only.
  • Pairing: the claim runs the production export planner and the receipt-publication steps, then the real take. It creates the fixture custody file directly and supplies placement_settled: true. It is not a full remote-converge test.
  • Claude OAuth ref is pinned to version 1, same shape as Cursor. Last srv1 custody run (2026-10-10, run 38086667921 / job 114315569214) resolved projects/582015116396/secrets/claude-code-oauth-harness/versions/1. admit_enrollment_secret_version already refuses latest. IAM grant is per secret (fleet_secret_accessor_roster), not per version. The operator confirms the intended exact version. Custody's receipt must agree with it. If the observed version differs, landing remains blocked until the intended version is explicitly selected and the declaration and custody are brought into agreement.

Landing gates

Claude/srv1 receipt and exact-head CI are satisfied. Cursor/srv2 receipt remains a mandatory C1 activation precondition; until it exists, Cursor snapshot acquisition refuses typed with no fallback.

Cursor activation chain: operator IAM bootstrap → approved gcp-iam-converge → srv2 Cursor custody converge → export receipt readback → C1 live entitlement trip. A missing or mismatched receipt must keep C1 from starting the provider.

  • Claude / srv1 read back. Run 38488382866 (job 115610565363), dispatched from main c6b72a3ead: export=PUBLISHED path=/etc/gunbc-claude-code/oauth-token-harness.gunbc-export-receipt sm_version=projects/582015116396/secrets/claude-code-oauth-harness/versions/1 digest=01062ea8d5e193fa4b0ddf78c74b9a922dbe5cb113a9b2a5676c4dd1f7946028, with custody=HELD, content=identical-to-sm-version, dir=owner=root:briansrls mode=0750. The version agrees with the pinned 1.
  • Cursor / srv2. C1 precondition, not a merge gate (side chat ruling: LAND at c38fd21aa8d by squash onto current main).

Test plan

Prior receipts on earlier heads remain in the conversation (coproduct, version join, rebase onto std.live_tree).

Mutation table (this head, same remote dispatch)

Mutant Discriminating claim What the mutant observed
(a) Body stats "$0" instead of /dev/stdin a_replaced_receipt_does_not_authenticate_held_writable_bytes object run exits 0; join returns SnapshotReceiptJoined over the held bytes, so the RED fails because the take accepted
(b) Directory decision moved after the custody read a_group_writable_directory_refuses_before_the_custody_read refuses SnapshotCustodyUnreadable; RED fails
(c) Open-file kind check dropped a_character_device_on_held_stdin_is_not_a_regular_receipt /dev/null is not refused; RED fails
(d) lstat pre-check dropped a_fifo_at_the_receipt_path_refuses_without_waiting refuses SnapshotReceiptReadTimedOut after about 5s; RED fails, no hang
(e) Fleet-project admission dropped a_foreign_declared_project_refuses_a_fleet_number_receipt / a_declared_foreign_project_refuses_a_fleet_number_receipt inverse RED fails

Side chat: source APPROVE at c38fd21aa8d; then LAND by squash onto current main, with no branch update or CI rerun.

Composition at merge (main dfa83f4baf): independent. Since the merge base, three PR files also changed on main (extdeps.exec.command, v2.extdeps.languages.bash, gunbc.roadmap.roadmap_dispatch_actuator), in disjoint regions; no added line uses the symbols #13882 moved. #13803 (v2 parser) changes no PR file. Its next-line [ rule applies only after an operand, and the PR's two line-leading list literals follow join(. Its lambda-body sealing only widens what parses. git merge-tree is clean.

Head: c38fd21aa8d4b00ec81d35c8f13b8274800a47d5

@gunbai-bot

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review 79272 (dashboard-only REQUEST_CHANGES):

  • join_snapshot_receipt_fields now compares receipt.host to SnapshotCustodyDeclaration.host (same HostIdentity). Wet control a_receipt_for_another_host_refuses_the_host is green: https://app.buildbuddy.io/invocation/7ca39fef-8752-4e16-adee-fbafab2089d5
  • Receipt directory must be owned by the declared publisher (SnapshotReceiptDirectoryWrongOwner) before the group/other-writable check. A reader-owned 0755 directory no longer passes.
  • Residual unchanged: attempt.instance.host_identity is still NonEmptyStr and is not string-compared to HostIdentity.

Do not merge: landing still waits on srv1/srv2 receipt read-back.

@gunbai-bot

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

review 79272 (on eccf863) is addressed on the current head cf33201.

  1. Host: join_snapshot_receipt_fields now refuses SnapshotReceiptHostMismatch when receipt.host is not declared.host (same HostIdentity). Wet: a_receipt_for_another_host_refuses_the_host — https://app.buildbuddy.io/invocation/7ca39fef-8752-4e16-adee-fbafab2089d5
    Residual kept: attempt.instance.host_identity is NonEmptyStr and is still not compared to HostIdentity.

  2. Directory: observe_snapshot_receipt_directory now stats the directory owner and refuses SnapshotReceiptDirectoryWrongOwner unless it is the declared publisher, then still refuses group/other write as SnapshotReceiptDirectoryReaderWritable. A reader-owned 0755 directory cannot pass.

— sent from clever-hawk-741

@gunbai-bot

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

review 79277 asked for the SecretRef join to go through resolve_returned_version and for a foreign-project wet RED. That is on head c9a4d05dc62.

snapshot_receipt_version_agrees (id-segment split) is gone. snapshot_receipt_version_join calls resolve_returned_version(returned: receipt.sm_version, ref: declared.secret, project_number: fleet_secrets_project_number):

  • SecretRefVersionResolvedExact admits
  • SecretRefVersionResolvedAlias is SnapshotReceiptSecretRefNotPinned (declared ref is not pinned)
  • SecretRefVersionResolutionRefused is SnapshotReceiptVersionMismatch

The foreign-project wet RED is a_receipt_naming_the_secret_in_another_project_refuses_the_version: same secret id and version 1 under projects/999999999999/... must be SnapshotReceiptVersionMismatch. Executed wet on https://app.buildbuddy.io/invocation/3bab4aaa-31f9-4e66-ba72-8b2f2bf9803e

— sent from clever-hawk-741

@gunbai-bot

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

review 79347: both findings are on head db42cb352cf.

  1. SnapshotReceiptObservedMetadata is now a coproduct: SnapshotReceiptObservedNotRegular | SnapshotReceiptObservedRegular { file_owner, file_mode, dir_owner, dir_mode }. The kind tag, snapshot_receipt_zero_mode, and snapshot_receipt_not_regular_observation (empty owners) are gone. Symlink and other non-regular stat arms return SnapshotReceiptNotRegularFile directly, so a later edit cannot match “regular” fields on a not-regular observation.

  2. SnapshotReceiptDirectoryReaderWritable is SnapshotReceiptDirectoryGroupOrOtherWritable. The refusal text is “writable by group or other”, matching snapshot_mode_group_or_other_writable_bits (group or other write), not “writable by the declared reader”.

Hermetic metadata claims: https://app.buildbuddy.io/invocation/62c8a69e-3ef8-4ea2-ab24-f2a0572ac060 (4 PASS).

Do not merge: landing still waits on srv1/srv2 receipt read-back.

— sent from clever-hawk-741

gunbc-ci-auto-heal and others added 7 commits October 13, 2026 12:45
The take joins the same buffer it copied to a root-published receipt (no receipt-free fallback) and records the generation on the sealed snapshot, so a launch can name which Secret Manager version it ran.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t directory be publisher-owned.

declared.host was unused; a receipt for another host with matching path and principals would have sealed. Directory mode bits alone also let a reader-owned 0755 directory pass. The attempt's NonEmptyStr host_identity stays a named residual.

Co-authored-by: Cursor <cursoragent@cursor.com>
…a publisher-owned directory.

Take uses resolve_returned_version (exact pin only), metadata is a pure decision over observed owners and modes, Claude custody is the declaration not a path fork, and the converge data leaf no longer imports SnapshotExecutor.

Co-authored-by: Cursor <cursoragent@cursor.com>
admit_enrollment_secret_version already refuses latest; the 2026-10-10 srv1 custody run resolved versions/1. The pin follows the srv1 export receipt if that receipt names another version.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t carry fabricated owners.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot
gunbai-bot Bot force-pushed the a2-snapshot-take-requires-export-receipt branch from 596b45c to 93da68d Compare October 13, 2026 12:47
gunbc-ci-auto-heal and others added 4 commits October 13, 2026 15:11
…ject before version join.

A path-based restat after the read let a replacement authenticate different bytes; directory authority, a regular-file lstat, and posix_sh_receipt_object_command keep owner, mode and content on one descriptor.

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

snapshot_path_dirname was a third, quadratic copy of dirname; the take now calls std.filesystem.path_parent_directory. exec < "$0" is a bash_build_with_redir_from_file row rather than a concatenated string.

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

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review 79442 (dashboard artifact /api/reviews/79442/artifacts/stdout.log).

  1. Path parent: deleted snapshot_path_dirname (quadratic concat walk). The take uses std.filesystem.path_parent_directory (linear reverse/list_tail). gunbc.live_deploy.emit path_parent_dir now calls that same function.

  2. Stdin redirect: posix_sh_receipt_object_program is emitted through the bash grammar. Added bash_build_with_redir_from_file / ^bash_production_fold_redir_from_file (same shape as the existing to-file redirect). Golden: bash_fold_with_redir_from_file_var_holds. The replacement fixture still uses the body program only; production opens via exec redirected from $0.

— sent from clever-hawk-741

gunbc-ci-auto-heal and others added 4 commits October 13, 2026 15:53
…ion arm has a discriminating RED.

The replacement RED now runs the body with \$0 as the receipt path, directory authority is one pure function the observer calls before any content read, and the join returns the decoded receipt so the take does not parse twice.

Co-authored-by: Cursor <cursoragent@cursor.com>
Native parse_g0 left tokens on `wrote && { let run ... }`, which refused the live-root population and failed witnesses.

Co-authored-by: Cursor <cursoragent@cursor.com>
…and the RED body.

review 79454 was right that the stat-and-cat statement was written twice; the body program stays because the replacement and character-device REDs run it without the wrapper.

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

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

review 79454

The unused-helper claim was stale on this head: posix_sh_receipt_object_body_program is called from a_replaced_receipt_does_not_authenticate_held_writable_bytes and a_character_device_on_held_stdin_is_not_a_regular_receipt (the wrapper/body split). Those REDs have to emit the body without exec < "$0".

The duplication of the stat && cat statement was real. posix_sh_receipt_object_body_stmt is now the single Node; both emit functions use it (6e9f9bf3360).

…ks keep a named cutover.

emit call sites now use std.filesystem path_parent_directory. Belt and frontend walks still return empty for a relative basename, which is a different remainder, so they stay until those consumers admit the root.

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

gunbai-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

review 79460

Body program. On this head posix_sh_receipt_object_body_program is called from the replacement RED and the character-device RED (dispatch_credential_snapshot_wet_witness). Production cannot call that string emit as a statement; both emit functions now share posix_sh_receipt_object_body_stmt (6e9f9bf3360). A grep of only sh_invocation.dag will still miss the consumers.

path_parent_dir. Deleted. The three emit sites and the live_deploy emit witness call std.filesystem path_parent_directory (440b424b18f).

/foo remainder / not "". Intended. path_parent_directory_drops_the_last_segment pins path_parent_directory("/receipt") == "/". Belt and frontend walks still return "" for a relative basename (README.md); that is a different remainder, and their cutover trigger is those consumers admitting the root — recorded on path_parent_directory, not as a silent alias.

gunbc-ci-auto-heal and others added 4 commits October 13, 2026 17:36
…f this PR.

The character-device RED already uses if/else so native parse_g0 does not leave tokens. SnapshotReceiptReadTimedOut is the timeout cause; live_deploy emit is restored to main.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t receipt at lstat.

Directory-first take never reaches SnapshotCustodyUnreadable without a planted receipt, and a missing receipt is unobservable at lstat rather than unreadable at open.

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

A basename with no slash is not the filesystem root, and the helper comment must not roster cutovers that are a different question.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot
gunbai-bot Bot merged commit e82db88 into main Oct 14, 2026
3 of 4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the a2-snapshot-take-requires-export-receipt branch October 14, 2026 01:26
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.

0 participants