Skip to content

feat(migration): plan entry v2 issuer (backfills, base-read capability, commit identity) - #117

Draft
AgentRelayBot wants to merge 2 commits into
feat/migration-grant-pair-bindingfrom
feat/migration-plan-v2-prototype
Draft

AgentRelayBot wants to merge 2 commits into
feat/migration-grant-pair-bindingfrom
feat/migration-plan-v2-prototype

Conversation

@AgentRelayBot

@AgentRelayBot AgentRelayBot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Plan entry v2 issuer (backfill epochs) for the RelayHistory to 96Bulls migration. Stacked on #111 (feat/migration-grant-pair-binding @ 2adf414a). Draft; do not merge.

Spec of record: 96BullSessions main @ f67f741, which is #86 merged. That commit holds docs/contracts/relayhistory-export-plan-entry-v2-issuer.md (blob b4bde2e6) and .vectors.json (blob 59a8bde9). Contract review PASSED at d4e0e040, and its blobs are identical. The vectors are vendored byte for byte at packages/server/src/__tests__/fixtures/relayhistory-export-plan-entry-v2-issuer.vectors.json.

Behaviour

  • Legacy conditional (§1). An absent backfills or backfills: [] takes the v1 path byte for byte: the same stored plan and digest, v1's rule of 1 to 1000 manifests, and unchanged audit rows. A non-empty list stores a v: 2 plan whose digest covers {v: 2, grant_id, exported_at, manifests, backfills}. In that case manifests may be [], but the plan needs at least one entry in total (ruling 2). A backfills that is null or not an array is invalid_plan.

  • v2 entries (§2). The input has exactly the spec's keys. RelayAuth derives grant_id, backfill_sha256 and raw_bytes, and refuses a client value for any of them. records is mandatory, offsets run on from 0, and start_offset + bytes ≤ MAX is checked exactly. A key may appear only once across both lists. A v2 entry in the wrong org gets a generic invalid_plan, and the precise backfill_org_mismatch appears only in the internal audit detail. v1 keeps plan_org_mismatch (ruling 3).

  • Two token classes (§3).

    commit {key, base} base-read {key, kind: "backfill_base_read"}
    sub agent_relayhistory_migration agent_relayhistory_migration_base_read
    tokenClass migration_assertion migration_base_read
    scopes ["rth:sync"] ["rth:migration_base_read"]
    aud [<origin>] [<origin>/v1/pipeline/manifests/base]
    TTL ≤ 60 s ≤ 30 s
    identity meta.upload (§5) meta.read (§4); meta.upload absent
    jti tok_ mbr_

    Both forms look the key up only in backfills. The pair, live-identity and revocation checks are v1's, and tenancy comes only from the stored entry. Sending both kind and base, or any other upload key, gets 400 invalid_upload_selector. Base-read issuance is default-off: RELAYAUTH_ENABLE_MIGRATION_BASE_READ (chart migrationAssertions.baseReadEnabled) must be exactly true, or the mint gets 403 migration_base_read_disabled (§9).

  • base bounds (§5). Exactly 7 keys. revision is 1 to MAX−1, chunk_count is 0 to MAX−4096, and epoch is 0 to 4294967294. version, owner and writer are 1 to 512 characters, and owner may not start with migration:. content_sha256 is lowercase hex.

  • Audit (rulings 1 and 4). v1 mint and refusal rows are exactly fix(migration): bind grants to canonical upload pairs #111's. v2 rows carry upload_kind, uploadKeyHash, entry_sha256 and, for a commit, base_revision. They omit the raw nonce and are correlated by tokenId, which is the jti. A v2 refusal row carries upload_kind and the key hash. No row holds a raw key, token or base content.

  • SDK verifier and types. meta.upload must be the v1 or v2 commit shape, and meta.read must be the base-read shape. A token carrying both is refused.

