From 6c79e1941c7305e567fe50e842347fe7a6bd2a9d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:30:27 +0800 Subject: [PATCH 1/2] fix: pin P02 capture to the reviewed control revision Co-Authored-By: Codex --- .../console-p02-baseline-capture.yml | 51 +++++++++++++++---- scripts/console_p02_baseline_capture.mjs | 7 ++- scripts/console_p02_baseline_hosted.mjs | 15 ++++-- scripts/console_p02_candidate_build.sh | 9 +++- tests/console_p02_baseline_capture.test.mjs | 8 ++- tests/console_p02_baseline_hosted.test.mjs | 6 +-- 6 files changed, 72 insertions(+), 24 deletions(-) diff --git a/.github/workflows/console-p02-baseline-capture.yml b/.github/workflows/console-p02-baseline-capture.yml index a7f34a33..a8e40f42 100644 --- a/.github/workflows/console-p02-baseline-capture.yml +++ b/.github/workflows/console-p02-baseline-capture.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: inputs: control_sha: - description: Reviewed control commit on main + description: Reviewed control commit in main history type: string required: true permissions: @@ -19,6 +19,7 @@ jobs: timeout-minutes: 30 environment: runtime-strategy-switch env: + P02_CONTROL_SHA: ${{ inputs.control_sha }} DIGEST_CANDIDATES_GCS_URI: ${{ vars.DIGEST_CANDIDATES_GCS_URI }} DIGEST_CANDIDATES_READER_WIF_PROVIDER: ${{ vars.DIGEST_CANDIDATES_READER_WIF_PROVIDER }} DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT: ${{ vars.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT }} @@ -29,27 +30,55 @@ jobs: with: persist-credentials: false fetch-depth: 0 + - name: Check out the reviewed control revision + env: + QRS_P02_STORAGE_CAPABILITY_RECEIPT: ${{ secrets.QRS_P02_STORAGE_CAPABILITY_RECEIPT }} + run: | + set -euo pipefail + [[ "$P02_CONTROL_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1 + [[ "$(git rev-parse HEAD)" == "$GITHUB_SHA" ]] || exit 1 + git merge-base --is-ancestor "$P02_CONTROL_SHA" "$GITHUB_SHA" || exit 1 + path=.github/workflows/console-p02-baseline-capture.yml + approved_workflow="$(git rev-parse --verify "$P02_CONTROL_SHA:$path")" || exit 1 + dispatched_workflow="$(git rev-parse --verify "$GITHUB_SHA:$path")" || exit 1 + [[ "$approved_workflow" == "$dispatched_workflow" ]] || exit 1 + tree="$(git rev-parse --verify "${P02_CONTROL_SHA}^{tree}")" || exit 1 + P02_CONTROL_TREE="$tree" python3 - <<'PY' || exit 1 + import json + import os + + try: + receipt = json.loads(os.environ["QRS_P02_STORAGE_CAPABILITY_RECEIPT"]) + matches = ( + receipt["schema"] == "qrs-p02-storage-capability-v1" + and receipt["control_sha"] == os.environ["P02_CONTROL_SHA"] + and receipt["control_tree"] == os.environ["P02_CONTROL_TREE"] + ) + except (KeyError, TypeError, ValueError): + matches = False + if not matches: + raise SystemExit("R2 reviewed control binding does not match") + PY + git checkout --detach "$P02_CONTROL_SHA" || exit 1 + [[ "$(git rev-parse HEAD)" == "$P02_CONTROL_SHA" ]] || exit 1 + printf 'P02_CONTROL_TREE=%s\n' "$tree" >> "$GITHUB_ENV" - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: '22.23.4' + - name: Verify protected exact-object capability + env: + QRS_P02_STORAGE_CAPABILITY_RECEIPT: ${{ secrets.QRS_P02_STORAGE_CAPABILITY_RECEIPT }} + run: node scripts/console_p02_baseline_hosted.mjs verify-capability - name: Verify control revision and fixed offline candidate env: - CONTROL_SHA: ${{ inputs.control_sha }} WRANGLER_SEND_METRICS: 'false' CLOUDFLARE_CF_FETCH_ENABLED: 'false' run: | set -euo pipefail - [[ "$CONTROL_SHA" == "$GITHUB_SHA" ]] - [[ "$(git rev-parse HEAD)" == "$GITHUB_SHA" ]] - [[ "$(git ls-remote origin refs/heads/main | cut -f1)" == "$GITHUB_SHA" ]] npm ci --ignore-scripts --prefix web/strategy-switch-console/frontend node --test tests/console_p02_baseline_capture.test.mjs tests/console_p02_baseline_hosted.test.mjs npx --yes wrangler@4.106.0 --version bash scripts/console_p02_candidate_build.sh "$RUNNER_TEMP/qrs-p02-baseline-candidate" - - name: Verify protected exact-object capability - env: - QRS_P02_STORAGE_CAPABILITY_RECEIPT: ${{ secrets.QRS_P02_STORAGE_CAPABILITY_RECEIPT }} - run: node scripts/console_p02_baseline_hosted.mjs verify-capability - name: Authenticate existing QRS WIF identity id: auth uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 @@ -66,5 +95,7 @@ jobs: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || vars.CLOUDFLARE_ACCOUNT_ID }} run: | set -euo pipefail - [[ "$(git ls-remote origin refs/heads/main | cut -f1)" == "$GITHUB_SHA" ]] + [[ "$(git rev-parse HEAD)" == "$P02_CONTROL_SHA" ]] || exit 1 + [[ "$(git rev-parse 'HEAD^{tree}')" == "$P02_CONTROL_TREE" ]] || exit 1 + git diff --quiet HEAD -- || exit 1 node scripts/console_p02_baseline_hosted.mjs capture "$RUNNER_TEMP/qrs-p02-baseline-candidate/worker.js" diff --git a/scripts/console_p02_baseline_capture.mjs b/scripts/console_p02_baseline_capture.mjs index f242736a..cf7b123c 100644 --- a/scripts/console_p02_baseline_capture.mjs +++ b/scripts/console_p02_baseline_capture.mjs @@ -86,10 +86,11 @@ export function rebuildWithAssets(candidate,active,ts){ return {declarations:observed.declarations,sha256:digest(bytes),bytes}; } // Storage reservation must complete before any Worker reads. A failed or unknown write is never retried. -export async function capture({api,store,candidate,controlSha,alternate,expectedActive,now=()=>new Date(),ts=require('typescript'),reference=REFERENCE,alternateReference=HISTORICAL}){ +export async function capture({api,store,candidate,controlSha,controlTree,alternate,expectedActive,now=()=>new Date(),ts=require('typescript'),reference=REFERENCE,alternateReference=HISTORICAL}){ let stage='candidate',writeAttempted=false; try{ if(!/^[a-f0-9]{40}$/.test(controlSha??'')||!Buffer.isBuffer(candidate)||digest(candidate)!==reference)throw Error('candidate'); + if(controlTree!==undefined&&!/^[a-f0-9]{40}$/.test(controlTree))throw Error('candidate'); if(!compareAssetDeclarations(candidate,candidate,ts).ok)throw Error('candidate'); stage='storage_preflight'; await store.reserve(); stage='active_before'; const before=identity(await api.deployments()); @@ -118,7 +119,9 @@ export async function capture({api,store,candidate,controlSha,alternate,expected rebuilt=rebuildWithAssets(candidate,assets,ts); } const capturedAt=now().toISOString(); - const record={schema:'qrs-worker-p02-baseline-v1',control_sha:controlSha,captured_at:capturedAt,...before,active_module_sha256:activeHash, + const record={schema:'qrs-worker-p02-baseline-v1',control_sha:controlSha, + ...(controlTree===undefined?{}:{control_tree:controlTree}), + captured_at:capturedAt,...before,active_module_sha256:activeHash, configuration_sha256:digest(config(r)),migration_tag_sha256:digest(r.script_runtime.migration_tag), reference_module_sha256:reference,asset_comparison:compared.equal,...(rebuilt?{historical_comparison:historical,asset_declarations:rebuilt.declarations,rebuilt_module_sha256:rebuilt.sha256,release_state:'hold_review_required'}:{release_state:'hold'})}; stage='save'; writeAttempted=true; const bytes=Buffer.from(JSON.stringify(record)); diff --git a/scripts/console_p02_baseline_hosted.mjs b/scripts/console_p02_baseline_hosted.mjs index 0e1789ca..8e43e4a7 100644 --- a/scripts/console_p02_baseline_hosted.mjs +++ b/scripts/console_p02_baseline_hosted.mjs @@ -1,19 +1,22 @@ // Hosted P02 capture: fixed object and routes; provider responses are never logged. import {readFile} from 'node:fs/promises'; import {pathToFileURL} from 'node:url'; -import {capture,REFERENCE,R2_ACTIVE_MODULE} from './console_p02_baseline_capture.mjs'; +import {capture} from './console_p02_baseline_capture.mjs'; import {createFixedApi} from './console_946_backend_release.mjs'; import {createHash} from 'node:crypto'; export const KEY='evidence/qrs-worker-p02/p02-20261109-r2/baseline.json'; +const REVIEWED_MODULE='8e38776bdfb0a665ae87b7bd93146881d8813010461ea3dc9b77e23275e720ea'; export function verifyCapability(env,now=Date.now()){ const uri=/^gs:\/\/([a-z0-9][a-z0-9._-]{1,220}[a-z0-9])(?:\/[^?#]*)?$/.exec(env.DIGEST_CANDIDATES_GCS_URI??''); const provider=env.DIGEST_CANDIDATES_READER_WIF_PROVIDER||env.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER; const account=env.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT||env.DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT; let r;try{r=JSON.parse(env.QRS_P02_STORAGE_CAPABILITY_RECEIPT||'')}catch{throw Error('capability');} const issued=Date.parse(r.issued_at),expires=Date.parse(r.expires_at); - if(!uri||!provider||!account||!/^[a-f0-9]{40}$/.test(env.GITHUB_SHA??'')||r.schema!=='qrs-p02-storage-capability-v1'|| - r.bucket!==uri[1]||r.object!==KEY||r.wif_provider!==provider||r.service_account!==account||r.control_sha!==env.GITHUB_SHA|| + if(!uri||!provider||!account||!/^[a-f0-9]{40}$/.test(env.P02_CONTROL_SHA??'')|| + !/^[a-f0-9]{40}$/.test(env.P02_CONTROL_TREE??'')||r.schema!=='qrs-p02-storage-capability-v1'|| + r.bucket!==uri[1]||r.object!==KEY||r.wif_provider!==provider||r.service_account!==account|| + r.control_sha!==env.P02_CONTROL_SHA||r.control_tree!==env.P02_CONTROL_TREE|| typeof r.owner_project!=='string'||!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(r.owner_project)|| r.policy_readback_reviewed!==true||r.create_permitted!==true||r.get_permitted!==true||r.target_absent!==true|| !Number.isFinite(issued)||!Number.isFinite(expires)||issued>now||expires<=now||expires-issued>24*60*60*1000) @@ -53,10 +56,12 @@ async function main(){ if(action==='verify-capability'){verifyCapability(process.env);console.log('{"ok":true,"capability_verified":true}');return;} if(action!=='capture'||process.argv.length!==4)throw Error('input'); const candidate=await readFile(process.argv[3]); - if(createHash('sha256').update(candidate).digest('hex')!==REFERENCE)throw Error('candidate'); + if(createHash('sha256').update(candidate).digest('hex')!==REVIEWED_MODULE)throw Error('candidate'); const {bucket}=verifyCapability(process.env); const store=storage({bucket,token:process.env.GOOGLE_ACCESS_TOKEN}); - const result=await capture({api:readOnlyWorkerApi(createFixedApi()),store,candidate,expectedActive:R2_ACTIVE_MODULE,controlSha:process.env.GITHUB_SHA}); + const result=await capture({api:readOnlyWorkerApi(createFixedApi()),store,candidate, + reference:REVIEWED_MODULE,expectedActive:REVIEWED_MODULE, + controlSha:process.env.P02_CONTROL_SHA,controlTree:process.env.P02_CONTROL_TREE}); console.log(JSON.stringify(result));if(!result.ok)process.exitCode=1; } if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href){ diff --git a/scripts/console_p02_candidate_build.sh b/scripts/console_p02_candidate_build.sh index f3eaadf2..f72efb7c 100644 --- a/scripts/console_p02_candidate_build.sh +++ b/scripts/console_p02_candidate_build.sh @@ -4,18 +4,23 @@ umask 077 readonly source=cabe5a1534e0c992713b084030af313eada758ca readonly tree=42b404d5755b658deff73100b495ecf26fa3a6b1 readonly blob=5287f259185ed44f3784aa6374cb00412b3c6238 +readonly assets_source=dd63b3c8b73b588ce411316d904efd26f2c4f382 +readonly assets_tree=c08d5542b032abaa008e882dd97ed2c38b3ad236 +readonly assets_blob=a7237657bb6e48c57e947875f629d28bafb5ea27 readonly asset=web/strategy-switch-console/v2_asset_map.js -readonly expected=912ad8b3efb9080b96112750146bdf050b6eefed6fae822b57dfa57e99feb57a +readonly expected=8e38776bdfb0a665ae87b7bd93146881d8813010461ea3dc9b77e23275e720ea output="${1:?output directory required}" [[ "$(git rev-parse "${source}^{tree}")" == "$tree" ]] [[ "$(git rev-parse "$source:$asset")" == "$blob" ]] -[[ "$(git rev-parse "084ba4281cfff3304cf226ce511a2fc0a6b67b08:$asset")" == "$blob" ]] +[[ "$(git rev-parse "${assets_source}^{tree}")" == "$assets_tree" ]] +[[ "$(git rev-parse "$assets_source:$asset")" == "$assets_blob" ]] [[ ! -e "$output" ]] tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT mkdir -p "$output" output="$(cd "$output" && pwd)" git archive "$source" | tar -x -C "$tmp" +git show "$assets_source:$asset" > "$tmp/$asset" cp "$tmp/web/strategy-switch-console/wrangler.toml.example" "$tmp/web/strategy-switch-console/wrangler.toml" ( cd "$tmp/web/strategy-switch-console" diff --git a/tests/console_p02_baseline_capture.test.mjs b/tests/console_p02_baseline_capture.test.mjs index a12b0b8f..2337d3d9 100644 --- a/tests/console_p02_baseline_capture.test.mjs +++ b/tests/console_p02_baseline_capture.test.mjs @@ -13,7 +13,7 @@ function fixture(options={}){ const store={async reserve(){calls.push('reserve');if(options.reserveFail)throw Error('private');},async create(b){calls.push('create');saved=b;if(options.createFail)throw Error('private');return '123';},async read(g){calls.push('read');assert.equal(g,'123');return saved;}}; return {api,store,calls,get record(){return JSON.parse(saved)}}; } -async function run(opt={}){const f=fixture(opt);return {f,result:await capture({api:f.api,store:f.store,candidate,reference,alternate:opt.alternate,alternateReference:opt.alternateReference,expectedActive:opt.expectedActive,controlSha:"a".repeat(40),now:()=>new Date("2026-11-09T00:00:00.000Z")})};} +async function run(opt={}){const f=fixture(opt);return {f,result:await capture({api:f.api,store:f.store,candidate,reference,alternate:opt.alternate,alternateReference:opt.alternateReference,expectedActive:opt.expectedActive,controlSha:"a".repeat(40),controlTree:opt.controlTree,now:()=>new Date("2026-11-09T00:00:00.000Z")})};} test('match writes one record after four fixed reads and verifies exact readback',async()=>{const {f,result}=await run();assert.equal(result.ok,true);assert.deepEqual(f.calls,['reserve','deployment','module','config','deployment','create','read']);assert.equal(f.record.asset_declarations,undefined);assert.equal(f.record.control_sha,'a'.repeat(40));assert.equal(f.record.captured_at,'2026-11-09T00:00:00.000Z');assert.equal(JSON.stringify(f.record).includes('private'),false);}); test('mismatch retains only statically literal declarations',async()=>{const active=Buffer.from(candidate.toString().replace('"html"','"other"'));const {f,result}=await run({active});assert.equal(result.ok,true);assert.equal(result.asset_declarations_equal,false);assert.match(f.record.asset_declarations.V2_PAGE_HTML,/other/);assert.match(f.record.rebuilt_module_sha256,/^[a-f0-9]{64}$/);assert.equal(f.record.release_state,'hold_review_required');}); test('unsupported declaration, drift, and storage failures stop without retries',async()=>{ @@ -25,7 +25,11 @@ test('bad candidate causes no storage or Worker reads',async()=>{const f=fixture test('rebind records only the pinned module with equal asset declarations',async()=>{ const active=Buffer.concat([candidate,Buffer.from('\nexport {};')]); const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); - const matched=await run({active,expectedActive:hash(active)}); + const controlTree='b'.repeat(40); + const matched=await run({active,expectedActive:hash(active),controlTree}); + assert.equal(matched.f.record.control_tree,controlTree); + const invalidTree=await run({active,expectedActive:hash(active),controlTree:'short'}); + assert.equal(invalidTree.result.ok,false);assert.deepEqual(invalidTree.f.calls,[]); assert.equal(matched.result.ok,true);assert.equal(matched.f.record.release_state,'hold'); assert.equal(matched.f.record.active_module_sha256,hash(active));assert.equal(matched.f.record.asset_declarations,undefined); assert.deepEqual(matched.f.calls,['reserve','deployment','module','config','deployment','create','read']); diff --git a/tests/console_p02_baseline_hosted.test.mjs b/tests/console_p02_baseline_hosted.test.mjs index 7ec4bf0c..44720221 100644 --- a/tests/console_p02_baseline_hosted.test.mjs +++ b/tests/console_p02_baseline_hosted.test.mjs @@ -2,11 +2,11 @@ import {test} from 'node:test'; import assert from 'node:assert/strict'; import {verifyCapability,storage,KEY,readOnlyWorkerApi} from '../scripts/console_p02_baseline_hosted.mjs'; const now=Date.parse('2026-11-09T00:00:00Z'); -const receipt={schema:'qrs-p02-storage-capability-v1',bucket:'synthetic-bucket',object:KEY,wif_provider:'synthetic-provider',service_account:'synthetic-account',control_sha:'a'.repeat(40),owner_project:'synthetic-owner',policy_readback_reviewed:true,create_permitted:true,get_permitted:true,target_absent:true,issued_at:'2026-11-08T23:00:00Z',expires_at:'2026-11-09T01:00:00Z'}; -const env={DIGEST_CANDIDATES_GCS_URI:'gs://synthetic-bucket/digest-candidates/',DIGEST_CANDIDATES_READER_WIF_PROVIDER:'synthetic-provider',DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT:'synthetic-account',GITHUB_SHA:'a'.repeat(40),QRS_P02_STORAGE_CAPABILITY_RECEIPT:JSON.stringify(receipt)}; +const receipt={schema:'qrs-p02-storage-capability-v1',bucket:'synthetic-bucket',object:KEY,wif_provider:'synthetic-provider',service_account:'synthetic-account',control_sha:'a'.repeat(40),control_tree:'c'.repeat(40),owner_project:'synthetic-owner',policy_readback_reviewed:true,create_permitted:true,get_permitted:true,target_absent:true,issued_at:'2026-11-08T23:00:00Z',expires_at:'2026-11-09T01:00:00Z'}; +const env={DIGEST_CANDIDATES_GCS_URI:'gs://synthetic-bucket/digest-candidates/',DIGEST_CANDIDATES_READER_WIF_PROVIDER:'synthetic-provider',DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT:'synthetic-account',GITHUB_SHA:'d'.repeat(40),P02_CONTROL_SHA:'a'.repeat(40),P02_CONTROL_TREE:'c'.repeat(40),QRS_P02_STORAGE_CAPABILITY_RECEIPT:JSON.stringify(receipt)}; test('requires protected proof bound to exact identity, object, control and short validity',()=>{ assert.deepEqual(verifyCapability(env,now),{bucket:'synthetic-bucket'}); - for(const delta of [{bucket:'wrong-bucket'},{object:'other'},{wif_provider:'other'},{service_account:'other'},{control_sha:'b'.repeat(40)},{policy_readback_reviewed:false},{create_permitted:false},{get_permitted:false},{target_absent:false},{expires_at:'2026-11-08T23:59:59Z'},{expires_at:'2026-11-11T00:00:00Z'},{owner_project:''}]) + for(const delta of [{bucket:'wrong-bucket'},{object:'other'},{wif_provider:'other'},{service_account:'other'},{control_sha:'b'.repeat(40)},{control_tree:'b'.repeat(40)},{policy_readback_reviewed:false},{create_permitted:false},{get_permitted:false},{target_absent:false},{expires_at:'2026-11-08T23:59:59Z'},{expires_at:'2026-11-11T00:00:00Z'},{owner_project:''}]) assert.throws(()=>verifyCapability({...env,QRS_P02_STORAGE_CAPABILITY_RECEIPT:JSON.stringify({...receipt,...delta})},now)); assert.throws(()=>verifyCapability({...env,QRS_P02_STORAGE_CAPABILITY_RECEIPT:''},now)); assert.throws(()=>verifyCapability({...env,QRS_P02_STORAGE_CAPABILITY_RECEIPT:JSON.stringify({...receipt,object:KEY.replace('-r2/','-r1/')})},now)); From 26d323df940e88b1886150226ccda64f53301086 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:39:56 +0800 Subject: [PATCH 2/2] fix: bind P02 preparation to the reviewed capture Co-Authored-By: Codex --- docs/console-p02-baseline-capture.md | 10 ++++++---- scripts/console_p02_baseline_capture.mjs | 2 +- scripts/console_p02_baseline_hosted.mjs | 7 +++---- scripts/console_p02_prepare.mjs | 12 ++++++++---- scripts/console_p02_prepare_local.mjs | 8 ++++++-- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/console-p02-baseline-capture.md b/docs/console-p02-baseline-capture.md index 7cc3b3b8..6b7e9ded 100644 --- a/docs/console-p02-baseline-capture.md +++ b/docs/console-p02-baseline-capture.md @@ -1,9 +1,11 @@ # P02 Worker baseline capture -The manual workflow captures one protected baseline for the fixed Worker. It does not upload or activate a Worker and does not change the release HOLD. The reference is the #1091 source tree with the #1089 asset blob; the dry-run bundle must have SHA-256 `912ad8b3efb9080b96112750146bdf050b6eefed6fae822b57dfa57e99feb57a`. +The manual workflow captures one protected baseline for the fixed Worker. It leaves the release HOLD in effect. The reference is the #1091 backend tree with the asset map from the reviewed #1097 source. The dry-run bundle must match the digest of the verified publication. The workflow stops if the active module or the three frontend declarations differ. -Before a run, an authorized human or Codex must review actual policy and identity evidence proving create and get on the exact object, its absence, and the bucket owner. Store the resulting JSON only in the protected `runtime-strategy-switch` environment secret `QRS_P02_STORAGE_CAPABILITY_RECEIPT`. Do not issue it from synthetic fixtures, an unverified assertion, or bucket-level `testIamPermissions`. The receipt has schema `qrs-p02-storage-capability-v1` and fields `bucket`, `owner_project`, `object`, `wif_provider`, `service_account`, `control_sha`, `issued_at`, `expires_at`, `policy_readback_reviewed`, `create_permitted`, `get_permitted`, and `target_absent`. The final four fields must be true. Times are UTC ISO timestamps, valid for at most 24 hours. The object is fixed in the script. Bind the exact main control SHA and the identity selected by the same reader-variable precedence as the daily workflow. The owner project is the verified bucket owner, which may differ from the consuming project. This secret is not configured by this change. +Before a run, an authorized operator must verify the exact object's absence, bucket owner, workload identity, and permission to create and get that object. Store the capability receipt only in the protected runtime-strategy-switch environment secret QRS_P02_STORAGE_CAPABILITY_RECEIPT. The qrs-p02-storage-capability-v1 receipt includes bucket, owner_project, object, wif_provider, service_account, control_sha, control_tree, issued_at, expires_at, policy_readback_reviewed, create_permitted, get_permitted, and target_absent. The last four fields must be true. Use UTC timestamps and a validity of at most 24 hours. -The workflow verifies the receipt and candidate before authentication. It then checks exact-object metadata for absence, reads the active deployment, module and configuration, and rechecks deployment identity: at most four fixed Worker GETs. It creates the fixed object once with `ifGenerationMatch=0`, then reads that exact generation. The metadata check and generation readback are additional GCS reads to the previously completed local storage preflight; there is no bucket listing. Provider values and the object contents do not enter the workflow log. The protected object contains the reviewed control SHA, capture timestamp, identity and hashes. When assets differ, it first compares the pinned historical source `9b695a4ade90bcc9931f53109e0ad33f63407927` if its rebuild passed the fixed digest; a matching historical module supplies the declarations. Otherwise it records the historical comparison or the reason it was unavailable, and extracts static literals from the active module. It contains only the three AST-decoded literal declarations and the digest of an in-memory reconstruction of the #1091 module. Reconstruction verifies declaration values and every byte outside those declarations, and remains on HOLD for review. Unsupported declarations stop capture. Object creation metadata, including generation, remains available to the owner through exact-object metadata readback; it is not printed. +Use the full reviewed control commit from main history as control_sha. Resolve its Git tree with git rev-parse on that commit's tree, and compare it with the reviewed tree before setting control_tree. The main dispatch revision must contain the control commit and the same workflow file. The run checks out the reviewed control commit and validates the receipt before installing dependencies. The existing reader variable precedence determines the workload identity. -An attempted write with an error or failed readback has an unknown outcome. Do not retry or dispatch again; the owner must inspect the exact object and generation through an authorized channel. Any later upload, activation, rollback or protected account read requires its own applicable authorization. +Use one dispatch for the batch. Check run history and record the attempt before sending it. The workflow checks object absence, reads the active deployment, module and configuration, and rechecks the active deployment with at most four Worker GETs. It creates the fixed object with ifGenerationMatch=0 and verifies that exact generation. The protected object stores the control commit and tree, capture time, active identity, module and configuration digests, and asset comparison. Provider values and object contents stay out of the workflow log. + +If a dispatch, write or readback is unconfirmed, stop and inspect the existing run or exact result object through the authorized channel. A verified capture records a point in time. Before any later authorized upload, activation or rollback, verify the active version and configuration again. diff --git a/scripts/console_p02_baseline_capture.mjs b/scripts/console_p02_baseline_capture.mjs index cf7b123c..8c3a15db 100644 --- a/scripts/console_p02_baseline_capture.mjs +++ b/scripts/console_p02_baseline_capture.mjs @@ -10,7 +10,7 @@ const uuid=x=>typeof x==='string'&&/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12} export const HISTORICAL='0c423d325873d85f9a6b6506c6a9ae35f44753a157e90e3ef1c049b094e0aec2'; export const HISTORICAL_SOURCE='9b695a4ade90bcc9931f53109e0ad33f63407927'; export const REFERENCE='912ad8b3efb9080b96112750146bdf050b6eefed6fae822b57dfa57e99feb57a'; -export const R2_ACTIVE_MODULE='599b86317957dbc77ed8a307274bdf15109683fa6c735a3263aed23e7aa49244'; +export const R2_ACTIVE_MODULE='8e38776bdfb0a665ae87b7bd93146881d8813010461ea3dc9b77e23275e720ea'; const names=['V2_BUILD_ID','V2_PAGE_HTML','V2_ASSETS']; function identity(value){ const ds=value?.deployments,d=ds?.[0],v=d?.versions?.[0]; diff --git a/scripts/console_p02_baseline_hosted.mjs b/scripts/console_p02_baseline_hosted.mjs index 8e43e4a7..ec9c46db 100644 --- a/scripts/console_p02_baseline_hosted.mjs +++ b/scripts/console_p02_baseline_hosted.mjs @@ -1,12 +1,11 @@ // Hosted P02 capture: fixed object and routes; provider responses are never logged. import {readFile} from 'node:fs/promises'; import {pathToFileURL} from 'node:url'; -import {capture} from './console_p02_baseline_capture.mjs'; +import {capture,R2_ACTIVE_MODULE} from './console_p02_baseline_capture.mjs'; import {createFixedApi} from './console_946_backend_release.mjs'; import {createHash} from 'node:crypto'; export const KEY='evidence/qrs-worker-p02/p02-20261109-r2/baseline.json'; -const REVIEWED_MODULE='8e38776bdfb0a665ae87b7bd93146881d8813010461ea3dc9b77e23275e720ea'; export function verifyCapability(env,now=Date.now()){ const uri=/^gs:\/\/([a-z0-9][a-z0-9._-]{1,220}[a-z0-9])(?:\/[^?#]*)?$/.exec(env.DIGEST_CANDIDATES_GCS_URI??''); const provider=env.DIGEST_CANDIDATES_READER_WIF_PROVIDER||env.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER; @@ -56,11 +55,11 @@ async function main(){ if(action==='verify-capability'){verifyCapability(process.env);console.log('{"ok":true,"capability_verified":true}');return;} if(action!=='capture'||process.argv.length!==4)throw Error('input'); const candidate=await readFile(process.argv[3]); - if(createHash('sha256').update(candidate).digest('hex')!==REVIEWED_MODULE)throw Error('candidate'); + if(createHash('sha256').update(candidate).digest('hex')!==R2_ACTIVE_MODULE)throw Error('candidate'); const {bucket}=verifyCapability(process.env); const store=storage({bucket,token:process.env.GOOGLE_ACCESS_TOKEN}); const result=await capture({api:readOnlyWorkerApi(createFixedApi()),store,candidate, - reference:REVIEWED_MODULE,expectedActive:REVIEWED_MODULE, + reference:R2_ACTIVE_MODULE,expectedActive:R2_ACTIVE_MODULE, controlSha:process.env.P02_CONTROL_SHA,controlTree:process.env.P02_CONTROL_TREE}); console.log(JSON.stringify(result));if(!result.ok)process.exitCode=1; } diff --git a/scripts/console_p02_prepare.mjs b/scripts/console_p02_prepare.mjs index 37338a44..9143a3ac 100644 --- a/scripts/console_p02_prepare.mjs +++ b/scripts/console_p02_prepare.mjs @@ -1,26 +1,30 @@ // Offline preparation only. The result does not authorize an upload. import {createHash} from 'node:crypto'; -import {rebuildWithAssets, REFERENCE, HISTORICAL_SOURCE, HISTORICAL, R2_ACTIVE_MODULE} from './console_p02_baseline_capture.mjs'; +import {rebuildWithAssets, HISTORICAL_SOURCE, HISTORICAL, R2_ACTIVE_MODULE} from './console_p02_baseline_capture.mjs'; import {POLICY} from './console_m09_release.mjs'; const digest=x=>createHash('sha256').update(x).digest('hex'); const hex=(x,n)=>typeof x==='string'&&new RegExp(`^[a-f0-9]{${n}}$`).test(x); const uuid=x=>typeof x==='string'&&/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(x); const names=['V2_BUILD_ID','V2_PAGE_HTML','V2_ASSETS']; -export const P02_PREPARE_PINS=Object.freeze({reference:REFERENCE, +export const P02_PREPARE_PINS=Object.freeze({reference:R2_ACTIVE_MODULE, active:R2_ACTIVE_MODULE, - referenceFrontendBlob:'5287f259185ed44f3784aa6374cb00412b3c6238', + assetsSource:'dd63b3c8b73b588ce411316d904efd26f2c4f382', + assetsTree:'c08d5542b032abaa008e882dd97ed2c38b3ad236', + referenceFrontendBlob:'a7237657bb6e48c57e947875f629d28bafb5ea27', historical:HISTORICAL_SOURCE,historicalBundle:HISTORICAL}); // The caller must obtain the protected record through its authorized channel. // Dependency injection exists for offline fixtures, not as a production policy override. export function prepareRecord(record,reference,evidence,ts,pins=P02_PREPARE_PINS){ if(record?.schema!=='qrs-worker-p02-baseline-v1'||!hex(pins.control,40)||record.control_sha!==pins.control|| + (pins.controlTree!==undefined&&(!hex(pins.controlTree,40)||record.control_tree!==pins.controlTree))|| (pins.active!==undefined&&record.active_module_sha256!==pins.active)|| record.reference_module_sha256!==pins.reference||!Buffer.isBuffer(reference)||digest(reference)!==pins.reference|| !uuid(record.version)||!uuid(record.deployment)|| !['active_module_sha256','configuration_sha256','migration_tag_sha256'].every(k=>hex(record[k],64))|| - evidence?.source!==POLICY.source||evidence?.tree!==POLICY.tree||evidence?.frontendBlob!==pins.referenceFrontendBlob) + evidence?.source!==POLICY.source||evidence?.tree!==POLICY.tree||evidence?.frontendBlob!==pins.referenceFrontendBlob|| + (pins.assetsTree!==undefined&&evidence.assetsTree!==pins.assetsTree)) throw Error('baseline_invalid'); const comparison=record.asset_comparison; if(!comparison||Object.keys(comparison).length!==names.length||!names.every(n=>typeof comparison[n]==='boolean')) diff --git a/scripts/console_p02_prepare_local.mjs b/scripts/console_p02_prepare_local.mjs index 1444a9d8..240c1e20 100644 --- a/scripts/console_p02_prepare_local.mjs +++ b/scripts/console_p02_prepare_local.mjs @@ -29,9 +29,13 @@ export async function prepareLocal({recordPath,referencePath,historicalPath,outp if(![recordPath,referencePath,outputPath].every(p=>typeof p==='string'&&isAbsolute(p)))throw Error('path'); const repo=deps.root??root,output=resolve(outputPath),rel=relative(repo,output); if(!rel||(!rel.startsWith('../')&&!isAbsolute(rel)))throw Error('output_location'); - const g=deps.git??git,pins=deps.pins??{...P02_PREPARE_PINS,control:controlSha}; + const g=deps.git??git; + if(!deps.pins&&!/^[a-f0-9]{40}$/.test(controlSha??''))throw Error('control'); + const pins=deps.pins??{...P02_PREPARE_PINS,control:controlSha,controlTree:g('rev-parse',`${controlSha}^{tree}`)}; + const assetsSource=pins.assetsSource??POLICY.source; const evidence={source:g('rev-parse',`${POLICY.source}^{commit}`),tree:g('rev-parse',`${POLICY.source}^{tree}`), - frontendBlob:g('rev-parse',`${POLICY.source}:${asset}`)}; + frontendBlob:g('rev-parse',`${assetsSource}:${asset}`), + ...(pins.assetsSource?{assetsTree:g('rev-parse',`${assetsSource}^{tree}`)}:{})}; const [record,reference]=await Promise.all([privateRecord(recordPath),readFile(referencePath)]); if(reference.length>10*1024*1024)throw Error('bundle'); let historical;