mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 21:01:54 +00:00
feat(checks): add bounded candidate source seams
TASK-6e4ff870-a386-4a11-a59c-444ebc23732e
This commit is contained in:
1 parent
316d8b7480
commit
84c78a2d37
13 files changed
+214
No files matched your search
@@ -0,0 +1,3 @@
|
||||
// Private init entrypoint. It is never a CLI surface: bootstrap is supplied only on inherited FD3 and acknowledgement only on FD4.
|
||||
import { closeSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
export function readPrivateFenceBootstrapV2() { const frame = readFileSync(3, 'utf8'); if (Buffer.byteLength(frame, 'utf8') > 65_536) throw new Error('CANDIDATE_FENCE_FRAME_TOO_LARGE'); const value = JSON.parse(frame); if (!value?.nonce || !value?.generation) throw new Error('CANDIDATE_FENCE_FRAME_INVALID'); writeFileSync(4, JSON.stringify({ nonce: value.nonce, generation: value.generation })); closeSync(3); closeSync(4); return Object.freeze({ nonce: value.nonce, generation: value.generation, admissionFdsClosed: true }); }
|
||||
@@ -0,0 +1,6 @@
|
||||
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 }); }
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, writeSync } from 'node:fs';
|
||||
import { isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const CANDIDATE_PRODUCT_INPUT_LIMITS_V2 = Object.freeze({ maxRecords: 8192, maxPathUtf8Bytes: 4096, maxFileBytes: 67_108_864, maxTotalBytes: 536_870_912, maxFrameBytes: 1_048_576, chunkBytes: 65_536 });
|
||||
const fail = (message, code, details) => { throw new TorchError(message, { code, details }); };
|
||||
const order = (a, b) => Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
|
||||
const bytes = (value) => Buffer.byteLength(value, 'utf8');
|
||||
function pathOf(value, limits) {
|
||||
if (typeof value !== 'string' || !value || isAbsolute(value) || value.includes('\\') || value.includes('\0') || bytes(value) > limits.maxPathUtf8Bytes || value.split('/').some((part) => !part || part === '.' || part === '..' || part === '.git')) fail('Candidate snapshot path is invalid', 'CANDIDATE_SNAPSHOT_PATH_INVALID', { path: value });
|
||||
return value;
|
||||
}
|
||||
function normalizeLimits(value = {}) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((key) => !Object.hasOwn(CANDIDATE_PRODUCT_INPUT_LIMITS_V2, key))) fail('Candidate snapshot limits are invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID');
|
||||
const result = {};
|
||||
for (const [key, maximum] of Object.entries(CANDIDATE_PRODUCT_INPUT_LIMITS_V2)) { const v = value[key] ?? maximum; if (!Number.isInteger(v) || v < 1 || v > maximum) fail('Candidate snapshot limit is invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID', { key, value: v }); result[key] = v; }
|
||||
return Object.freeze(result);
|
||||
}
|
||||
function regular(path) { const stat = lstatSync(path); if (stat.isSymbolicLink()) fail('Candidate snapshot rejects links', 'CANDIDATE_SNAPSHOT_LINK_REJECTED', { path }); if (!stat.isFile() && !stat.isDirectory()) fail('Candidate snapshot rejects special files', 'CANDIDATE_SNAPSHOT_SPECIAL_REJECTED', { path }); return stat; }
|
||||
function files(root, paths, limits) {
|
||||
const result = [];
|
||||
const visit = (rel) => { const absolute = join(root, rel); const stat = regular(absolute); if (stat.isDirectory()) for (const name of readdirSync(absolute).sort(order)) visit(`${rel}/${name}`); else { if (stat.size > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED', { path: rel }); result.push({ path: rel, size: stat.size, executable: Boolean(stat.mode & 0o111) }); } };
|
||||
for (const path of [...new Set(paths.map((p) => pathOf(p, limits)))].sort(order)) visit(path);
|
||||
if (!result.length || result.length > limits.maxRecords) fail('Candidate snapshot record cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED');
|
||||
return result.sort((a, b) => order(a.path, b.path));
|
||||
}
|
||||
function copyAndHash(source, destination, expected, limits) {
|
||||
let inFd; let outFd;
|
||||
try {
|
||||
inFd = openSync(source, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(inFd);
|
||||
if (!before.isFile() || before.size !== expected.size || before.size > limits.maxFileBytes) fail('Candidate snapshot source drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
outFd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, expected.executable ? 0o500 : 0o400);
|
||||
const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0;
|
||||
for (;;) { const count = readSync(inFd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); let offset = 0; while (offset < count) offset += writeSync(outFd, buffer, offset, count - offset); }
|
||||
const after = fstatSync(inFd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot source changed while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable };
|
||||
} finally { if (inFd !== undefined) closeSync(inFd); if (outFd !== undefined) closeSync(outFd); }
|
||||
}
|
||||
function hashRegularFile(path, expected, limits) {
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(fd);
|
||||
if (!before.isFile() || before.size !== expected.bytes || before.size > limits.maxFileBytes) fail('Candidate snapshot drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0;
|
||||
for (;;) { const count = readSync(fd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); }
|
||||
const after = fstatSync(fd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot drifted while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable };
|
||||
} 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() } = {}) {
|
||||
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 = [];
|
||||
for (const item of listed) { const target = join(destination, item.path); mkdirSync(resolve(target, '..'), { recursive: true, mode: 0o700 }); const record = copyAndHash(join(source, item.path), target, item, limits); totalBytes += record.bytes; if (totalBytes > limits.maxTotalBytes) fail('Candidate snapshot total cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); records.push(record); }
|
||||
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 = {}) {
|
||||
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));
|
||||
if (JSON.stringify(observed) !== JSON.stringify(snapshot.manifest.files)
|
||||
|| createHash('sha256').update(JSON.stringify(observed)).digest('hex') !== snapshot.manifest.digest) {
|
||||
fail('Candidate snapshot content drifted', 'CANDIDATE_SNAPSHOT_DRIFT');
|
||||
}
|
||||
return Object.freeze({ verified: true, digest: snapshot.manifest.digest, files: observed.length, totalBytes: snapshot.manifest.totalBytes });
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { 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;
|
||||
}
|
||||
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 }) });
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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) }) });
|
||||
}
|
||||
@@ -84,6 +84,19 @@ function captureGeneratedArtifacts({ generatedDirectory, commit, checkId, defini
|
||||
};
|
||||
}
|
||||
|
||||
// B2 admission boundary only. Kernel owns policy parsing/registration and the parent;
|
||||
// this service must never synthesize either from cwd, environment, or caller JSON.
|
||||
function assertCandidateNativeAdmissionAvailable(definition) {
|
||||
if (!Object.hasOwn(definition ?? {}, 'candidate_execution_policy')) return;
|
||||
if (!definition.candidate_execution_policy) {
|
||||
throw new TorchError('Candidate execution policy is not registered', { code: 'CANDIDATE_SNAPSHOT_POLICY_UNDECLARED' });
|
||||
}
|
||||
// A registered policy alone is not the genuine parent issuer or receipt adapter.
|
||||
throw new TorchError('Candidate native parent bootstrap is unavailable', {
|
||||
code: 'CANDIDATE_PARENT_NATIVE_BOOTSTRAP_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
|
||||
export class CheckService {
|
||||
constructor({
|
||||
repositoryRoot, controlPlane, resourceService = null,
|
||||
@@ -330,6 +343,8 @@ export class CheckService {
|
||||
if (missing.length) throw new TorchError('Prepared check requires resource leases', {
|
||||
code: 'CHECK_BLOCKED', details: missing.map((resourceId) => ({ code: 'RESOURCE_LEASE_REQUIRED', resourceId })),
|
||||
});
|
||||
// Deliberately before snapshot verification, state mutation, artifacts, or child spawn.
|
||||
assertCandidateNativeAdmissionAvailable(currentDefinition);
|
||||
verifyCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot });
|
||||
const claimed = this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'running', runner_json = ?
|
||||
WHERE id = ? AND project_id = ? AND area_id = ? AND state = 'prepared'`)
|
||||
|
||||
Reference in new issue
Block a user