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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,49 @@ jobs:
echo "genuinely changed, that is a claim change and belongs on HAC-333."
} >> "$GITHUB_STEP_SUMMARY"

filmed-run-gate:
name: Filmed run record gate
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

# HAC-324's filmed-run record is *derived* from the packet the traversal
# emitted, which sits committed beside it. A derivation that quietly
# changed an execution fact would therefore look corroborated rather than
# contradicted, so the gate rebuilds the record and proves every execution
# fact is byte-identical to the emitted bytes.
- name: Verify the filmed-run record
run: pnpm run check:filmed-run

- name: Every gate is proven to fail on the defect it targets
run: pnpm vitest run test/hac-324-filmed-run-gates.test.mjs

- name: Explain the failure
if: failure()
run: |
{
echo "## Filmed run record gate failed"
echo
echo "**Invariant.** \`experiments/hac-324/evidence/filmed-run.json\` is"
echo "\`filmed-run.raw.json\` plus exactly one declared correction: the"
echo "observer principal, which the HAC-340 provisioning script recorded as"
echo "the provisioning caller rather than the principal that performed the"
echo "read-back. Every execution fact must be byte-identical between the two,"
echo "the capture package must name the same run, and every frame must still"
echo "match its recorded digest."
echo
echo "Rebuild with \`node experiments/hac-324/bin/build-filmed-run.mjs\`."
echo "Do not hand-edit either file: the raw packet is the emitted evidence and"
echo "the derived record is generated from it."
} >> "$GITHUB_STEP_SUMMARY"

evaluation-gate:
name: Evaluation gate
runs-on: ubuntu-24.04
Expand Down
118 changes: 118 additions & 0 deletions experiments/hac-324/bin/build-filmed-run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
/**
* HAC-324 — derive the filmed-run record from the packet the traversal emitted.
*
* The traversal's own packet carries one field that is not true of the run it
* describes. `10-provision.sh` records `observerPrincipal` from
* `gcloud config get-value account` at *provision* time, so it names the human
* operator. The authoritative traversal did not authenticate as that human: it
* impersonated a dedicated keyless observer service account, because gcloud 580
* refuses audience-scoped identity tokens for user accounts.
*
* Two ways to fix that were available and both were wrong. Editing the emitted
* packet in place destroys the only verbatim record of what the run produced.
* Leaving it and explaining the discrepancy in prose elsewhere leaves two
* artifacts disagreeing about who performed OBSERVED, and the wrong one is the
* one that looks authoritative.
*
* So the emitted bytes stay untouched in `filmed-run.raw.json`, and this derives
* `filmed-run.json` from them, splitting one overloaded field into the two
* distinct facts it was conflating:
*
* operatorPrincipal — who provisioned the environment
* observerPrincipal — who performed the independently authenticated read-back
*
* The producing layer for HAC-324's evidence is this script. The HAC-340 script
* that emitted the raw packet is the approved runtime source at
* `ae6d0d3c405b6169d5f0495c22aaf05d8fc1de4a` and is deliberately not modified —
* changing it would change the runtime source SHA and break the parity claim
* the run exists to make.
*
* node experiments/hac-324/bin/build-filmed-run.mjs
*/

import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const here = dirname(fileURLToPath(import.meta.url));
const evidence = join(here, '..', 'evidence');

/**
* The observer identity the traversal actually used.
*
* Recorded here rather than inferred: the Cloud Run services are deleted, so
* nothing can be re-queried to establish it, and a value that cannot be
* re-derived should be stated explicitly rather than reconstructed.
*/
const OBSERVER_PRINCIPAL =
'serviceAccount:interlock-hac340-observer@interlock-film-260823.iam.gserviceaccount.com';

