mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
TASK-6e4ff870-a386-4a11-a59c-444ebc23732e: add hermetic candidate checks
This commit is contained in:
1 parent
1a2b5b0fec
commit
9946a2a3e4
5 files changed
+502
No files matched your search
@@ -2321,3 +2321,35 @@ test_integrity_note:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Candidate engine and artifact sealing Phase A (2026-10-01)
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-candidate-engine-pure-fixture
|
||||
given: Hermetic frozen fixture descriptors, bounded command data, explicit environment and a fixture artifact directory
|
||||
when: The isolated Phase-A engine observes a child execution
|
||||
then: Only bounded parent-observed terminal data and a nonpromotable SourceAttestation are returned
|
||||
- id: SCN-artifact-manifest-seal
|
||||
given: Fixed regular artifact bytes in a hermetic temporary directory
|
||||
when: The parent seals the directory
|
||||
then: Canonical sorted paths, byte lengths and SHA-256 values are write-once and verify unchanged
|
||||
observable_outcomes:
|
||||
- Raw exit, error, signal, timeout and overflow observations; zero exit plus overflow is not success
|
||||
- No receipt identity, writer capability, registered-state handle or consumer eligibility
|
||||
- Capped canonical manifests and refusals for links, traversal, special files, overwrite and post-seal drift
|
||||
determinism_controls:
|
||||
- Fixed bytes, injected executors and isolated temporary directories
|
||||
- No retries, sleeps, network, registered control-plane state or provider process
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- Hard-coded PASS or fixture receipt promotion
|
||||
- Ambient environment or registered-state capability leakage
|
||||
- Snapshot/golden rubber-stamping and unbounded output masking
|
||||
- Link, traversal, overwrite or post-seal artifact substitution
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const ARTIFACT_MANIFEST_FILE = '.torch-artifact-manifest-v1.json';
|
||||
export const ARTIFACT_MANIFEST_LIMITS = Object.freeze({ maxFiles: 64, maxFileBytes: 1_048_576, maxTotalBytes: 8_388_608 });
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
}
|
||||
|
||||
function canonicalJson(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function digest(value) {
|
||||
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
||||
}
|
||||
|
||||
function limits(input = {}) {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) fail('Artifact limits must be an object', 'CANDIDATE_ARTIFACT_LIMIT_INVALID');
|
||||
const result = {};
|
||||
for (const [key, maximum] of Object.entries(ARTIFACT_MANIFEST_LIMITS)) {
|
||||
const value = input[key] ?? maximum;
|
||||
if (!Number.isInteger(value) || value < 1 || value > maximum) fail('Artifact limit is invalid', 'CANDIDATE_ARTIFACT_LIMIT_INVALID', { key, value, maximum });
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function assertArtifactRelativePath(path) {
|
||||
if (typeof path !== 'string' || !path || isAbsolute(path) || path.includes('\\')
|
||||
|| path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
||||
fail('Artifact path is not canonical relative evidence', 'CANDIDATE_ARTIFACT_PATH_INVALID', { path });
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function artifactNodeKind(stat, path) {
|
||||
if (stat.isSymbolicLink()) fail('Artifact cannot be a link', 'CANDIDATE_ARTIFACT_LINK_REJECTED', { path });
|
||||
if (stat.isDirectory()) return 'directory';
|
||||
if (stat.isFile()) return 'file';
|
||||
fail('Artifact must be a regular file or directory', 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED', { path });
|
||||
}
|
||||
|
||||
function inventory(root, configuredLimits) {
|
||||
const files = [];
|
||||
let totalBytes = 0;
|
||||
function visit(current, parent = '') {
|
||||
for (const name of readdirSync(current).sort()) {
|
||||
if (!parent && name === ARTIFACT_MANIFEST_FILE) continue;
|
||||
const path = parent ? `${parent}/${name}` : name;
|
||||
assertArtifactRelativePath(path);
|
||||
const absolute = join(current, name);
|
||||
const stat = lstatSync(absolute);
|
||||
const kind = artifactNodeKind(stat, path);
|
||||
if (kind === 'directory') visit(absolute, path);
|
||||
else {
|
||||
if (stat.size > configuredLimits.maxFileBytes) fail('Artifact exceeds per-file limit', 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED', { path });
|
||||
totalBytes += stat.size;
|
||||
if (files.length + 1 > configuredLimits.maxFiles || totalBytes > configuredLimits.maxTotalBytes) {
|
||||
fail('Artifact manifest exceeds aggregate limits', 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED');
|
||||
}
|
||||
const bytes = readFileSync(absolute);
|
||||
files.push({ path, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') });
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(root);
|
||||
return { files: files.sort((left, right) => left.path.localeCompare(right.path)), totalBytes };
|
||||
}
|
||||
|
||||
function validateManifest(manifest, configuredLimits) {
|
||||
if (!manifest || manifest.schema !== 'torch.dev/artifact-manifest/v1alpha1' || !Array.isArray(manifest.files)
|
||||
|| !Number.isInteger(manifest.totalBytes) || typeof manifest.sealDigest !== 'string') {
|
||||
fail('Artifact manifest is malformed', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
}
|
||||
const seen = new Set();
|
||||
let totalBytes = 0;
|
||||
for (const record of manifest.files) {
|
||||
assertArtifactRelativePath(record?.path);
|
||||
if (seen.has(record.path)) fail('Artifact manifest contains duplicate paths', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
seen.add(record.path);
|
||||
if (!Number.isInteger(record.bytes) || record.bytes < 0 || record.bytes > configuredLimits.maxFileBytes
|
||||
|| !/^[0-9a-f]{64}$/.test(record.sha256 ?? '')) fail('Artifact manifest record is invalid', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
totalBytes += record.bytes;
|
||||
}
|
||||
if (manifest.files.length > configuredLimits.maxFiles || totalBytes !== manifest.totalBytes || totalBytes > configuredLimits.maxTotalBytes) {
|
||||
fail('Artifact manifest limits do not match', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
}
|
||||
const sorted = [...manifest.files].sort((left, right) => left.path.localeCompare(right.path));
|
||||
if (canonicalJson(sorted) !== canonicalJson(manifest.files)) fail('Artifact manifest paths are not canonical', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
const payload = { schema: manifest.schema, files: manifest.files, totalBytes: manifest.totalBytes };
|
||||
if (digest(payload) !== manifest.sealDigest) fail('Artifact manifest seal digest is invalid', 'CANDIDATE_ARTIFACT_SEAL_INVALID');
|
||||
return payload;
|
||||
}
|
||||
|
||||
function ownedRoot(directory) {
|
||||
if (typeof directory !== 'string' || !directory || !existsSync(directory)) fail('Artifact directory is missing', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
const root = realpathSync(directory);
|
||||
if (!lstatSync(root).isDirectory()) fail('Artifact directory is not a directory', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
return root;
|
||||
}
|
||||
|
||||
export function sealArtifactManifest({ directory, limits: requestedLimits } = {}) {
|
||||
const root = ownedRoot(directory);
|
||||
const configuredLimits = limits(requestedLimits);
|
||||
const path = join(root, ARTIFACT_MANIFEST_FILE);
|
||||
if (existsSync(path)) fail('Artifact manifest is already sealed', 'CANDIDATE_ARTIFACT_ALREADY_SEALED');
|
||||
const records = inventory(root, configuredLimits);
|
||||
const payload = { schema: 'torch.dev/artifact-manifest/v1alpha1', ...records };
|
||||
const manifest = { ...payload, sealDigest: digest(payload) };
|
||||
writeFileSync(path, `${canonicalJson(manifest)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||
return Object.freeze({ ...manifest, manifestPath: path });
|
||||
}
|
||||
|
||||
export function verifyArtifactManifest({ directory, limits: requestedLimits } = {}) {
|
||||
const root = ownedRoot(directory);
|
||||
const configuredLimits = limits(requestedLimits);
|
||||
const path = join(root, ARTIFACT_MANIFEST_FILE);
|
||||
if (!existsSync(path)) fail('Artifact manifest is missing', 'CANDIDATE_ARTIFACT_MANIFEST_MISSING');
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(readFileSync(path, 'utf8')); }
|
||||
catch { fail('Artifact manifest cannot be parsed', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID'); }
|
||||
const payload = validateManifest(manifest, configuredLimits);
|
||||
const current = inventory(root, configuredLimits);
|
||||
if (canonicalJson(current) !== canonicalJson({ files: payload.files, totalBytes: payload.totalBytes })) {
|
||||
fail('Artifact contents changed after sealing', 'CANDIDATE_ARTIFACT_SEAL_DRIFT');
|
||||
}
|
||||
if (resolve(path) !== join(root, ARTIFACT_MANIFEST_FILE) || relative(root, path).startsWith('..')) {
|
||||
fail('Artifact manifest escaped its directory', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
}
|
||||
return Object.freeze({ ...manifest, manifestPath: path, verified: true });
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const CANDIDATE_ENGINE_LIMITS = Object.freeze({
|
||||
maxInputBytes: 1_048_576,
|
||||
maxDefinitionBytes: 65_536,
|
||||
maxArgs: 64,
|
||||
maxArgBytes: 4_096,
|
||||
maxEnvironmentEntries: 32,
|
||||
maxEnvironmentNameBytes: 128,
|
||||
maxEnvironmentValueBytes: 4_096,
|
||||
maxOutputBytes: 65_536,
|
||||
maxArtifactPathBytes: 4_096,
|
||||
});
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
}
|
||||
|
||||
function byteLength(value) {
|
||||
return Buffer.byteLength(value, 'utf8');
|
||||
}
|
||||
|
||||
function exactObject(value, allowed, label) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
fail(`${label} must be an object`, 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowed.has(key)) fail(`${label} contains unsupported capability data`, 'CANDIDATE_ENGINE_CAPABILITY_FORBIDDEN', { key });
|
||||
}
|
||||
}
|
||||
|
||||
function boundedString(value, limit, label) {
|
||||
if (typeof value !== 'string' || !value || byteLength(value) > limit) {
|
||||
fail(`${label} is missing or exceeds its limit`, 'CANDIDATE_ENGINE_INPUT_INVALID', { label, limit });
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeLimits(limits = {}) {
|
||||
exactObject(limits, new Set(Object.keys(CANDIDATE_ENGINE_LIMITS)), 'limits');
|
||||
const normalized = {};
|
||||
for (const [key, maximum] of Object.entries(CANDIDATE_ENGINE_LIMITS)) {
|
||||
const value = limits[key] ?? maximum;
|
||||
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
||||
fail(`Invalid candidate-engine limit: ${key}`, 'CANDIDATE_ENGINE_LIMIT_INVALID', { key, value, maximum });
|
||||
}
|
||||
normalized[key] = value;
|
||||
}
|
||||
return Object.freeze(normalized);
|
||||
}
|
||||
|
||||
function freeze(value) {
|
||||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||||
for (const item of Object.values(value)) freeze(item);
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeInput(input, limits) {
|
||||
exactObject(input, new Set(['directory', 'digest', 'bytes']), 'input');
|
||||
const directory = boundedString(input.directory, limits.maxArtifactPathBytes, 'input.directory');
|
||||
const digest = boundedString(input.digest, 128, 'input.digest');
|
||||
if (!/^[0-9a-f]{64}$/i.test(digest) || !Number.isInteger(input.bytes) || input.bytes < 0 || input.bytes > limits.maxInputBytes) {
|
||||
fail('Candidate input must be a bounded immutable fixture descriptor', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
return freeze({ directory, digest: digest.toLowerCase(), bytes: input.bytes });
|
||||
}
|
||||
|
||||
function normalizeDefinition(definition, limits) {
|
||||
exactObject(definition, new Set(['command', 'args']), 'definition');
|
||||
const command = boundedString(definition.command, limits.maxArgBytes, 'definition.command');
|
||||
const args = normalizeArgs(definition.args ?? [], limits, 'definition.args');
|
||||
const normalized = { command, args };
|
||||
if (byteLength(JSON.stringify(normalized)) > limits.maxDefinitionBytes) {
|
||||
fail('Candidate definition exceeds its limit', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
return freeze(normalized);
|
||||
}
|
||||
|
||||
function normalizeArgs(args, limits, label = 'args') {
|
||||
if (!Array.isArray(args) || args.length > limits.maxArgs) {
|
||||
fail(`${label} exceeds its count limit`, 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
return freeze(args.map((value) => boundedString(value, limits.maxArgBytes, label)));
|
||||
}
|
||||
|
||||
function normalizeEnvironment(environment, limits) {
|
||||
exactObject(environment, new Set(Object.keys(environment ?? {})), 'environment');
|
||||
const entries = Object.entries(environment);
|
||||
if (entries.length > limits.maxEnvironmentEntries) {
|
||||
fail('Candidate environment exceeds its entry limit', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
const normalized = {};
|
||||
for (const [key, value] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
||||
if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || byteLength(key) > limits.maxEnvironmentNameBytes) {
|
||||
fail('Candidate environment name is invalid', 'CANDIDATE_ENGINE_INPUT_INVALID', { key });
|
||||
}
|
||||
normalized[key] = boundedString(value, limits.maxEnvironmentValueBytes, `environment.${key}`);
|
||||
}
|
||||
return freeze(normalized);
|
||||
}
|
||||
|
||||
function boundedOutput(value, limit) {
|
||||
const output = typeof value === 'string' ? value : value?.toString?.() ?? '';
|
||||
const bytes = Buffer.byteLength(output);
|
||||
return { bytes, text: bytes > limit ? output.slice(0, limit) : output, overflow: bytes > limit };
|
||||
}
|
||||
|
||||
function normalizeError(error) {
|
||||
if (!error) return null;
|
||||
return {
|
||||
code: typeof error.code === 'string' ? error.code.slice(0, 128) : null,
|
||||
message: typeof error.message === 'string' ? error.message.slice(0, 512) : String(error).slice(0, 512),
|
||||
};
|
||||
}
|
||||
|
||||
function terminalObservation(result, limits) {
|
||||
const stdout = boundedOutput(result?.stdout, limits.maxOutputBytes);
|
||||
const stderr = boundedOutput(result?.stderr, limits.maxOutputBytes);
|
||||
const error = normalizeError(result?.error);
|
||||
const signal = typeof result?.signal === 'string' ? result.signal : null;
|
||||
const exitStatus = Number.isInteger(result?.status) ? result.status : null;
|
||||
const timedOut = result?.timedOut === true || error?.code === 'ETIMEDOUT';
|
||||
const overflow = stdout.overflow || stderr.overflow || error?.code === 'ENOBUFS';
|
||||
let kind = 'exit';
|
||||
if (timedOut) kind = 'timeout';
|
||||
else if (overflow) kind = 'overflow';
|
||||
else if (signal) kind = 'signal';
|
||||
else if (error) kind = 'error';
|
||||
return freeze({
|
||||
kind, exitStatus, signal, error, timedOut, overflow,
|
||||
success: kind === 'exit' && exitStatus === 0,
|
||||
stdout, stderr,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixture-only Phase-A seam. It deliberately exposes a raw parent observation,
|
||||
* never a receipt, capability, ControlPlane handle, or registered project root.
|
||||
*/
|
||||
export function runCandidateEngine(options = {}) {
|
||||
exactObject(options, new Set(['input', 'definition', 'args', 'environment', 'artifactDirectory', 'executor', 'limits']), 'candidate engine options');
|
||||
const limits = normalizeLimits(options.limits ?? {});
|
||||
const input = normalizeInput(options.input, limits);
|
||||
const definition = normalizeDefinition(options.definition, limits);
|
||||
const args = normalizeArgs(options.args ?? [], limits);
|
||||
const environment = normalizeEnvironment(options.environment ?? {}, limits);
|
||||
const artifactDirectory = boundedString(options.artifactDirectory, limits.maxArtifactPathBytes, 'artifactDirectory');
|
||||
const executor = options.executor ?? ((command, commandArgs, execution) => spawnSync(command, commandArgs, execution));
|
||||
if (typeof executor !== 'function') fail('Candidate executor must be a function', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
|
||||
let raw;
|
||||
try {
|
||||
raw = executor(definition.command, [...definition.args, ...args], freeze({
|
||||
cwd: input.directory,
|
||||
env: environment,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: limits.maxOutputBytes,
|
||||
artifactDirectory,
|
||||
}));
|
||||
} catch (error) {
|
||||
raw = { status: null, error };
|
||||
}
|
||||
const terminal = terminalObservation(raw, limits);
|
||||
return freeze({
|
||||
schema: 'torch.dev/candidate-engine-observation/v1alpha1',
|
||||
fixtureOnly: true,
|
||||
sourceAttestation: freeze({ schema: 'torch.dev/source-attestation/v1alpha1', promotion: 'nonpromotable' }),
|
||||
input, definition, args, environment, artifactDirectory, terminal,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
ARTIFACT_MANIFEST_FILE, artifactNodeKind, assertArtifactRelativePath, sealArtifactManifest, verifyArtifactManifest,
|
||||
} from '../../src/checks/artifact-manifest.mjs';
|
||||
|
||||
function fixture() {
|
||||
return mkdtempSync(join(tmpdir(), 'torch-candidate-manifest-'));
|
||||
}
|
||||
|
||||
function write(root, path, content) {
|
||||
const target = join(root, path);
|
||||
mkdirSync(join(target, '..'), { recursive: true });
|
||||
writeFileSync(target, content);
|
||||
}
|
||||
|
||||
test('SCN-artifact-manifest-seal: regular fixture artifacts seal as canonical sorted hash and length evidence regardless of creation order', () => {
|
||||
const first = fixture();
|
||||
const second = fixture();
|
||||
write(first, 'z/proof.txt', 'zeta');
|
||||
write(first, 'a/proof.txt', 'alpha');
|
||||
write(second, 'a/proof.txt', 'alpha');
|
||||
write(second, 'z/proof.txt', 'zeta');
|
||||
const left = sealArtifactManifest({ directory: first });
|
||||
const right = sealArtifactManifest({ directory: second });
|
||||
assert.deepEqual(left.files, right.files);
|
||||
assert.equal(left.sealDigest, right.sealDigest);
|
||||
assert.deepEqual(left.files.map((file) => file.path), ['a/proof.txt', 'z/proof.txt']);
|
||||
assert.equal(left.totalBytes, 9);
|
||||
assert.equal(verifyArtifactManifest({ directory: first }).verified, true);
|
||||
assert.throws(() => sealArtifactManifest({ directory: first }), { code: 'CANDIDATE_ARTIFACT_ALREADY_SEALED' });
|
||||
});
|
||||
|
||||
test('SCN-artifact-manifest-negative-paths: traversal, links, special files, caps and duplicate manifest paths refuse', () => {
|
||||
assert.throws(() => assertArtifactRelativePath('../escape'), { code: 'CANDIDATE_ARTIFACT_PATH_INVALID' });
|
||||
assert.throws(() => assertArtifactRelativePath('/absolute'), { code: 'CANDIDATE_ARTIFACT_PATH_INVALID' });
|
||||
|
||||
const linked = fixture();
|
||||
write(linked, 'safe.txt', 'safe');
|
||||
symlinkSync('safe.txt', join(linked, 'linked.txt'));
|
||||
assert.throws(() => sealArtifactManifest({ directory: linked }), { code: 'CANDIDATE_ARTIFACT_LINK_REJECTED' });
|
||||
|
||||
const limited = fixture();
|
||||
write(limited, 'large.txt', '12345');
|
||||
assert.throws(() => sealArtifactManifest({ directory: limited, limits: { maxFileBytes: 4 } }),
|
||||
{ code: 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED' });
|
||||
|
||||
assert.throws(() => artifactNodeKind({
|
||||
isSymbolicLink: () => false, isDirectory: () => false, isFile: () => false,
|
||||
}, 'pipe'), { code: 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED' });
|
||||
|
||||
const malformed = fixture();
|
||||
write(malformed, 'proof.txt', 'proof');
|
||||
sealArtifactManifest({ directory: malformed });
|
||||
const path = join(malformed, ARTIFACT_MANIFEST_FILE);
|
||||
const record = JSON.parse(readFileSync(path, 'utf8'));
|
||||
record.files.push({ ...record.files[0] });
|
||||
writeFileSync(path, JSON.stringify(record));
|
||||
assert.throws(() => verifyArtifactManifest({ directory: malformed }), { code: 'CANDIDATE_ARTIFACT_MANIFEST_INVALID' });
|
||||
});
|
||||
|
||||
test('SCN-artifact-manifest-drift: post-seal mutation or overwrite is detected and cannot become retained evidence', () => {
|
||||
const root = fixture();
|
||||
write(root, 'proof.txt', 'before');
|
||||
sealArtifactManifest({ directory: root });
|
||||
write(root, 'proof.txt', 'after');
|
||||
assert.throws(() => verifyArtifactManifest({ directory: root }), { code: 'CANDIDATE_ARTIFACT_SEAL_DRIFT' });
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { runCandidateEngine } from '../../src/checks/candidate-engine.mjs';
|
||||
|
||||
function fixtureInput() {
|
||||
return { directory: mkdtempSync(join(tmpdir(), 'torch-candidate-engine-')), digest: 'a'.repeat(64), bytes: 12 };
|
||||
}
|
||||
|
||||
function fixtureOptions(overrides = {}) {
|
||||
return {
|
||||
input: fixtureInput(),
|
||||
definition: { command: 'fixture-command', args: ['--fixed'] },
|
||||
args: ['--bounded'],
|
||||
environment: { FIXTURE_MODE: '1', TZ: 'UTC' },
|
||||
artifactDirectory: mkdtempSync(join(tmpdir(), 'torch-candidate-artifacts-')),
|
||||
executor: () => ({ status: 0, stdout: 'fixture output', stderr: '' }),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('SCN-candidate-engine-pure-fixture: immutable bounded fixture execution returns only parent-observed nonpromotable evidence', () => {
|
||||
let invocation;
|
||||
const options = fixtureOptions({
|
||||
executor: (command, args, execution) => {
|
||||
invocation = { command, args, execution };
|
||||
return { status: 0, stdout: 'fixture output', stderr: '' };
|
||||
},
|
||||
});
|
||||
const observed = runCandidateEngine(options);
|
||||
assert.equal(observed.fixtureOnly, true);
|
||||
assert.deepEqual(observed.sourceAttestation, {
|
||||
schema: 'torch.dev/source-attestation/v1alpha1', promotion: 'nonpromotable',
|
||||
});
|
||||
assert.equal(Object.hasOwn(observed.sourceAttestation, 'receiptId'), false);
|
||||
assert.equal(observed.terminal.success, true);
|
||||
assert.equal(observed.terminal.kind, 'exit');
|
||||
assert.deepEqual(invocation.args, ['--fixed', '--bounded']);
|
||||
assert.equal(invocation.execution.cwd, options.input.directory);
|
||||
assert.deepEqual(invocation.execution.env, { FIXTURE_MODE: '1', TZ: 'UTC' });
|
||||
assert.equal(Object.isFrozen(invocation.execution), true);
|
||||
assert.equal(Object.hasOwn(invocation.execution, 'token'), false);
|
||||
assert.equal(Object.hasOwn(invocation.execution, 'receiptWriter'), false);
|
||||
assert.equal(Object.hasOwn(invocation.execution, 'controlPlane'), false);
|
||||
});
|
||||
|
||||
test('SCN-candidate-engine-limits: invalid bounded inputs, arguments, environment and artifact descriptors refuse before execution', () => {
|
||||
let executions = 0;
|
||||
const executor = () => { executions += 1; return { status: 0 }; };
|
||||
assert.throws(() => runCandidateEngine(fixtureOptions({ executor, input: { directory: '/tmp', digest: 'bad', bytes: 1 } })),
|
||||
{ code: 'CANDIDATE_ENGINE_INPUT_INVALID' });
|
||||
assert.throws(() => runCandidateEngine(fixtureOptions({ executor, args: ['x'.repeat(8)], limits: { maxArgBytes: 4 } })),
|
||||
{ code: 'CANDIDATE_ENGINE_INPUT_INVALID' });
|
||||
assert.throws(() => runCandidateEngine(fixtureOptions({ executor, environment: { bad: '1' } })),
|
||||
{ code: 'CANDIDATE_ENGINE_INPUT_INVALID' });
|
||||
assert.throws(() => runCandidateEngine(fixtureOptions({ executor, artifactDirectory: 'x'.repeat(5), limits: { maxArtifactPathBytes: 4 } })),
|
||||
{ code: 'CANDIDATE_ENGINE_INPUT_INVALID' });
|
||||
assert.equal(executions, 0, 'refusal must occur before a candidate executor starts');
|
||||
});
|
||||
|
||||
test('SCN-candidate-engine-terminal-observation: overflow, including exit zero plus ENOBUFS, is raw parent evidence and never success', () => {
|
||||
const overflow = runCandidateEngine(fixtureOptions({
|
||||
limits: { maxOutputBytes: 4 },
|
||||
executor: () => ({ status: 0, error: Object.assign(new Error('overflow'), { code: 'ENOBUFS' }), stdout: 'too many bytes' }),
|
||||
}));
|
||||
assert.equal(overflow.terminal.exitStatus, 0);
|
||||
assert.equal(overflow.terminal.kind, 'overflow');
|
||||
assert.equal(overflow.terminal.success, false);
|
||||
assert.equal(overflow.terminal.overflow, true);
|
||||
|
||||
const timeout = runCandidateEngine(fixtureOptions({
|
||||
executor: () => ({ status: null, timedOut: true, error: Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' }) }),
|
||||
}));
|
||||
assert.equal(timeout.terminal.kind, 'timeout');
|
||||
assert.equal(timeout.terminal.success, false);
|
||||
|
||||
const signal = runCandidateEngine(fixtureOptions({ executor: () => ({ status: null, signal: 'SIGTERM' }) }));
|
||||
assert.equal(signal.terminal.kind, 'signal');
|
||||
assert.equal(signal.terminal.success, false);
|
||||
});
|
||||
|
||||
test('SCN-candidate-engine-capability-boundary: registered-state and writer-shaped inputs are rejected before execution', () => {
|
||||
let executions = 0;
|
||||
const options = fixtureOptions({ executor: () => { executions += 1; return { status: 0 }; } });
|
||||
options.controlPlane = {};
|
||||
assert.throws(() => runCandidateEngine(options), { code: 'CANDIDATE_ENGINE_CAPABILITY_FORBIDDEN' });
|
||||
assert.equal(executions, 0);
|
||||
});
|
||||
Reference in new issue
Block a user