From efc19c68b4aa338870e277da93a6af39f2706134 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:22:58 +0800 Subject: [PATCH] feat: Capture the reviewed Worker baseline Co-Authored-By: Codex --- .../console-p02-baseline-capture.yml | 79 +++++++++++ docs/console-p02-baseline-capture.md | 9 ++ scripts/console_946_backend_release.mjs | 4 +- scripts/console_p02_baseline_capture.mjs | 127 ++++++++++++++++++ scripts/console_p02_baseline_hosted.mjs | 65 +++++++++ scripts/console_p02_candidate_build.sh | 27 ++++ tests/console_p02_baseline_capture.test.mjs | 44 ++++++ tests/console_p02_baseline_hosted.test.mjs | 27 ++++ 8 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/console-p02-baseline-capture.yml create mode 100644 docs/console-p02-baseline-capture.md create mode 100644 scripts/console_p02_baseline_capture.mjs create mode 100644 scripts/console_p02_baseline_hosted.mjs create mode 100644 scripts/console_p02_candidate_build.sh create mode 100644 tests/console_p02_baseline_capture.test.mjs create mode 100644 tests/console_p02_baseline_hosted.test.mjs diff --git a/.github/workflows/console-p02-baseline-capture.yml b/.github/workflows/console-p02-baseline-capture.yml new file mode 100644 index 00000000..e0b307e0 --- /dev/null +++ b/.github/workflows/console-p02-baseline-capture.yml @@ -0,0 +1,79 @@ +name: Console P02 baseline capture +on: + workflow_dispatch: + inputs: + control_sha: + description: Reviewed control commit on main + type: string + required: true +permissions: + contents: read + id-token: write +concurrency: + group: strategy-switch-console-deploy + cancel-in-progress: false +jobs: + capture: + if: github.repository == 'QuantStrategyLab/QuantRuntimeSettings' && github.ref == 'refs/heads/main' && github.run_attempt == 1 + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: runtime-strategy-switch + env: + 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 }} + DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER }} + DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ vars.DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + fetch-depth: 0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: '22.23.4' + - 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: Prepare optional pinned historical module + run: | + set -euo pipefail + if bash scripts/console_946_reviewed_main_build.sh > "$RUNNER_TEMP/qrs-p02-historical-build.log" 2>&1; then + echo 'historical_build_verified' + else + rm -f "$RUNNER_TEMP/qrs-reviewed-main/worker.js" + echo 'historical_build_unavailable' + fi + - 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 + with: + workload_identity_provider: ${{ vars.DIGEST_CANDIDATES_READER_WIF_PROVIDER || vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT || vars.DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + token_format: access_token + create_credentials_file: false + - name: Capture fixed baseline and verify exact object generation + env: + QRS_P02_STORAGE_CAPABILITY_RECEIPT: ${{ secrets.QRS_P02_STORAGE_CAPABILITY_RECEIPT }} + GOOGLE_ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + 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" ]] + node scripts/console_p02_baseline_hosted.mjs capture "$RUNNER_TEMP/qrs-p02-baseline-candidate/worker.js" "$RUNNER_TEMP/qrs-reviewed-main/worker.js" diff --git a/docs/console-p02-baseline-capture.md b/docs/console-p02-baseline-capture.md new file mode 100644 index 00000000..7cc3b3b8 --- /dev/null +++ b/docs/console-p02-baseline-capture.md @@ -0,0 +1,9 @@ +# 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`. + +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. + +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. + +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. diff --git a/scripts/console_946_backend_release.mjs b/scripts/console_946_backend_release.mjs index 9c7747cf..0b35717c 100644 --- a/scripts/console_946_backend_release.mjs +++ b/scripts/console_946_backend_release.mjs @@ -30,7 +30,7 @@ function versions(result){ if(new Set(items.map(x=>x.id)).size!==items.length)fail('version_list_invalid'); return items.map(x=>x.id); } -function resources(value,id){ +export function resources(value,id){ if(value?.id!==id)fail('baseline_invalid'); const x=value.resources,r=x?.script_runtime,s=x?.script; if(!object(x)||!object(r)||!object(s)||!Array.isArray(x.bindings)||!x.bindings.length||x.bindings.length>128 @@ -58,7 +58,7 @@ function resources(value,id){ ||!x.bindings.some(b=>b.name==='STRATEGY_SWITCH_RUNTIME_INSTANCES'&&b.type==='durable_object_namespace'))fail('baseline_invalid'); return x; } -function config(x){ +export function config(x){ const runtime=structuredClone(x.script_runtime);delete runtime.migration_tag; return canonical({bindings:[...x.bindings].sort((a,b)=>a.name.localeCompare(b.name)),runtime, handlers:x.script.handlers,named_handlers:x.script.named_handlers===undefined?null:[...x.script.named_handlers].sort((a,b)=>a.name.localeCompare(b.name)), diff --git a/scripts/console_p02_baseline_capture.mjs b/scripts/console_p02_baseline_capture.mjs new file mode 100644 index 00000000..9b52c821 --- /dev/null +++ b/scripts/console_p02_baseline_capture.mjs @@ -0,0 +1,127 @@ +// Fixed P02 baseline capture. No provider values or identifiers are logged. +import {createHash} from 'node:crypto'; +import {createRequire} from 'node:module'; +import {resources,config} from './console_946_backend_release.mjs'; +import {compareAssetDeclarations} from './console_m09_asset_compare.mjs'; + +const require=createRequire(new URL('../web/strategy-switch-console/frontend/package.json',import.meta.url)); +const digest=x=>createHash('sha256').update(x).digest('hex'); +const uuid=x=>typeof x==='string'&&/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(x); +export const HISTORICAL='0c423d325873d85f9a6b6506c6a9ae35f44753a157e90e3ef1c049b094e0aec2'; +export const HISTORICAL_SOURCE='9b695a4ade90bcc9931f53109e0ad33f63407927'; +export const REFERENCE='912ad8b3efb9080b96112750146bdf050b6eefed6fae822b57dfa57e99feb57a'; +const names=['V2_BUILD_ID','V2_PAGE_HTML','V2_ASSETS']; +function identity(value){ + const ds=value?.deployments,d=ds?.[0],v=d?.versions?.[0]; + if(!Array.isArray(ds)||!ds.length||ds.length>10||!uuid(d?.id)||d.strategy!=='percentage'|| + !Array.isArray(d.versions)||d.versions.length!==1||!uuid(v?.version_id)||v.percentage!==100)throw Error('identity'); + return {deployment:d.id,version:v.version_id}; +} +function moduleBytes(value,id){ + const m=value?.modules?.[0]; + if(value?.id!==id||!Array.isArray(value.modules)||value.modules.length!==1||m?.name!=='worker.js'|| + m.content_type!=='application/javascript+module'||typeof m.content_base64!=='string'||!m.content_base64.length|| + m.content_base64.length>Math.ceil(10*1024*1024/3)*4||m.content_base64.length%4||/[^A-Za-z0-9+/=]/.test(m.content_base64))throw Error('module'); + const b=Buffer.from(m.content_base64,'base64'); + if(!b.length||b.length>10*1024*1024||b.toString('base64')!==m.content_base64)throw Error('module'); + return b; +} +// Only literal strings and ordinary, non-computed object properties are accepted. +function assetDeclarations(bytes,ts){ + const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes); + const source=ts.createSourceFile('worker.js',text,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS); + if(source.parseDiagnostics.length)throw Error('unsupported'); + function literal(node){ + if(ts.isStringLiteral(node))return node.text; + if(!ts.isObjectLiteralExpression(node))throw Error('unsupported'); + const value=Object.create(null); + for(const p of node.properties){ + if(!ts.isPropertyAssignment(p)||!(ts.isStringLiteral(p.name)||ts.isIdentifier(p.name)))throw Error('unsupported'); + const key=p.name.text; + if(key==='__proto__'||Object.hasOwn(value,key))throw Error('unsupported'); + value[key]=literal(p.initializer); + } + return value; + } + const found={},values={},spans={}; + for(const s of source.statements){ + if(!ts.isVariableStatement(s))continue; + for(const d of s.declarationList.declarations){ + if(!ts.isIdentifier(d.name)||!names.includes(d.name.text))continue; + const n=d.name.text; + if(s.modifiers?.length||(s.declarationList.flags & (ts.NodeFlags.Let|ts.NodeFlags.Const))||Object.hasOwn(found,n)||s.declarationList.declarations.length!==1||!d.initializer)throw Error('unsupported'); + let value=d.initializer; + if(n==='V2_ASSETS'){ + if(!ts.isCallExpression(value)||!ts.isPropertyAccessExpression(value.expression)|| + !ts.isIdentifier(value.expression.expression)||value.expression.expression.text!=='Object'||value.expression.name.text!=='freeze'|| + value.arguments.length!==1||!ts.isObjectLiteralExpression(value.arguments[0]))throw Error('unsupported'); + value=value.arguments[0]; + } else if(!ts.isStringLiteral(value))throw Error('unsupported'); + const decoded=literal(value), encoded=JSON.stringify(decoded); + values[n]=encoded; spans[n]={start:s.getStart(source),end:s.end}; + found[n]=n==='V2_ASSETS'?`var ${n} = Object.freeze(${encoded});`:`var ${n} = ${encoded};`; + } + } + if(names.some(n=>!Object.hasOwn(found,n)))throw Error('unsupported'); + return {declarations:found,values,spans,text}; +} +// Reparse the reconstructed module; only the three declaration spans may change. +export function rebuildWithAssets(candidate,active,ts){ + const original=assetDeclarations(candidate,ts), observed=assetDeclarations(active,ts); + const ordered=names.map(name=>({name,...original.spans[name]})).sort((a,b)=>a.start-b.start); + let cursor=0, rebuilt=''; + for(const span of ordered){ + if(span.startchecked.values[name]!==observed.values[name]))throw Error('rebuild'); + const after=names.map(name=>({name,...checked.spans[name]})).sort((a,b)=>a.start-b.start); + if(ordered.some((span,i)=>span.name!==after[i].name))throw Error('rebuild'); + const outside=(text,spans)=>{let p=0;const result=[];for(const span of spans){result.push(text.slice(p,span.start));p=span.end;}result.push(text.slice(p));return result;}; + const oldParts=outside(original.text,ordered),newParts=outside(checked.text,after); + if(oldParts.some((part,i)=>!Buffer.from(part).equals(Buffer.from(newParts[i]))))throw Error('rebuild'); + return {declarations:observed.declarations,sha256:digest(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,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(!compareAssetDeclarations(candidate,candidate,ts).ok)throw Error('candidate'); + stage='storage_preflight'; await store.reserve(); + stage='active_before'; const before=identity(await api.deployments()); + stage='module'; const active=moduleBytes(await api.betaVersion(before.version),before.version); + stage='configuration'; const r=resources(await api.scriptVersion(before.version),before.version); + if(typeof r.script_runtime.migration_tag!=='string'||!r.script_runtime.migration_tag)throw Error('configuration'); + stage='active_after'; const after=identity(await api.deployments()); + if(before.version!==after.version||before.deployment!==after.deployment)throw Error('drift'); + stage='assets'; const compared=compareAssetDeclarations(active,candidate,ts); + if(!compared.equal)throw Error('unsupported'); + let rebuilt, historical; + if(!compared.ok){ + historical={source:HISTORICAL_SOURCE,status:'unavailable',reason:'build_or_read_unavailable'}; + let assets=active; + if(Buffer.isBuffer(alternate)){ + if(digest(alternate)!==alternateReference)historical.reason='digest_mismatch'; + else { + const comparison=compareAssetDeclarations(active,alternate,ts); + if(comparison.ok){historical={source:HISTORICAL_SOURCE,status:'match'};assets=alternate;} + else if(comparison.equal)historical={source:HISTORICAL_SOURCE,status:'different'}; + else historical={source:HISTORICAL_SOURCE,status:'unavailable',reason:'unsupported_declarations'}; + } + } + 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:digest(active), + 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)); + const generation=await store.create(bytes); + stage='readback'; const saved=await store.read(generation); + if(!Buffer.isBuffer(saved)||!saved.equals(bytes))throw Error('readback'); + return {ok:true,asset_declarations_equal:compared.ok,saved:true,readback_verified:true,hold:true}; + }catch{return {ok:false,stage,write_attempted:writeAttempted,retry:false,hold:true};} +} diff --git a/scripts/console_p02_baseline_hosted.mjs b/scripts/console_p02_baseline_hosted.mjs new file mode 100644 index 00000000..8d8c0d30 --- /dev/null +++ b/scripts/console_p02_baseline_hosted.mjs @@ -0,0 +1,65 @@ +// 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} 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-r1/baseline.json'; +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|| + 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) + throw Error('capability'); + return {bucket:uri[1]}; +} +export function storage({bucket,token,fetcher=fetch}){ + if(!bucket||!token)throw Error('storage'); + const base=`https://storage.googleapis.com/storage/v1/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(KEY)}`; + const headers={Authorization:`Bearer ${token}`}; + const request=(url,options={})=>fetcher(url,{...options,redirect:'error',signal:AbortSignal.timeout(20000),headers:{...headers,...options.headers}}); + return { + async reserve(){const response=await request(base);await response.body?.cancel();if(response.status!==404)throw Error('target_unavailable');}, + async create(bytes){ + const url=`https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodeURIComponent(KEY)}&ifGenerationMatch=0`; + const response=await request(url,{method:'POST',headers:{'Content-Type':'application/json'},body:bytes}); + if(!response.ok){await response.body?.cancel();throw Error('write_unknown');} + const body=await response.json(); + if(body?.bucket!==bucket||body.name!==KEY||!/^\d+$/.test(body.generation??''))throw Error('write_unknown'); + return body.generation; + }, + async read(generation){ + if(!/^\d+$/.test(generation??''))throw Error('readback'); + const response=await request(`${base}?alt=media&generation=${generation}`); + if(!response.ok){await response.body?.cancel();throw Error('readback');} + const len=response.headers.get('content-length'); + if(len!==null&&(!/^\d+$/.test(len)||Number(len)>12*1024*1024)){await response.body?.cancel();throw Error('readback');} + const data=Buffer.from(await response.arrayBuffer());if(data.length>12*1024*1024)throw Error('readback');return data; + } + }; +} +export function readOnlyWorkerApi(api){ + return Object.freeze({deployments:api.deployments,betaVersion:api.betaVersion,scriptVersion:api.scriptVersion}); +} +async function main(){ + const action=process.argv[2]; + if(action==='verify-capability'){verifyCapability(process.env);console.log('{"ok":true,"capability_verified":true}');return;} + if(action!=='capture'||process.argv.length!==5)throw Error('input'); + const candidate=await readFile(process.argv[3]); + if(createHash('sha256').update(candidate).digest('hex')!==REFERENCE)throw Error('candidate'); + let alternate;try{alternate=await readFile(process.argv[4]);}catch{} + const {bucket}=verifyCapability(process.env); + const store=storage({bucket,token:process.env.GOOGLE_ACCESS_TOKEN}); + const result=await capture({api:readOnlyWorkerApi(createFixedApi()),store,candidate,alternate,controlSha:process.env.GITHUB_SHA}); + console.log(JSON.stringify(result));if(!result.ok)process.exitCode=1; +} +if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href){ + try{await main()}catch{console.error('{"ok":false,"reason":"input_or_capability_invalid","retry":false}');process.exitCode=1;} +} diff --git a/scripts/console_p02_candidate_build.sh b/scripts/console_p02_candidate_build.sh new file mode 100644 index 00000000..f3eaadf2 --- /dev/null +++ b/scripts/console_p02_candidate_build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +readonly source=cabe5a1534e0c992713b084030af313eada758ca +readonly tree=42b404d5755b658deff73100b495ecf26fa3a6b1 +readonly blob=5287f259185ed44f3784aa6374cb00412b3c6238 +readonly asset=web/strategy-switch-console/v2_asset_map.js +readonly expected=912ad8b3efb9080b96112750146bdf050b6eefed6fae822b57dfa57e99feb57a +output="${1:?output directory required}" +[[ "$(git rev-parse "${source}^{tree}")" == "$tree" ]] +[[ "$(git rev-parse "$source:$asset")" == "$blob" ]] +[[ "$(git rev-parse "084ba4281cfff3304cf226ce511a2fc0a6b67b08:$asset")" == "$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" +cp "$tmp/web/strategy-switch-console/wrangler.toml.example" "$tmp/web/strategy-switch-console/wrangler.toml" +( + cd "$tmp/web/strategy-switch-console" + unset CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_KEY CLOUDFLARE_EMAIL + export WRANGLER_SEND_METRICS=false CLOUDFLARE_CF_FETCH_ENABLED=false npm_config_update_notifier=false + npx --offline wrangler@4.106.0 deploy --dry-run --config wrangler.toml --outdir "$output" +) +[[ -s "$output/worker.js" ]] +[[ "$(shasum -a 256 "$output/worker.js" | cut -d' ' -f1)" == "$expected" ]] diff --git a/tests/console_p02_baseline_capture.test.mjs b/tests/console_p02_baseline_capture.test.mjs new file mode 100644 index 00000000..cc41c61e --- /dev/null +++ b/tests/console_p02_baseline_capture.test.mjs @@ -0,0 +1,44 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {createHash} from 'node:crypto'; +import {capture} from '../scripts/console_p02_baseline_capture.mjs'; +const candidate=Buffer.from('var V2_BUILD_ID = "a";\nvar V2_PAGE_HTML = "html";\nvar V2_ASSETS = Object.freeze({"/x": {contentType: "text/javascript", base64: "YWJj"}});'); +const reference=createHash('sha256').update(candidate).digest('hex'); +const version='aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',dep='bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'; +function fixture(options={}){ + const calls=[];let saved,ds=0; + const api={async deployments(){calls.push('deployment');return {deployments:[{id:options.drift&&++ds===2?'cccccccc-cccc-4ccc-8ccc-cccccccccccc':dep,strategy:'percentage',versions:[{version_id:version,percentage:100}]}]};}, + async betaVersion(){calls.push('module');return {id:version,modules:[{name:'worker.js',content_type:'application/javascript+module',content_base64:(options.active||candidate).toString('base64')}]};}, + async scriptVersion(){calls.push('config');return {id:version,resources:{bindings:[{name:'STRATEGY_SWITCH_CONFIG',type:'kv_namespace',namespace_id:'1'.repeat(32)},{name:'STRATEGY_SWITCH_RUNTIME_INSTANCES',type:'durable_object_namespace',namespace_id:'2'.repeat(32),class_name:'RuntimeInstances'}],script_runtime:{compatibility_date:'2026-06-08',usage_model:'standard',migration_tag:'private'},script:{handlers:['fetch']}}};}}; + 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,controlSha:"a".repeat(40),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()=>{ + for(const [opt,stage,expected] of [[{active:Buffer.from(candidate.toString().replace('"YWJj"','secret()'))},'assets',['reserve','deployment','module','config','deployment']], [{drift:true},'active_after',['reserve','deployment','module','config','deployment']], [{reserveFail:true},'storage_preflight',['reserve']], [{createFail:true},'save',['reserve','deployment','module','config','deployment','create']]]){ + const {f,result}=await run(opt);assert.equal(result.ok,false);assert.equal(result.stage,stage);assert.equal(result.retry,false);assert.deepEqual(f.calls,expected); + } +}); +test('bad candidate causes no storage or Worker reads',async()=>{const f=fixture();const r=await capture({api:f.api,store:f.store,candidate:Buffer.from('bad'),reference,controlSha:'a'.repeat(40)});assert.equal(r.ok,false);assert.deepEqual(f.calls,[]);}); +test('mismatch removes comments and rejects ambiguous or modified literal declarations',async()=>{ + const annotated=Buffer.from(candidate.toString().replace('"html"','/* private-marker */ "other"')); + const good=await run({active:annotated});assert.equal(good.result.ok,true);assert.doesNotMatch(JSON.stringify(good.f.record),/private-marker/); + for(const text of [candidate.toString().replace('"YWJj"','secret()'),candidate.toString().replace('base64: "YWJj"','base64: "YWJj", base64: "ZGVm"'),candidate.toString().replace('base64: "YWJj"','__proto__: "x"'),candidate.toString().replace('var V2_PAGE_HTML','export var V2_PAGE_HTML')]){ + const {f,result}=await run({active:Buffer.from(text)});assert.equal(result.ok,false);assert.ok(!f.calls.includes('create')); + } +}); + +test('mismatch checks the pinned historical artifact before deriving declarations',async()=>{ + const active=Buffer.from(candidate.toString().replace('"html"','"historical"')); + const hash=b=>createHash('sha256').update(b).digest('hex'); + const matched=await run({active,alternate:active,alternateReference:hash(active)}); + assert.equal(matched.result.ok,true);assert.equal(matched.f.record.historical_comparison.status,'match'); + assert.match(matched.f.record.asset_declarations.V2_PAGE_HTML,/historical/); + const different=Buffer.from(candidate.toString().replace('"html"','"alternate"')); + const other=await run({active,alternate:different,alternateReference:hash(different)}); + assert.equal(other.f.record.historical_comparison.status,'different');assert.match(other.f.record.asset_declarations.V2_PAGE_HTML,/historical/); + const absent=await run({active});assert.equal(absent.f.record.historical_comparison.reason,'build_or_read_unavailable'); + const invalid=await run({active,alternate:different,alternateReference:'f'.repeat(64)});assert.equal(invalid.f.record.historical_comparison.reason,'digest_mismatch'); +}); diff --git a/tests/console_p02_baseline_hosted.test.mjs b/tests/console_p02_baseline_hosted.test.mjs new file mode 100644 index 00000000..c148b55a --- /dev/null +++ b/tests/console_p02_baseline_hosted.test.mjs @@ -0,0 +1,27 @@ +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)}; +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:''}]) + assert.throws(()=>verifyCapability({...env,QRS_P02_STORAGE_CAPABILITY_RECEIPT:JSON.stringify({...receipt,...delta})},now)); + assert.throws(()=>verifyCapability({...env,QRS_P02_STORAGE_CAPABILITY_RECEIPT:''},now)); +}); +test('storage addresses exact object, creates once conditionally, reads exact generation',async()=>{ + const calls=[]; + const fetcher=async(url,opt)=>{calls.push({url,opt});if(calls.length===1)return new Response('',{status:404});if(calls.length===2)return Response.json({bucket:'synthetic-bucket',name:KEY,generation:'42'});return new Response('record');}; + const s=storage({bucket:'synthetic-bucket',token:'synthetic-token',fetcher}); + await s.reserve();const gen=await s.create(Buffer.from('record'));assert.equal(gen,'42');assert.equal((await s.read(gen)).toString(),'record'); + assert.equal(calls.length,3);assert.equal(calls[0].opt.method,undefined);assert.ok(calls[0].url.includes(encodeURIComponent(KEY))); + assert.equal(calls[1].opt.method,'POST');assert.match(calls[1].url,/ifGenerationMatch=0/);assert.ok(calls[1].url.includes(encodeURIComponent(KEY))); + assert.match(calls[2].url,/alt=media&generation=42$/);assert.ok(calls.every(c=>!c.url.includes('synthetic-token'))); +}); +test('existing or unreadable target and uncertain writes stop without retry',async()=>{ + for(const status of [200,403,500]){let count=0;const s=storage({bucket:'synthetic-bucket',token:'x',fetcher:async()=>{count++;return new Response('',{status});}});await assert.rejects(s.reserve());assert.equal(count,1);} + let count=0;const s=storage({bucket:'synthetic-bucket',token:'x',fetcher:async()=>{count++;throw Error('private');}});await assert.rejects(s.create(Buffer.from('x')));assert.equal(count,1); +}); + +test('Worker interface exposes only the three fixed GET methods',()=>{const f=()=>{};const result=readOnlyWorkerApi({deployments:f,betaVersion:f,scriptVersion:f,upload:f,activate:f,versions:f});assert.deepEqual(Object.keys(result),['deployments','betaVersion','scriptVersion']);assert.ok(Object.isFrozen(result));});