mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-08 05:42:12 +00:00
TASK-6e4ff870-a386-4a11-a59c-444ebc23732e: harden candidate evidence bounds
This commit is contained in:
1 parent
9946a2a3e4
commit
b1c4c26b1b
5 files changed
+451
-52
No files matched your search
@@ -1,10 +1,18 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readSync, 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 });
|
||||
export const ARTIFACT_MANIFEST_LIMITS = Object.freeze({
|
||||
maxFiles: 64,
|
||||
maxFileBytes: 1_048_576,
|
||||
maxTotalBytes: 8_388_608,
|
||||
maxPathBytes: 4_096,
|
||||
maxManifestBytes: 65_536,
|
||||
});
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
@@ -20,6 +28,10 @@ function digest(value) {
|
||||
|
||||
function limits(input = {}) {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) fail('Artifact limits must be an object', 'CANDIDATE_ARTIFACT_LIMIT_INVALID');
|
||||
const allowed = new Set(Object.keys(ARTIFACT_MANIFEST_LIMITS));
|
||||
for (const key of Object.keys(input)) {
|
||||
if (!allowed.has(key)) fail('Artifact limit is unsupported', 'CANDIDATE_ARTIFACT_LIMIT_INVALID', { key });
|
||||
}
|
||||
const result = {};
|
||||
for (const [key, maximum] of Object.entries(ARTIFACT_MANIFEST_LIMITS)) {
|
||||
const value = input[key] ?? maximum;
|
||||
@@ -29,8 +41,13 @@ function limits(input = {}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function assertArtifactRelativePath(path) {
|
||||
function bytewisePathOrder(left, right) {
|
||||
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
|
||||
}
|
||||
|
||||
export function assertArtifactRelativePath(path, maxPathBytes = ARTIFACT_MANIFEST_LIMITS.maxPathBytes) {
|
||||
if (typeof path !== 'string' || !path || isAbsolute(path) || path.includes('\\')
|
||||
|| path.includes('\0') || Buffer.byteLength(path, 'utf8') > maxPathBytes
|
||||
|| path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
||||
fail('Artifact path is not canonical relative evidence', 'CANDIDATE_ARTIFACT_PATH_INVALID', { path });
|
||||
}
|
||||
@@ -44,14 +61,55 @@ export function artifactNodeKind(stat, path) {
|
||||
fail('Artifact must be a regular file or directory', 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED', { path });
|
||||
}
|
||||
|
||||
function lstatOrNull(path) {
|
||||
try { return lstatSync(path); }
|
||||
catch (error) {
|
||||
if (error?.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function readRegularFileCapped(path, limit, label) {
|
||||
const beforePath = lstatOrNull(path);
|
||||
if (!beforePath) fail(`${label} is missing`, 'CANDIDATE_ARTIFACT_MANIFEST_MISSING', { path });
|
||||
artifactNodeKind(beforePath, path);
|
||||
if (!beforePath.isFile()) fail(`${label} must be a regular file`, 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED', { path });
|
||||
if (beforePath.size > limit) fail(`${label} exceeds its byte limit`, 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED', { path, limit });
|
||||
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
const before = fstatSync(descriptor);
|
||||
if (!before.isFile()) fail(`${label} must be a regular file`, 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED', { path });
|
||||
if (before.size > limit) fail(`${label} exceeds its byte limit`, 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED', { path, limit });
|
||||
const buffer = Buffer.alloc(before.size + 1);
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const read = readSync(descriptor, buffer, offset, buffer.length - offset, offset);
|
||||
if (read === 0) break;
|
||||
offset += read;
|
||||
}
|
||||
const after = fstatSync(descriptor);
|
||||
if (after.size !== before.size || offset !== before.size) {
|
||||
fail(`${label} changed while being read`, 'CANDIDATE_ARTIFACT_SEAL_DRIFT', { path });
|
||||
}
|
||||
return buffer.subarray(0, before.size);
|
||||
} catch (error) {
|
||||
if (error instanceof TorchError) throw error;
|
||||
fail(`${label} cannot be read as a regular file`, 'CANDIDATE_ARTIFACT_LINK_REJECTED', { path });
|
||||
} finally {
|
||||
if (descriptor !== undefined) closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function inventory(root, configuredLimits) {
|
||||
const files = [];
|
||||
let totalBytes = 0;
|
||||
function visit(current, parent = '') {
|
||||
for (const name of readdirSync(current).sort()) {
|
||||
for (const name of readdirSync(current).sort(bytewisePathOrder)) {
|
||||
if (!parent && name === ARTIFACT_MANIFEST_FILE) continue;
|
||||
const path = parent ? `${parent}/${name}` : name;
|
||||
assertArtifactRelativePath(path);
|
||||
assertArtifactRelativePath(path, configuredLimits.maxPathBytes);
|
||||
const absolute = join(current, name);
|
||||
const stat = lstatSync(absolute);
|
||||
const kind = artifactNodeKind(stat, path);
|
||||
@@ -62,13 +120,16 @@ function inventory(root, configuredLimits) {
|
||||
if (files.length + 1 > configuredLimits.maxFiles || totalBytes > configuredLimits.maxTotalBytes) {
|
||||
fail('Artifact manifest exceeds aggregate limits', 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED');
|
||||
}
|
||||
const bytes = readFileSync(absolute);
|
||||
const bytes = readRegularFileCapped(absolute, configuredLimits.maxFileBytes, 'Artifact');
|
||||
if (bytes.length !== stat.size || bytes.length > configuredLimits.maxFileBytes) {
|
||||
fail('Artifact changed while being read', 'CANDIDATE_ARTIFACT_SEAL_DRIFT', { path });
|
||||
}
|
||||
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 };
|
||||
return { files: files.sort((left, right) => bytewisePathOrder(left.path, right.path)), totalBytes };
|
||||
}
|
||||
|
||||
function validateManifest(manifest, configuredLimits) {
|
||||
@@ -76,10 +137,18 @@ function validateManifest(manifest, configuredLimits) {
|
||||
|| !Number.isInteger(manifest.totalBytes) || typeof manifest.sealDigest !== 'string') {
|
||||
fail('Artifact manifest is malformed', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
}
|
||||
const manifestKeys = new Set(['schema', 'files', 'totalBytes', 'sealDigest']);
|
||||
for (const key of Object.keys(manifest)) {
|
||||
if (!manifestKeys.has(key)) fail('Artifact manifest contains unsupported fields', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID', { key });
|
||||
}
|
||||
const seen = new Set();
|
||||
let totalBytes = 0;
|
||||
for (const record of manifest.files) {
|
||||
assertArtifactRelativePath(record?.path);
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)
|
||||
|| Object.keys(record).some((key) => !new Set(['path', 'bytes', 'sha256']).has(key))) {
|
||||
fail('Artifact manifest record is invalid', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID');
|
||||
}
|
||||
assertArtifactRelativePath(record.path, configuredLimits.maxPathBytes);
|
||||
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
|
||||
@@ -89,7 +158,7 @@ function validateManifest(manifest, configuredLimits) {
|
||||
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));
|
||||
const sorted = [...manifest.files].sort((left, right) => bytewisePathOrder(left.path, 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');
|
||||
@@ -97,7 +166,11 @@ function validateManifest(manifest, configuredLimits) {
|
||||
}
|
||||
|
||||
function ownedRoot(directory) {
|
||||
if (typeof directory !== 'string' || !directory || !existsSync(directory)) fail('Artifact directory is missing', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
if (typeof directory !== 'string' || !directory) fail('Artifact directory is missing', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
const initial = lstatOrNull(directory);
|
||||
if (!initial) fail('Artifact directory is missing', 'CANDIDATE_ARTIFACT_DIRECTORY_INVALID');
|
||||
if (initial.isSymbolicLink()) fail('Artifact directory cannot be a link', 'CANDIDATE_ARTIFACT_LINK_REJECTED', { directory });
|
||||
if (!initial.isDirectory()) fail('Artifact directory is not a directory', '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;
|
||||
@@ -107,11 +180,15 @@ 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');
|
||||
if (lstatOrNull(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' });
|
||||
const serialized = Buffer.from(`${canonicalJson(manifest)}\n`, 'utf8');
|
||||
if (serialized.length > configuredLimits.maxManifestBytes) {
|
||||
fail('Artifact manifest exceeds its byte limit', 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED');
|
||||
}
|
||||
writeFileSync(path, serialized, { mode: 0o600, flag: 'wx' });
|
||||
return Object.freeze({ ...manifest, manifestPath: path });
|
||||
}
|
||||
|
||||
@@ -119,9 +196,15 @@ 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');
|
||||
const manifestStat = lstatOrNull(path);
|
||||
if (!manifestStat) fail('Artifact manifest is missing', 'CANDIDATE_ARTIFACT_MANIFEST_MISSING');
|
||||
artifactNodeKind(manifestStat, ARTIFACT_MANIFEST_FILE);
|
||||
if (!manifestStat.isFile()) fail('Artifact manifest must be a regular file', 'CANDIDATE_ARTIFACT_SPECIAL_REJECTED');
|
||||
if (manifestStat.size > configuredLimits.maxManifestBytes) {
|
||||
fail('Artifact manifest exceeds its byte limit', 'CANDIDATE_ARTIFACT_LIMIT_EXCEEDED');
|
||||
}
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(readFileSync(path, 'utf8')); }
|
||||
try { manifest = JSON.parse(readRegularFileCapped(path, configuredLimits.maxManifestBytes, 'Artifact manifest').toString('utf8')); }
|
||||
catch { fail('Artifact manifest cannot be parsed', 'CANDIDATE_ARTIFACT_MANIFEST_INVALID'); }
|
||||
const payload = validateManifest(manifest, configuredLimits);
|
||||
const current = inventory(root, configuredLimits);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const CANDIDATE_ENGINE_LIMITS = Object.freeze({
|
||||
@@ -6,11 +7,19 @@ export const CANDIDATE_ENGINE_LIMITS = Object.freeze({
|
||||
maxDefinitionBytes: 65_536,
|
||||
maxArgs: 64,
|
||||
maxArgBytes: 4_096,
|
||||
maxEnvironmentEntries: 32,
|
||||
maxEnvironmentEntries: 3,
|
||||
maxEnvironmentNameBytes: 128,
|
||||
maxEnvironmentValueBytes: 4_096,
|
||||
maxOutputBytes: 65_536,
|
||||
maxArtifactPathBytes: 4_096,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
|
||||
/** Deterministic variables only; no ambient process environment is inherited. */
|
||||
export const CANDIDATE_ENGINE_ENV_ALLOWLIST = Object.freeze({
|
||||
LANG: Object.freeze(['C.UTF-8']),
|
||||
LC_ALL: Object.freeze(['C']),
|
||||
TZ: Object.freeze(['UTC']),
|
||||
});
|
||||
|
||||
function fail(message, code, details) {
|
||||
@@ -21,6 +30,10 @@ function byteLength(value) {
|
||||
return Buffer.byteLength(value, 'utf8');
|
||||
}
|
||||
|
||||
function sha256(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex');
|
||||
}
|
||||
|
||||
function exactObject(value, allowed, label) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
fail(`${label} must be an object`, 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
@@ -37,11 +50,14 @@ function boundedString(value, limit, label) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeLimits(limits = {}) {
|
||||
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 (!Object.hasOwn(limits, key)) {
|
||||
fail(`Candidate-engine limit is required: ${key}`, 'CANDIDATE_ENGINE_LIMIT_INVALID', { key });
|
||||
}
|
||||
const value = limits[key];
|
||||
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
||||
fail(`Invalid candidate-engine limit: ${key}`, 'CANDIDATE_ENGINE_LIMIT_INVALID', { key, value, maximum });
|
||||
}
|
||||
@@ -59,13 +75,22 @@ function freeze(value) {
|
||||
}
|
||||
|
||||
function normalizeInput(input, limits) {
|
||||
exactObject(input, new Set(['directory', 'digest', 'bytes']), 'input');
|
||||
exactObject(input, new Set(['directory', 'digest', 'bytes', 'byteLength']), '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');
|
||||
if (!Buffer.isBuffer(input.bytes) && !(input.bytes instanceof Uint8Array)) {
|
||||
fail('Candidate input bytes are required', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
return freeze({ directory, digest: digest.toLowerCase(), bytes: input.bytes });
|
||||
const snapshot = Buffer.from(input.bytes);
|
||||
if (snapshot.length > limits.maxInputBytes || !Number.isInteger(input.byteLength)
|
||||
|| input.byteLength !== snapshot.length || typeof input.digest !== 'string'
|
||||
|| !/^[0-9a-f]{64}$/i.test(input.digest)) {
|
||||
fail('Candidate input must be a bounded immutable byte snapshot', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
const observedDigest = sha256(snapshot);
|
||||
if (input.digest.toLowerCase() !== observedDigest) {
|
||||
fail('Candidate input digest does not bind the supplied bytes', 'CANDIDATE_ENGINE_INPUT_INVALID');
|
||||
}
|
||||
return freeze({ directory, digest: observedDigest, bytes: snapshot.length });
|
||||
}
|
||||
|
||||
function normalizeDefinition(definition, limits) {
|
||||
@@ -93,27 +118,48 @@ function normalizeEnvironment(environment, limits) {
|
||||
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 });
|
||||
for (const [key, value] of entries.sort(([left], [right]) => Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')))) {
|
||||
const allowedValues = CANDIDATE_ENGINE_ENV_ALLOWLIST[key];
|
||||
if (!allowedValues || byteLength(key) > limits.maxEnvironmentNameBytes) {
|
||||
fail('Candidate environment name is not allowlisted', 'CANDIDATE_ENGINE_ENVIRONMENT_INVALID', { key });
|
||||
}
|
||||
normalized[key] = boundedString(value, limits.maxEnvironmentValueBytes, `environment.${key}`);
|
||||
const bounded = boundedString(value, limits.maxEnvironmentValueBytes, `environment.${key}`);
|
||||
if (!allowedValues.includes(bounded)) {
|
||||
fail('Candidate environment value is not allowlisted', 'CANDIDATE_ENGINE_ENVIRONMENT_INVALID', { key });
|
||||
}
|
||||
normalized[key] = bounded;
|
||||
}
|
||||
return freeze(normalized);
|
||||
}
|
||||
|
||||
function safeUtf8Prefix(bytes, limit) {
|
||||
const capped = Math.min(bytes.length, limit);
|
||||
const decoder = new TextDecoder('utf-8', { fatal: true });
|
||||
for (let length = capped; length >= 0; length -= 1) {
|
||||
try {
|
||||
const text = decoder.decode(bytes.subarray(0, length));
|
||||
if (byteLength(text) <= limit) return text;
|
||||
} catch {
|
||||
// Shorten until the prefix ends on a complete UTF-8 sequence.
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
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 };
|
||||
const output = Buffer.isBuffer(value) ? Buffer.from(value)
|
||||
: Buffer.from(typeof value === 'string' ? value : value?.toString?.() ?? '', 'utf8');
|
||||
const bytes = output.length;
|
||||
const text = safeUtf8Prefix(output, bytes > limit ? limit : bytes);
|
||||
return freeze({ bytes, retainedBytes: byteLength(text), text, 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),
|
||||
};
|
||||
return freeze({
|
||||
code: typeof error.code === 'string' ? boundedOutput(error.code, 128).text : null,
|
||||
message: boundedOutput(typeof error.message === 'string' ? error.message : String(error), 512).text,
|
||||
});
|
||||
}
|
||||
|
||||
function terminalObservation(result, limits) {
|
||||
@@ -142,7 +188,7 @@ function terminalObservation(result, limits) {
|
||||
*/
|
||||
export function runCandidateEngine(options = {}) {
|
||||
exactObject(options, new Set(['input', 'definition', 'args', 'environment', 'artifactDirectory', 'executor', 'limits']), 'candidate engine options');
|
||||
const limits = normalizeLimits(options.limits ?? {});
|
||||
const limits = normalizeLimits(options.limits);
|
||||
const input = normalizeInput(options.input, limits);
|
||||
const definition = normalizeDefinition(options.definition, limits);
|
||||
const args = normalizeArgs(options.args ?? [], limits);
|
||||
@@ -151,16 +197,20 @@ export function runCandidateEngine(options = {}) {
|
||||
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');
|
||||
|
||||
const commandArgs = freeze([...definition.args, ...args]);
|
||||
const execution = freeze({
|
||||
cwd: input.directory,
|
||||
env: environment,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: limits.maxOutputBytes,
|
||||
timeout: limits.timeoutMs,
|
||||
killSignal: 'SIGTERM',
|
||||
artifactDirectory,
|
||||
});
|
||||
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,
|
||||
}));
|
||||
raw = executor(definition.command, commandArgs, execution);
|
||||
} catch (error) {
|
||||
raw = { status: null, error };
|
||||
}
|
||||
@@ -169,6 +219,6 @@ export function runCandidateEngine(options = {}) {
|
||||
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,
|
||||
input, definition, args, environment, artifactDirectory, limits, terminal,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user