Ruling 5 (#112)

#112's request fingerprint must strip grant_id from backfill entries. That change is made on #112's own branch as a stacked follow-up; see #112.

Tests (new or changed; all pass)

  • migration-plan-v2.test.ts: 67, including subtests.
    • Reproduces legacy.absent and legacy.empty (stored JSON and digest a6682d4d…).
    • Reproduces v2_plan (f29e6cc4…), backfill_entry_stored, backfill_entry_sha256, base_read_meta_read (including canonical JSON) and commit_meta_upload.
    • Covers the §2 and §10 negatives, including null or string backfills, no entries at all, the wrong-org audit reason, and MAX and MAX+1 bounds.
    • Covers 9 stored-plan tampers, 17 base bound cases, and a backfill-only plan.
  • migration-assertion.test.ts, v2 section: 6 tests, 22 including subtests.
    • Grants: v1 stored byte for byte, with no v2 audit fields.
    • Mint claims and audit for both classes, plus the v1 audit key set pinned exactly.
    • A 12-case selector, list, pair and tenant refusal matrix, with every refusal audited.
    • The verifier audience matrix in both directions, and RelayAuth refusing the capability as a refresh token and as a bearer token.
    • The base-read flag (default-off, enabled only by exactly true, never overriding the main switch).
    • A backfill-only grant, and the wrong-org response kept generic with the reason only in the audit.
  • SDK verify.test.ts: 1 test that accepts the commit shape in meta.upload and the base-read shape in meta.read, and refuses 19 confused or malformed shapes.

Validation

Local, all uncached:

Check Result
turbo typecheck --force 11/11
turbo build --force 9/9
turbo test --force (16/16, 0 cached) server 816/816, sdk 171/171, ai 70/70, core 19/19, migrate 18/18, relayauth 12/12, types 1/1
Root tests 8/8
Server test:e2e 40 pass, 0 fail, 93 skipped (live environment)
helm template renders RELAYAUTH_ENABLE_MIGRATION_BASE_READ only when baseReadEnabled=true

Prettier (npx --yes prettier@3.6.2 --no-config --check <the 16 added or modified files>) exits 1. The files it warns about are exactly the ones that already fail the same check at 2adf414a. deployment.yaml is a Go template, so it can't be parsed as YAML. The new and newly touched files, the fixture, values.yaml and lib/migration-grants.ts, pass. I did not reformat existing files.

Hosted CI: ci.yml triggers only for PRs targeting main and has no workflow_dispatch. CI therefore runs on the temporary CI-only draft #118, from this exact head. #118 will be closed once CI reports.

Nothing was merged, deployed or touched live, and no keys or credentials were created or used.

Independent SOC2 re-review pending.

🤖 Generated with Claude Code


Agent Relay sessions

  • claude session 719b5435 · opened via gh pr create · last active 2026-10-31

…, commit identity)

PROTOTYPE ONLY. Built against 96BullSessions #86 @ e74be4c0, which is on
contract-review HOLD pending amendments. Not final; do not merge.

- Plan: optional `backfills` of v2 inputs {v:2, kind:"backfill", key,
  org_id, workspace_id, source_user_id, session, format:"ndjson",
  chunks:[{start_offset, bytes, sha256, records}]}. The issuer adds
  grant_id and derives backfill_sha256 (v1 hash chain, epoch 0) and
  raw_bytes; client-supplied derived fields are refused. records is
  mandatory, offsets contiguous from 0, keys unique across both lists,
  pairs and limits span both lists.
- Digest: absent and empty `backfills` give the byte-identical v1 plan
  and digest. With a backfill: sha256(canonical({v:2, grant_id,
  exported_at, manifests, backfills})). verifyStoredMigrationGrantPlan
  recomputes both shapes; a v2 row with no backfills or a v1 row
  carrying backfills is refused. Old binaries refuse v2 rows (fail
  closed).
- Mint: upload is exactly {key} (v1, unchanged), {key, kind:
  "backfill_base_read"} or {key, base}; each form looks up only its own
  list; tenancy and digests come only from the stored entry.
- Base-read capability follows the review direction: its own subject,
  audience (<grant origin>/v1/pipeline/manifests/base), scope
  (rth:migration-base:read), tokenClass, TTL (<=30 s) and jti namespace
  (mbr_), audited as resource migration-base-read. Single-use consume
  is the ingest's job.
- Base: exact 7 keys and safe-integer bounds (revision 1..2^53-2,
  chunk_count <= 2^53-1-4096, epoch <= 2^32-2), strings 1..512, owner
  not migration:*, content_sha256 lowercase hex.