/** The one field this derivation is allowed to change, and what it becomes. */
export const PRINCIPAL_CORRECTION = {
field: 'resources.observerPrincipal',
rawValue: 'user:qwynn@marcellelabs.io',
reclassifiedAs: 'resources.operatorPrincipal',
correctedValue: OBSERVER_PRINCIPAL,
reason:
'10-provision.sh records the provisioning caller, not the principal that performed the read-back. '
+ 'The traversal impersonated a dedicated keyless observer service account because gcloud 580 refuses '
+ 'audience-scoped identity tokens for user accounts. Adjudicated NON_MATERIAL: the substitution changed '
+ 'no authorization behaviour, and the fail-closed controls returned an identical 403/401/403.',
classification: 'NON_MATERIAL',
};

/**
* Everything about the run that this derivation must leave alone.
*
* Named rather than implied, so the verifier can prove the derivation touched
* no execution fact instead of asserting it.
*/
export const EXECUTION_FACTS = [
'commitSha',
'model',
'adkPath',
'correlationId',
'decision',
'receiptId',
'receiptDigest',
'protectedMutation',
'observation',
'runtimeProof',
'controls',
'expectedConfiguration',
'observedConfiguration',
];

export function deriveFilmedRun(raw) {
const derived = structuredClone(raw);
const resources = derived.resources;

// Split the conflated field. The provisioning caller keeps its own name so
// the fact is preserved rather than overwritten.
resources.operatorPrincipal = raw.resources.observerPrincipal;
resources.observerPrincipal = OBSERVER_PRINCIPAL;

derived.principalProjection = {
note:
'observerPrincipal in the emitted packet named the provisioning caller. It is corrected here and the '
+ 'original value is preserved as operatorPrincipal. filmed-run.raw.json holds the emitted bytes unchanged.',
correction: PRINCIPAL_CORRECTION,
rawPacket: 'experiments/hac-324/evidence/filmed-run.raw.json',
producer: 'experiments/hac-324/bin/build-filmed-run.mjs',
};

return derived;
}

if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
const raw = JSON.parse(readFileSync(join(evidence, 'filmed-run.raw.json'), 'utf8'));
const derived = deriveFilmedRun(raw);
writeFileSync(join(evidence, 'filmed-run.json'), `${JSON.stringify(derived, null, 2)}\n`);
process.stdout.write(
'HAC-324 filmed-run record derived\n'
+ ` operatorPrincipal ${derived.resources.operatorPrincipal}\n`
+ ` observerPrincipal ${derived.resources.observerPrincipal}\n`
+ ` execution facts carried unchanged: ${EXECUTION_FACTS.length}\n`,
);
}
114 changes: 114 additions & 0 deletions experiments/hac-324/bin/verify-filmed-run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* HAC-324 — refuses a filmed-run record that is not the emitted packet plus the
* one declared correction.
*
* A derivation that can silently change an execution fact is worse than no
* derivation: the raw packet would still be sitting beside it looking like
* corroboration. So this proves three things rather than asserting them.
*
* 1. `filmed-run.json` is exactly what the producer rebuilds from the raw
* packet — no hand edit survives.
* 2. Every execution fact is byte-identical between raw and derived. The
* correction touched the principal projection and nothing else.
* 3. The capture package agrees with the derived record about which run it
* is, and its frames are still the frames it claims.
*
* node experiments/hac-324/bin/verify-filmed-run.mjs
*/

import { readFileSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { deriveFilmedRun, EXECUTION_FACTS, PRINCIPAL_CORRECTION } from './build-filmed-run.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, '..', '..', '..');
const evidence = join(here, '..', 'evidence');

const errors = [];
const fail = (message) => errors.push(message);
const readJson = (p) => JSON.parse(readFileSync(p, 'utf8'));

const raw = readJson(join(evidence, 'filmed-run.raw.json'));
const derived = readJson(join(evidence, 'filmed-run.json'));
const pkg = readJson(join(evidence, 'capture-package.json'));
const teardown = readJson(join(evidence, 'teardown.json'));

/* -- 1. the record is derived, not hand-edited ----------------------------- */

const rebuilt = deriveFilmedRun(raw);
if (JSON.stringify(rebuilt) !== JSON.stringify(derived)) {
fail('filmed-run.json is not what build-filmed-run.mjs produces from the raw packet');
}

/* -- 2. the correction changed the projection and nothing else ------------- */

for (const field of EXECUTION_FACTS) {
if (JSON.stringify(raw[field]) !== JSON.stringify(derived[field])) {
fail(`execution fact "${field}" differs between the emitted packet and the derived record`);
}
}

// The resources block is the only one that may differ, and only in the two
// principal keys. Anything else moving there would be a silent rewrite of the
// environment the run happened in.
const rawResourceKeys = Object.keys(raw.resources);
for (const key of rawResourceKeys) {
if (key === 'observerPrincipal') continue;
if (JSON.stringify(raw.resources[key]) !== JSON.stringify(derived.resources[key])) {
fail(`resources.${key} differs between the emitted packet and the derived record`);
}
}
if (derived.resources.operatorPrincipal !== raw.resources.observerPrincipal) {
fail('operatorPrincipal does not preserve the value the packet actually emitted');
}
if (derived.resources.observerPrincipal === raw.resources.observerPrincipal) {
fail('observerPrincipal was not corrected; the record still names the provisioning caller');
}
if (!derived.resources.observerPrincipal.startsWith('serviceAccount:')) {
fail('the corrected observerPrincipal is not a service account');
}
if (derived.principalProjection?.correction?.classification !== PRINCIPAL_CORRECTION.classification) {
fail('the derived record does not carry the adjudicated classification for the correction');
}

/* -- 3. the capture package still describes this run, and these frames ----- */

if (pkg.filmedRunId !== derived.correlationId) {
fail(`capture package names run ${pkg.filmedRunId}, the record names ${derived.correlationId}`);
}
if (pkg.receiptId !== derived.receiptId) fail('capture package receipt id disagrees with the record');
if (pkg.runtimeSourceSha !== derived.commitSha) fail('capture package runtime source SHA disagrees with the record');
if (pkg.externalCallerPrincipal !== derived.resources.observerPrincipal) {
fail('capture package and record disagree about who performed the observation');
}

for (const frame of pkg.frames) {
const path = join(root, 'experiments', 'hac-324', 'frames', `scene-${frame.sceneId}.png`);
if (!existsSync(path)) {
fail(`frame for scene ${frame.sceneId} is missing on disk`);
continue;
}
const digest = createHash('sha256').update(readFileSync(path)).digest('hex');
if (digest !== frame.sha256) fail(`frame ${frame.sceneId} does not match its recorded sha256`);
if (!frame.qualityPass) fail(`frame ${frame.sceneId} is recorded as failing the capture-quality floor`);
}

if (teardown.status !== 'completed') fail('teardown evidence is not complete');

if (errors.length) {
process.stderr.write(`HAC-324 filmed-run record violated:\n${errors.map((e) => ` - ${e}`).join('\n')}\n`);

Check warning on line 103 in experiments/hac-324/bin/verify-filmed-run.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not use nested template literals.

See more on https://sonarcloud.io/project/issues?id=Marcelle-Labs_interlock&issues=AaAxl55yoib3IMj3kGVS&open=AaAxl55yoib3IMj3kGVS&pullRequest=35
process.exit(1);
}

process.stdout.write(
'HAC-324 filmed-run record verified\n'
+ ` run ${derived.correlationId}, receipt ${derived.receiptId}\n`
+ ` ${EXECUTION_FACTS.length} execution facts byte-identical to the emitted packet\n`
+ ` observer ${derived.resources.observerPrincipal}\n`
+ ` operator ${derived.resources.operatorPrincipal}\n`
+ ` ${pkg.frames.length} frames match their digests, all quality-PASS; teardown ${teardown.status}\n`,
);
7 changes: 4 additions & 3 deletions experiments/hac-324/evidence/capture-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
"allFramesPassQuality": true,
"knownDiscrepancies": [
{
"field": "filmed-run.json \u2192 resources.observerPrincipal",
"field": "filmed-run.raw.json \u2192 resources.observerPrincipal",
"recorded": "user:qwynn@marcellelabs.io",
"actual": "serviceAccount:interlock-hac340-observer@interlock-film-260823.iam.gserviceaccount.com",
"why": "10-provision.sh records the caller from `gcloud config get-value account` at provision time. The traversal authenticated by impersonating the dedicated keyless observer SA, because gcloud 580 refuses audience-scoped ID tokens for user accounts. The packet field was not edited \u2014 evidence is recorded as emitted, and the correction lives here.",
"why": "Corrected at the producing layer. The emitted packet is preserved verbatim as filmed-run.raw.json; experiments/hac-324/bin/build-filmed-run.mjs derives filmed-run.json, splitting the conflated field into operatorPrincipal (who provisioned) and observerPrincipal (who observed). experiments/hac-324/bin/verify-filmed-run.mjs proves every execution fact is byte-identical between the two.",
"classification": "NON_MATERIAL \u2014 adjudicated principal-type substitution; controls returned identical 403/401/403"
},
{
Expand All @@ -99,5 +99,6 @@
"deletedAt": "2026-08-24T01:50:43.559Z",
"projectLifecycleState": "DELETE_REQUESTED"
},
"packetVerification": "HAC-340 packet verified \u2014 all 19 assertions including teardown"
"packetVerification": "HAC-340 packet verified \u2014 all 19 assertions including teardown",
"operatorPrincipal": "user:qwynn@marcellelabs.io"
}
20 changes: 17 additions & 3 deletions experiments/hac-324/evidence/filmed-run.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
"agentServiceAccount": "serviceAccount:interlock-hac340-agent@interlock-film-260823.iam.gserviceaccount.com",
"proxyServiceAccount": "serviceAccount:interlock-hac340-proxy@interlock-film-260823.iam.gserviceaccount.com",
"targetServiceAccount": "serviceAccount:interlock-hac340-target@interlock-film-260823.iam.gserviceaccount.com",
"observerPrincipal": "user:qwynn@marcellelabs.io",
"observerPrincipal": "serviceAccount:interlock-hac340-observer@interlock-film-260823.iam.gserviceaccount.com",
"nodeImage": "us-central1-docker.pkg.dev/interlock-film-260823/interlock-hac340/interlock-node:ae6d0d3",
"agentImage": "us-central1-docker.pkg.dev/interlock-film-260823/interlock-hac340/interlock-adk:ae6d0d3"
"agentImage": "us-central1-docker.pkg.dev/interlock-film-260823/interlock-hac340/interlock-adk:ae6d0d3",
"operatorPrincipal": "user:qwynn@marcellelabs.io"
},
"correlationId": "ilk-hac340-cloud-1787536029323",
"decision": "ALLOW",
Expand Down Expand Up @@ -107,5 +108,18 @@
"wrongAudienceStatus": 401,
"directBypassStatus": 403
},
"teardown": "pending"
"teardown": "pending",
"principalProjection": {
"note": "observerPrincipal in the emitted packet named the provisioning caller. It is corrected here and the original value is preserved as operatorPrincipal. filmed-run.raw.json holds the emitted bytes unchanged.",
"correction": {
"field": "resources.observerPrincipal",
"rawValue": "user:qwynn@marcellelabs.io",
"reclassifiedAs": "resources.operatorPrincipal",
"correctedValue": "serviceAccount:interlock-hac340-observer@interlock-film-260823.iam.gserviceaccount.com",
"reason": "10-provision.sh records the provisioning caller, not the principal that performed the read-back. The traversal impersonated a dedicated keyless observer service account because gcloud 580 refuses audience-scoped identity tokens for user accounts. Adjudicated NON_MATERIAL: the substitution changed no authorization behaviour, and the fail-closed controls returned an identical 403/401/403.",
"classification": "NON_MATERIAL"
},
"rawPacket": "experiments/hac-324/evidence/filmed-run.raw.json",
"producer": "experiments/hac-324/bin/build-filmed-run.mjs"
}
}
Loading
Loading