Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/console-p02-baseline-capture.yml
Original file line number Diff line number Diff line change
@@ -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"
9 changes: 9 additions & 0 deletions docs/console-p02-baseline-capture.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions scripts/console_946_backend_release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)),
Expand Down
127 changes: 127 additions & 0 deletions scripts/console_p02_baseline_capture.mjs
Original file line number Diff line number Diff line change
@@ -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.start<cursor)throw Error('unsupported');
rebuilt+=original.text.slice(cursor,span.start)+observed.declarations[span.name];cursor=span.end;
}
rebuilt+=original.text.slice(cursor);
const bytes=Buffer.from(rebuilt,'utf8'), checked=assetDeclarations(bytes,ts);
if(names.some(name=>checked.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};}
}
Loading
Loading