- Audit: upload_kind, entry_sha256 and base_revision only.
- SDK verifier and @relayauth/types accept exactly the three shapes.

Tests: vendored #79 @ c91f4ac0 vectors reproduced (entry, backfill and
plan digests, commit identity, work key); v1 digest a6682d4d unchanged
for absent and []; negatives from #86 section 8; stored-plan tamper;
base bounds; mint form/list/pair/selector matrix with audited refusals;
verifier audience matrix both ways.

Validation (local): turbo typecheck 11/11, build 9/9, npm test 16/16
(server 805/805, sdk 171/171, ai 70, core 19, migrate 18, relayauth 12,
types 1, root 8); server e2e 40 pass, 93 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Aligns the #117 prototype with 96BullSessions #86, the spec of record
(main @ f67f741; reviewed and PASSED at d4e0e040, same blobs):
docs/contracts/relayhistory-export-plan-entry-v2-issuer.md and its
vectors (blob 59a8bde).

- Base-read capability: sub agent_relayhistory_migration_base_read,
  tokenClass migration_base_read, scope rth:migration_base_read, aud
  <origin>/v1/pipeline/manifests/base, TTL <= 30 s, jti mbr_, identity
  in meta.read with meta.upload absent. Issuance is default-off behind
  RELAYAUTH_ENABLE_MIGRATION_BASE_READ (403 migration_base_read_disabled;
  chart migrationAssertions.baseReadEnabled).
- Legacy conditional (section 1): absent or [] backfills is the v1 path
  byte for byte, including v1's 1..1000 manifests rule; a non-empty
  list is v2 and may have manifests: [] (ruling 2: >= 1 combined entry);
  null or non-array backfills is invalid_plan.
- Exact bounds (section 2): start_offset + bytes <= MAX checked without
  a float sum; records mandatory 0..MAX.
- Rulings: v1 mint and refusal audit rows unchanged (no upload_kind or
  entry_sha256 on v1); v2 rows omit the raw nonce and correlate by
  tokenId; v2 wrong-org is a generic invalid_plan externally with
  detail backfill_org_mismatch only in the internal audit; v1 keeps
  plan_org_mismatch.
- SDK verifier: meta.upload is v1 or v2 commit only; meta.read is the
  base-read only; both together are refused.
- Tests use #86's issuer vectors (legacy.absent, legacy.empty, v2_plan,
  backfill_entry_*, base_read_meta_read, commit_meta_upload) in place of
  the #79 c91f4ac0 copy.

Review: adversarial (token-class confusion both ways at the verifier,
selector/list/pair/tenant override refusals, bounds at MAX and MAX + 1,
flag exactness, no raw key/nonce/base content in audit) and contextual
(v1 vectors and #111 tests unchanged, docs, chart, env wiring). Fixed a
timestamp-ordering flake in a new test found by repeated runs.

Validation (local; hosted CI only triggers for base main):
turbo typecheck --force 11/11, build --force 9/9, test --force 16/16
(0 cached): server 816/816, sdk 171/171, ai 70, core 19, migrate 18,
relayauth 12, types 1; root tests 8/8; server e2e 40 pass, 93 skipped.
prettier 3.6.2 --no-config --check: new files pass; the warned files
are exactly those already non-conforming at 2adf414.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AgentRelayBot AgentRelayBot changed the title PROTOTYPE: migration plan entry v2 issuer (backfills, base-read, commit identity) feat(migration): plan entry v2 issuer (backfills, base-read capability, commit identity) Oct 31, 2026
AgentRelayBot pushed a commit that referenced this pull request Oct 31, 2026
…gerprint

Ruling 5 on 96BullSessions #86 (spec of record main @ f67f741): the
idempotency request fingerprint must not depend on any generated
grant_id, including those in plan entry v2 `backfills` (relayauth #117).
migrationGrantFingerprintPlan now strips the plan's grant_id and each
entry's in both `manifests` and `backfills`. A v1 plan has no
`backfills` key, so v1 fingerprints are byte-identical to before (pinned
by a test against the previous construction).

Validation (local): turbo typecheck --force 11/11; turbo test --force
16/16 (0 cached): server 754/754, sdk 170/170, ai 70, core 19, migrate
18, relayauth 12, types 1; root tests 8/8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant