diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index bed22bc..74b4b62 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2754,3 +2754,44 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Candidate source B2 authenticated-attempt spec correction (2026-10-01) + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-candidate-bootstrap-one-shot + given: "A CheckService-issued opaque registered-plan handle" + when: "A foreign, cloned, expired, replayed, or already-closed handle is used" + then: "No FD3 admission opens; the only accepted acknowledgement is the exact FD4 binding frame and it closes the original linear handle" + - id: SCN-candidate-input-consumption + given: "A frozen, bounded product snapshot and a fixed Node E binding" + when: "The original product bytes change before execution" + then: "An actual spawned child reads the frozen bytes; post-capture drift refuses before spawn" + - id: SCN-candidate-execution-boundary + given: "An opaque attempt with fixed command, arguments, explicit environment, and output cap" + when: "The child exits zero after exceeding the cap" + then: "The parent records overflow as non-success without accepting an injected executor or caller-selected limit" + - id: SCN-candidate-fence-closure + given: "A closed opaque attempt" + when: "The backend has not recorded namespace identity, FD closure, and descendant reaping" + then: "Terminal proof is UNKNOWN; fabricated observations and cloned handles cannot create proof" + observable_outcomes: + - "Only the exact FD4 acknowledgement closes a one-shot opaque admission" + - "Child stdout proves frozen-byte consumption, not same-process test reads" + - "Zero exit plus output overflow is never success" + - "Missing backend fence evidence is never TerminalProof" + determinism_controls: + - "Temporary source trees, fixed absolute Node runtime, explicit empty child environment, bounded streams" + - "No injected executor, caller-provided command, root, environment, limit, or terminal observation" + anti_cheat_rationale: + prevents: + - "Arbitrary tuple issuance or a fabricated bootstrap literal" + - "Boolean caller observations minting terminal proof" + - "Cloned or reused handles reopening an admission" + - "Same-process snapshot reads impersonating child consumption" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/checks/candidate-execution-fence.mjs b/src/checks/candidate-execution-fence.mjs index 4990a2f..6bd72ab 100644 --- a/src/checks/candidate-execution-fence.mjs +++ b/src/checks/candidate-execution-fence.mjs @@ -1,6 +1,33 @@ import { TorchError } from '../kernel/errors.mjs'; -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; -export function createCandidateExecutionFenceV2({ bootstrap, backend = 'private-pid1-namespace' } = {}) { if (!bootstrap?.transport?.closesBeforeCandidateSpawn || backend !== 'private-pid1-namespace') fail('Candidate fence has no supported private bootstrap', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ schema: 'torch.dev/candidate-execution-fence/v2alpha1', backend, nonce: bootstrap.nonce, closed: false }); } -/** A direct exit or empty PGID is deliberately insufficient: missing observed PID1/reap proof is UNKNOWN. */ -export function observeCandidateExecutionFenceV2({ fence, observation = {} } = {}) { if (!fence || fence.closed) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); const closed = observation.privatePid1 === true && observation.namespaceObserved === true && observation.descendantsReaped === true && observation.admissionFdsClosed === true; return Object.freeze({ schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: closed ? 'terminal' : 'unknown', terminalProof: closed, reason: closed ? null : 'descendant-closure-unobserved', observation: Object.freeze({ privatePid1: observation.privatePid1 === true, namespaceObserved: observation.namespaceObserved === true, descendantsReaped: observation.descendantsReaped === true, admissionFdsClosed: observation.admissionFdsClosed === true }) }); } -export function closeCandidateExecutionFenceV2(fence) { if (!fence || fence.closed) fail('Candidate fence is already closed', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ ...fence, closed: true }); } + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +/** + * Fence observations are owned by the backend, never submitted by the caller. + * Until the backend has recorded a PID1 namespace identity, FD closure, and + * namespace-wide reaping, every attempt is UNKNOWN. A process-group exit never + * creates TerminalProof. + */ +export function createCandidateExecutionFenceRuntimeV2({ stateForAttempt, backendObservation } = {}) { + if (typeof stateForAttempt !== 'function' || typeof backendObservation !== 'function') throw new TypeError('candidate fence runtime is incomplete'); + const closed = new WeakSet(); + function close(attempt) { + if (!stateForAttempt(attempt) || closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); + closed.add(attempt); + } + function terminal(attempt) { + if (!stateForAttempt(attempt) || !closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); + const observed = backendObservation(attempt); + const qualified = observed?.backend === 'private-pid1-namespace' + && Number.isSafeInteger(observed.namespacePid) && observed.namespacePid > 0 + && typeof observed.namespaceIdentity === 'string' && observed.namespaceIdentity.length > 0 + && observed.admissionFdsClosed === true && observed.descendantsReaped === true && observed.escapedDescendants === false; + return Object.freeze({ + schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: qualified ? 'terminal' : 'unknown', terminalProof: qualified, + reason: qualified ? null : 'descendant-closure-unobserved', + }); + } + return Object.freeze({ close, terminal }); +} diff --git a/src/checks/candidate-input-snapshot.mjs b/src/checks/candidate-input-snapshot.mjs index 20f2fe8..e5dd562 100644 --- a/src/checks/candidate-input-snapshot.mjs +++ b/src/checks/candidate-input-snapshot.mjs @@ -49,7 +49,7 @@ function hashRegularFile(path, expected, limits) { } finally { if (fd !== undefined) closeSync(fd); } } /** Private caller seam: callers obtain inputRoot from the returned opaque snapshot, never from a policy selector. */ -export function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) { +function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) { const limits = normalizeLimits(requestedLimits); if (typeof sourceRoot !== 'string' || typeof destinationRoot !== 'string' || !Array.isArray(paths)) fail('Candidate snapshot request is invalid', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); const source = resolve(sourceRoot); const destination = resolve(destinationRoot); if (source === destination || !relative(source, destination).startsWith('..')) fail('Candidate snapshot destination must be outside source', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); const listed = files(source, paths, limits); mkdirSync(destination, { recursive: false, mode: 0o700 }); let totalBytes = 0; const records = []; @@ -57,7 +57,7 @@ export function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, p const manifest = Object.freeze({ schema: 'torch.dev/candidate-input-manifest/v2alpha1', id, files: Object.freeze(records), totalBytes, digest: createHash('sha256').update(JSON.stringify(records)).digest('hex') }); return Object.freeze({ schema: 'torch.dev/candidate-input-snapshot/v2alpha1', inputRoot: destination, manifest, limits }); } -export function verifyCandidateInputSnapshotV2(snapshot = {}) { +function verifyCandidateInputSnapshotV2(snapshot = {}) { if (snapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || !snapshot.manifest?.files || typeof snapshot.inputRoot !== 'string') fail('Candidate snapshot is malformed', 'CANDIDATE_SNAPSHOT_INVALID'); const listed = files(snapshot.inputRoot, snapshot.manifest.files.map((file) => file.path), snapshot.limits); const observed = listed.map((file) => hashRegularFile(join(snapshot.inputRoot, file.path), { ...file, bytes: file.size }, snapshot.limits)); @@ -67,3 +67,30 @@ export function verifyCandidateInputSnapshotV2(snapshot = {}) { } return Object.freeze({ verified: true, digest: snapshot.manifest.digest, files: observed.length, totalBytes: snapshot.manifest.totalBytes }); } + +/** + * The public boundary accepts only a service-issued attempt. Source roots, + * paths, destinations, and S limits live in the service's private state. + */ +export function createCandidateInputSnapshotRuntimeV2({ stateForAttempt } = {}) { + if (typeof stateForAttempt !== 'function') throw new TypeError('stateForAttempt is required'); + const snapshots = new WeakMap(); + function capture(attempt) { + const state = stateForAttempt(attempt); + if (!state?.input) fail('Candidate attempt has no frozen input authority', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + const snapshot = captureCandidateInputSnapshotV2(state.input); + snapshots.set(attempt, snapshot); + return Object.freeze({ digest: snapshot.manifest.digest, files: snapshot.manifest.files.length, totalBytes: snapshot.manifest.totalBytes }); + } + function verify(attempt) { + const snapshot = snapshots.get(attempt); + if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + return verifyCandidateInputSnapshotV2(snapshot); + } + function cwd(attempt) { + const snapshot = snapshots.get(attempt); + if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + return snapshot.inputRoot; + } + return Object.freeze({ capture, verify, cwd }); +} diff --git a/src/checks/candidate-source-bootstrap.mjs b/src/checks/candidate-source-bootstrap.mjs index ccf0af2..02c5c64 100644 --- a/src/checks/candidate-source-bootstrap.mjs +++ b/src/checks/candidate-source-bootstrap.mjs @@ -1,16 +1,81 @@ -import { randomBytes } from 'node:crypto'; +import { createHash, randomBytes } from 'node:crypto'; import { TorchError } from '../kernel/errors.mjs'; -const live = new WeakSet(); const used = new Set(); -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; -/** CheckService-only issuer. The returned object is intentionally not serializable and carries no DB/adapter/writer capability. */ -export function issueCandidateNativeAdmissionV2({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt } = {}) { - for (const [key, value] of Object.entries({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt })) if (typeof value !== 'string' || !value) fail('Candidate native admission is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key }); - const admission = Object.freeze({ schema: 'torch.dev/candidate-native-admission/v2alpha1', projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt, nonce: randomBytes(32).toString('base64url') }); live.add(admission); return admission; +const ADMISSION_SCHEMA = 'torch.dev/candidate-source-parent-admission/v2alpha1'; +const ACK_SCHEMA = 'torch.dev/candidate-source-parent-binding/v2alpha1'; +const SHA256 = /^[a-f0-9]{64}$/; +const MAX_FRAME_BYTES = 64 * 1024; + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); } -export function openCandidateSourceBootstrapV2(admission, { now = () => new Date() } = {}) { - if (!live.has(admission) || used.has(admission.nonce)) fail('Candidate native admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); - if (Number.isNaN(Date.parse(admission.expiresAt)) || Date.parse(admission.expiresAt) <= now().getTime()) fail('Candidate native admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED'); - used.add(admission.nonce); live.delete(admission); - return Object.freeze({ schema: 'torch.dev/candidate-bootstrap/v2alpha1', nonce: admission.nonce, runId: admission.runId, generation: admission.generation, policyDigest: admission.policyDigest, engineDigest: admission.engineDigest, expiresAt: admission.expiresAt, transport: Object.freeze({ inputFd: 3, acknowledgementFd: 4, closesBeforeCandidateSpawn: true }) }); + +function digest(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +function validString(value) { + return typeof value === 'string' && value.length > 0; +} + +/** + * Service-only closure factory. It has no public issuer that accepts caller + * project/check/run tuples: CheckService supplies an opaque registered-plan + * handle to stateForPlan and this module serializes only that captured state. + */ +export function createCandidateSourceBootstrapRuntimeV2({ stateForPlan, now = () => new Date() } = {}) { + if (typeof stateForPlan !== 'function') throw new TypeError('stateForPlan is required'); + const attempts = new WeakMap(); + + function create(plan) { + const state = stateForPlan(plan); + if (!state || typeof state !== 'object') fail('Registered candidate plan is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REQUIRED'); + const required = ['admissionId', 'leaseId', 'checkId', 'policyBytes', 'policyDigest', 'engine', 'subject', 'artifactManifest', 'expiresAt']; + for (const key of required) if (!Object.hasOwn(state, key)) fail('Registered candidate plan is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key }); + if (!Buffer.isBuffer(state.policyBytes) || state.policyBytes.length === 0 || state.policyBytes.length > 1024 * 1024 + || !SHA256.test(state.policyDigest) || digest(state.policyBytes) !== state.policyDigest + || !validString(state.admissionId) || !validString(state.leaseId) || !validString(state.checkId) + || !Number.isSafeInteger(state.runGeneration) || !Number.isSafeInteger(state.guardGeneration) + || Number.isNaN(Date.parse(state.expiresAt))) fail('Registered candidate plan is invalid', 'CANDIDATE_NATIVE_ADMISSION_INVALID'); + const handle = Object.freeze({}); + attempts.set(handle, { state, used: false, closed: false, nonce: randomBytes(32).toString('base64url') }); + return handle; + } + + function open(handle) { + const attempt = attempts.get(handle); + if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + if (Date.parse(attempt.state.expiresAt) <= now().getTime()) { + attempt.closed = true; + fail('Candidate admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED'); + } + attempt.used = true; + const { state } = attempt; + const frame = Buffer.from(JSON.stringify({ + schema: ADMISSION_SCHEMA, version: 2, admissionId: state.admissionId, leaseId: state.leaseId, + checkId: state.checkId, runGeneration: state.runGeneration, guardGeneration: state.guardGeneration, + policyBytesBase64: state.policyBytes.toString('base64'), policyDigest: state.policyDigest, + engine: state.engine, subject: state.subject, artifactManifest: state.artifactManifest, + receiptAdapterAdmission: null, + }), 'utf8'); + if (frame.length > MAX_FRAME_BYTES) fail('Candidate parent admission frame exceeds its bound', 'CANDIDATE_NATIVE_ADMISSION_INVALID'); + return Object.freeze({ frame, acknowledgement: Buffer.from(`${ACK_SCHEMA}\n${state.admissionId}\n${state.policyDigest}\n`, 'utf8'), nonce: attempt.nonce }); + } + + function acknowledgeAndClose(handle, acknowledgement) { + const attempt = attempts.get(handle); + if (!attempt || !attempt.used || attempt.closed || !Buffer.isBuffer(acknowledgement)) fail('Candidate acknowledgement is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + const expected = Buffer.from(`${ACK_SCHEMA}\n${attempt.state.admissionId}\n${attempt.state.policyDigest}\n`, 'utf8'); + if (!acknowledgement.equals(expected)) fail('Candidate parent acknowledgement mismatches admission', 'CANDIDATE_PARENT_ACK_MISMATCH'); + attempt.closed = true; + return Object.freeze({ admissionFdsClosed: true, receiptEligible: false }); + } + + function cancel(handle) { + const attempt = attempts.get(handle); + if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + attempt.closed = true; + } + + return Object.freeze({ create, open, acknowledgeAndClose, cancel }); } diff --git a/src/checks/candidate-source-execution.mjs b/src/checks/candidate-source-execution.mjs index 30151d8..5f52511 100644 --- a/src/checks/candidate-source-execution.mjs +++ b/src/checks/candidate-source-execution.mjs @@ -1,15 +1,61 @@ import { spawnSync } from 'node:child_process'; -import { isAbsolute } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; -import { verifyCandidateInputSnapshotV2 } from './candidate-input-snapshot.mjs'; -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; + export const CANDIDATE_EXECUTION_LIMITS_V2 = Object.freeze({ timeoutMs: 300000, maxOutputBytes: 1048576, maxArgs: 64, maxArgBytes: 4096 }); -function bound(value, limit, name) { if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > limit) fail('Candidate execution value is invalid', 'CANDIDATE_EXECUTION_INVALID', { name }); return value; } -export function runCandidateSourceExecutionV2({ command, args = [], inputSnapshot, environment = {}, executor = spawnSync, limits = CANDIDATE_EXECUTION_LIMITS_V2 } = {}) { - if (!isAbsolute(command) || inputSnapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || typeof executor !== 'function' || !Array.isArray(args) || args.length > limits.maxArgs || Object.keys(environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) fail('Candidate execution request is invalid', 'CANDIDATE_EXECUTION_INVALID'); - verifyCandidateInputSnapshotV2(inputSnapshot); - const safeArgs = args.map((arg) => bound(arg, limits.maxArgBytes, 'arg')); const env = Object.freeze(Object.fromEntries(Object.entries(environment).sort())); - let raw; try { raw = executor(command, safeArgs, { cwd: inputSnapshot.inputRoot, env, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: limits.timeoutMs, maxBuffer: limits.maxOutputBytes, killSignal: 'SIGTERM' }); } catch (error) { raw = { status: null, error }; } - const stdout = Buffer.from(raw?.stdout ?? ''); const stderr = Buffer.from(raw?.stderr ?? ''); const overflow = stdout.length > limits.maxOutputBytes || stderr.length > limits.maxOutputBytes || raw?.error?.code === 'ENOBUFS'; const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true; - return Object.freeze({ schema: 'torch.dev/candidate-source-execution/v2alpha1', terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }), output: Object.freeze({ stdout: stdout.subarray(0, limits.maxOutputBytes), stderr: stderr.subarray(0, limits.maxOutputBytes) }) }); + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +function validRuntime(state) { + const runtime = state?.runtime; + if (!runtime || typeof runtime.command !== 'string' || !runtime.command.startsWith('/') || !Array.isArray(runtime.args) + || !runtime.args.every((arg) => typeof arg === 'string' && Buffer.byteLength(arg, 'utf8') <= CANDIDATE_EXECUTION_LIMITS_V2.maxArgBytes) + || runtime.args.length > CANDIDATE_EXECUTION_LIMITS_V2.maxArgs + || !runtime.environment || Object.keys(runtime.environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) { + fail('Candidate fixed runtime is invalid', 'CANDIDATE_EXECUTION_RUNTIME_INVALID'); + } + return runtime; +} + +/** + * Runs a fixed, CheckService-derived E binding. `run` takes only an opaque + * attempt; command, arguments, environment, executor, cwd and limits cannot + * be selected by callers. It is raw terminal evidence, never a receipt PASS. + */ +export function createCandidateSourceExecutionRuntimeV2({ stateForAttempt, snapshotRuntime } = {}) { + if (typeof stateForAttempt !== 'function' || !snapshotRuntime) throw new TypeError('candidate execution runtime is incomplete'); + const results = new WeakMap(); + function run(attempt) { + const state = stateForAttempt(attempt); + const runtime = validRuntime(state); + snapshotRuntime.verify(attempt); + const cwd = snapshotRuntime.cwd(attempt); + let raw; + try { + raw = spawnSync(runtime.command, runtime.args, { + cwd, env: Object.freeze({ ...runtime.environment }), encoding: 'buffer', + stdio: ['ignore', 'pipe', 'pipe'], timeout: CANDIDATE_EXECUTION_LIMITS_V2.timeoutMs, + maxBuffer: CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes, killSignal: 'SIGTERM', + }); + } catch (error) { raw = { status: null, error }; } + const stdout = Buffer.from(raw?.stdout ?? ''); + const stderr = Buffer.from(raw?.stderr ?? ''); + const overflow = stdout.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || stderr.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || raw?.error?.code === 'ENOBUFS'; + const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true; + const observation = Object.freeze({ + schema: 'torch.dev/candidate-source-execution/v2alpha1', + terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }), + output: Object.freeze({ stdout: stdout.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes), stderr: stderr.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes) }), + }); + snapshotRuntime.verify(attempt); + results.set(attempt, observation); + return observation; + } + function observation(attempt) { + const value = results.get(attempt); + if (!value) fail('Candidate execution has not run', 'CANDIDATE_EXECUTION_UNAVAILABLE'); + return value; + } + return Object.freeze({ run, observation }); } diff --git a/test/fleet/candidate-execution-fence.test.mjs b/test/fleet/candidate-execution-fence.test.mjs index 7b7590c..00d6ad1 100644 --- a/test/fleet/candidate-execution-fence.test.mjs +++ b/test/fleet/candidate-execution-fence.test.mjs @@ -1,5 +1,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { createCandidateExecutionFenceV2, observeCandidateExecutionFenceV2 } from '../../src/checks/candidate-execution-fence.mjs'; -const bootstrap = { nonce: 'n', transport: { closesBeforeCandidateSpawn: true } }; -test('SCN-candidate-fence-closure: double-fork/escaped-descendant uncertainty is UNKNOWN, never terminal proof', () => { const fence = createCandidateExecutionFenceV2({ bootstrap }); const unknown = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: false } }); assert.equal(unknown.outcome, 'unknown'); assert.equal(unknown.terminalProof, false); const terminal = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: true } }); assert.equal(terminal.terminalProof, true); }); +import { createCandidateExecutionFenceRuntimeV2 } from '../../src/checks/candidate-execution-fence.mjs'; + +test('SCN-candidate-fence-closure: a closed opaque attempt without backend namespace/reaping evidence is UNKNOWN and clones cannot be reused', () => { + const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { authenticated: true }); + const fence = createCandidateExecutionFenceRuntimeV2({ stateForAttempt: (value) => states.get(value), backendObservation: () => null }); + assert.throws(() => fence.close({}), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); + fence.close(attempt); const terminal = fence.terminal(attempt); + assert.equal(terminal.outcome, 'unknown'); assert.equal(terminal.terminalProof, false); + assert.throws(() => fence.terminal({ ...attempt }), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); + assert.throws(() => fence.close(attempt), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); +}); diff --git a/test/fleet/candidate-input-consumption.test.mjs b/test/fleet/candidate-input-consumption.test.mjs index 3aac9fb..6ab8ca0 100644 --- a/test/fleet/candidate-input-consumption.test.mjs +++ b/test/fleet/candidate-input-consumption.test.mjs @@ -1,10 +1,28 @@ import assert from 'node:assert/strict'; -import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { chmodSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2, verifyCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; -function roots() { const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured'); return { root, snapshot: `${root}-snapshot` }; } -test('SCN-candidate-input-consumption: child execution seam receives frozen cwd bytes after original source mutation', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); writeFileSync(join(root, 'src', 'input.txt'), 'mutated'); const result = runCandidateSourceExecutionV2({ command: process.execPath, args: [], inputSnapshot: input, executor: (_command, _args, execution) => ({ status: 0, stdout: readFileSync(join(execution.cwd, 'src', 'input.txt')) }) }); assert.equal(result.terminal.success, true); assert.equal(result.output.stdout.toString(), 'captured'); assert.equal(verifyCandidateInputSnapshotV2(input).verified, true); }); -test('SCN-candidate-input-drift: frozen-input mutation refuses rather than relabeling evidence', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); const file = join(input.inputRoot, 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift'); assert.throws(() => verifyCandidateInputSnapshotV2(input), { code: 'CANDIDATE_SNAPSHOT_DRIFT' }); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs'; + +function attemptWithInput() { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured'); + const states = new WeakMap(); const attempt = Object.freeze({}); + states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['src'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write(require("node:fs").readFileSync("src/input.txt"))'], environment: {} } }); + return { root, attempt, states }; +} + +test('SCN-candidate-input-consumption: an actual fixed Node child reads frozen bytes after the original source mutates', () => { + const { root, attempt, states } = attemptWithInput(); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + snapshots.capture(attempt); writeFileSync(join(root, 'src', 'input.txt'), 'mutated'); + const execution = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt); + assert.equal(execution.terminal.success, true); assert.equal(execution.output.stdout.toString(), 'captured'); +}); + +test('SCN-candidate-input-drift: a post-capture mutation refuses before a real child can be launched', () => { + const { attempt, states } = attemptWithInput(); const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + snapshots.capture(attempt); const file = join(snapshots.cwd(attempt), 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift'); + assert.throws(() => createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt), { code: 'CANDIDATE_SNAPSHOT_DRIFT' }); +}); diff --git a/test/fleet/candidate-snapshot-policy.test.mjs b/test/fleet/candidate-snapshot-policy.test.mjs index 61c2352..19a8887 100644 --- a/test/fleet/candidate-snapshot-policy.test.mjs +++ b/test/fleet/candidate-snapshot-policy.test.mjs @@ -3,5 +3,12 @@ import { mkdtempSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -test('SCN-candidate-snapshot-policy: product links and S caps refuse before a snapshot is admitted', () => { const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link')); assert.throws(() => captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] }), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' }); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; + +test('SCN-candidate-snapshot-policy: an opaque service attempt still refuses product links before capture', () => { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link')); + const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] } }); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + assert.throws(() => snapshots.capture(attempt), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' }); + assert.throws(() => snapshots.capture(Object.freeze({})), { code: 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED' }); +}); diff --git a/test/fleet/candidate-source-bootstrap.test.mjs b/test/fleet/candidate-source-bootstrap.test.mjs index 95116dc..f496762 100644 --- a/test/fleet/candidate-source-bootstrap.test.mjs +++ b/test/fleet/candidate-source-bootstrap.test.mjs @@ -1,12 +1,28 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import test from 'node:test'; -import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; +import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; -const issued = (expiresAt = '2030-01-01T00:00:00.000Z') => issueCandidateNativeAdmissionV2({ projectId: 'project', areaId: 'work-integration', checkId: 'test', runId: 'run', generation: 'g1', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt }); -test('SCN-candidate-bootstrap-one-shot: only issued native admission opens once and supplies no authority handles', () => { - assert.throws(() => openCandidateSourceBootstrapV2({}), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); - const admission = issued(); const bootstrap = openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }); - assert.equal(bootstrap.transport.closesBeforeCandidateSpawn, true); assert.deepEqual(Object.keys(bootstrap).sort(), ['engineDigest', 'expiresAt', 'generation', 'nonce', 'policyDigest', 'runId', 'schema', 'transport']); - assert.equal(Object.hasOwn(bootstrap, 'controlPlane'), false); assert.throws(() => openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); +function planState(expiresAt = '2030-01-01T00:00:00.000Z') { + const policyBytes = Buffer.from('policy\n'); + return { admissionId: 'admission-1', leaseId: 'lease-1', checkId: 'test', runGeneration: 1, guardGeneration: 1, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt }; +} + +test('SCN-candidate-bootstrap-one-shot: only a service-issued opaque plan opens once and its observed FD acknowledgement closes it', () => { + const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState()); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') }); + assert.throws(() => runtime.create(Object.freeze({})), { code: 'CANDIDATE_NATIVE_ADMISSION_REQUIRED' }); + const handle = runtime.create(plan); const clone = { ...handle }; + assert.throws(() => runtime.open(clone), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); + const channel = runtime.open(handle); + assert.equal(JSON.parse(channel.frame).receiptAdapterAdmission, null); + runtime.acknowledgeAndClose(handle, channel.acknowledgement); + assert.throws(() => runtime.open(handle), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); + assert.throws(() => runtime.acknowledgeAndClose(handle, channel.acknowledgement), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); +}); + +test('SCN-candidate-bootstrap-expiry: an expired service plan refuses before a parent frame is made', () => { + const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState('2020-01-01T00:00:00.000Z')); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value) }); + assert.throws(() => runtime.open(runtime.create(plan)), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' }); }); -test('SCN-candidate-bootstrap-expiry: expired admission refuses before any child protocol exists', () => assert.throws(() => openCandidateSourceBootstrapV2(issued('2020-01-01T00:00:00.000Z')), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' })); diff --git a/test/fleet/candidate-source-execution.test.mjs b/test/fleet/candidate-source-execution.test.mjs index 11b5f0e..c3b0025 100644 --- a/test/fleet/candidate-source-execution.test.mjs +++ b/test/fleet/candidate-source-execution.test.mjs @@ -1,9 +1,16 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; -function snapshot() { const source = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(source, 'input'), 'x'); return captureCandidateInputSnapshotV2({ sourceRoot: source, destinationRoot: `${source}-snapshot`, paths: ['input'] }); } -test('SCN-candidate-execution-boundary: ambient PATH/HOME fallback and output overflow cannot become success', () => { const frozen = snapshot(); assert.throws(() => runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, environment: { PATH: '/bin' } }), { code: 'CANDIDATE_EXECUTION_INVALID' }); const observed = runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, limits: { timeoutMs: 1000, maxOutputBytes: 4, maxArgs: 64, maxArgBytes: 4096 }, executor: () => ({ status: 0, stdout: Buffer.from('xxxxxxxx') }) }); assert.equal(observed.terminal.overflow, true); assert.equal(observed.terminal.success, false); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs'; + +test('SCN-candidate-execution-boundary: only fixed E is run and actual zero-exit output overflow is not success', () => { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(root, 'input'), 'x'); + const states = new WeakMap(); const attempt = Object.freeze({}); + states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['input'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write("x".repeat(1048577))'], environment: {} } }); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); snapshots.capture(attempt); + const result = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt); + assert.equal(result.terminal.overflow, true); assert.equal(result.terminal.success, false); +}); diff --git a/test/fleet/candidate-source-parent.test.mjs b/test/fleet/candidate-source-parent.test.mjs index b1c426b..bf3c7d4 100644 --- a/test/fleet/candidate-source-parent.test.mjs +++ b/test/fleet/candidate-source-parent.test.mjs @@ -1,8 +1,14 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import test from 'node:test'; -import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; -test('SCN-candidate-parent-capability: bootstrap frame carries only a bound tuple, never roots, env, writers, adapters, or receipt authority', () => { - const ticket = issueCandidateNativeAdmissionV2({ projectId: 'p', areaId: 'a', checkId: 'c', runId: 'r', generation: 'g', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt: '2030-01-01T00:00:00.000Z' }); - const frame = openCandidateSourceBootstrapV2(ticket, { now: () => new Date('2029-01-01T00:00:00.000Z') }); - for (const forbidden of ['projectId', 'areaId', 'checkId', 'inputRoot', 'cwd', 'environment', 'writer', 'adapter', 'receipt']) assert.equal(Object.hasOwn(frame, forbidden), false); +import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; + +test('SCN-candidate-parent-capability: the private FD3 record carries M only and reserves A as null', () => { + const policyBytes = Buffer.from('policy\n'); const states = new WeakMap(); const plan = Object.freeze({}); + states.set(plan, { admissionId: 'admission-2', leaseId: 'lease-2', checkId: 'test', runGeneration: 1, guardGeneration: 2, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt: '2030-01-01T00:00:00.000Z' }); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') }); + const frame = JSON.parse(runtime.open(runtime.create(plan)).frame); + for (const forbidden of ['root', 'cwd', 'environment', 'writer', 'adapter', 'receipt', 'command', 'executor']) assert.equal(Object.hasOwn(frame, forbidden), false); + assert.equal(frame.receiptAdapterAdmission, null); + assert.deepEqual(Object.keys(frame.artifactManifest).sort(), ['manifestSha256', 'schema']); });