Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
349 lines
10 KiB
JavaScript
349 lines
10 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { basename, join, relative, sep } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const PROCESS_TIMEOUT_MS = 10 * 60 * 1000;
|
|
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
const MAX_DIAGNOSTIC_TEXT = 4_000;
|
|
|
|
export function findEvalReleaseTarball(
|
|
releaseDirectory,
|
|
platform = process.platform,
|
|
arch = process.arch,
|
|
) {
|
|
if (platform !== 'linux' || arch !== 'x64') {
|
|
throw new Error('The real Eval release validation requires Linux x64');
|
|
}
|
|
return findReleaseTarball(releaseDirectory);
|
|
}
|
|
|
|
export function findReleaseTarball(releaseDirectory) {
|
|
const tarballs = readdirSync(releaseDirectory)
|
|
.filter((name) => /^maka-agent-[^/]+\.tgz$/u.test(name))
|
|
.map((name) => join(releaseDirectory, name));
|
|
if (tarballs.length !== 1) {
|
|
throw new Error(
|
|
`Expected one release tarball in ${releaseDirectory}, found ${tarballs.length}`,
|
|
);
|
|
}
|
|
return tarballs[0];
|
|
}
|
|
|
|
export function isolatedEnvironment(home) {
|
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
const environment = {
|
|
...process.env,
|
|
HOME: home,
|
|
NODE_PATH: '',
|
|
XDG_CACHE_HOME: join(home, '.cache'),
|
|
XDG_CONFIG_HOME: join(home, '.config'),
|
|
XDG_DATA_HOME: join(home, '.local/share'),
|
|
};
|
|
delete environment.PYTHONHOME;
|
|
delete environment.PYTHONPATH;
|
|
for (const name of Object.keys(environment)) {
|
|
if (name.startsWith('MAKA_EVAL_') || name.startsWith('GIT_')) delete environment[name];
|
|
}
|
|
environment.GIT_CONFIG_NOSYSTEM = '1';
|
|
for (const name of [
|
|
'ANTHROPIC_API_KEY',
|
|
'DEEPSEEK_API_KEY',
|
|
'OPENAI_API_KEY',
|
|
'OPENROUTER_API_KEY',
|
|
]) {
|
|
delete environment[name];
|
|
}
|
|
return environment;
|
|
}
|
|
|
|
export function createTaskFixture(root, environment, taskImage) {
|
|
const repositoryRoot = join(root, 'repository');
|
|
const taskRoot = join(repositoryRoot, 'task');
|
|
mkdirSync(join(taskRoot, 'environment'), { recursive: true, mode: 0o700 });
|
|
mkdirSync(join(taskRoot, 'tests'), { recursive: true, mode: 0o700 });
|
|
writeFileSync(
|
|
join(taskRoot, 'task.toml'),
|
|
[
|
|
'version = "1.0"',
|
|
'',
|
|
'[metadata]',
|
|
'',
|
|
'[verifier]',
|
|
'timeout_sec = 60.0',
|
|
'',
|
|
'[agent]',
|
|
'timeout_sec = 60.0',
|
|
'',
|
|
'[environment]',
|
|
'build_timeout_sec = 120.0',
|
|
'',
|
|
].join('\n'),
|
|
{ mode: 0o600 },
|
|
);
|
|
writeFileSync(
|
|
join(taskRoot, 'instruction.md'),
|
|
'Exit successfully without modifying the task.\n',
|
|
{ mode: 0o600 },
|
|
);
|
|
writeFileSync(join(taskRoot, 'environment/Dockerfile'), `FROM ${taskImage}\nWORKDIR /app\n`, {
|
|
mode: 0o600,
|
|
});
|
|
const testPath = join(taskRoot, 'tests/test.sh');
|
|
writeFileSync(testPath, '#!/bin/sh\nset -eu\nprintf "1\\n" > /logs/verifier/reward.txt\n', {
|
|
mode: 0o700,
|
|
});
|
|
chmodSync(testPath, 0o700);
|
|
run('git', ['init', '--initial-branch=main'], environment, repositoryRoot);
|
|
run('git', ['config', 'user.name', 'Maka Release Validation'], environment, repositoryRoot);
|
|
run(
|
|
'git',
|
|
['config', 'user.email', 'release-validation@maka.invalid'],
|
|
environment,
|
|
repositoryRoot,
|
|
);
|
|
run('git', ['add', '.'], environment, repositoryRoot);
|
|
run(
|
|
'git',
|
|
['commit', '--message', 'Add deterministic release validation task'],
|
|
environment,
|
|
repositoryRoot,
|
|
);
|
|
const commit = run('git', ['rev-parse', 'HEAD'], environment, repositoryRoot).trim();
|
|
return {
|
|
commit,
|
|
repository: pathToFileURL(repositoryRoot).href,
|
|
tasksRoot: repositoryRoot,
|
|
};
|
|
}
|
|
|
|
export function readFrameworkOutputs({ framework, invocation, outputRoot, trialsRoot }) {
|
|
if (invocation.status !== 0) {
|
|
throw frameworkOutputError(
|
|
framework,
|
|
`exited with status ${invocation.status}`,
|
|
invocation,
|
|
[],
|
|
findJsonDiagnostics(trialsRoot),
|
|
);
|
|
}
|
|
const attempts = findAttemptFiles(join(outputRoot, 'attempts'));
|
|
const attemptNames = attempts.map((path) => relative(outputRoot, path).split(sep).join('/'));
|
|
if (attempts.length !== 1) {
|
|
throw frameworkOutputError(
|
|
framework,
|
|
`produced ${attempts.length} attempt files instead of one`,
|
|
invocation,
|
|
attemptNames,
|
|
findJsonDiagnostics(trialsRoot),
|
|
);
|
|
}
|
|
const summary = decodeJsonRecord(invocation.stdout);
|
|
if (!summary.ok) {
|
|
throw frameworkOutputError(
|
|
framework,
|
|
'produced invalid summary JSON',
|
|
invocation,
|
|
attemptNames,
|
|
findJsonDiagnostics(trialsRoot),
|
|
{ summary: summary.evidence },
|
|
);
|
|
}
|
|
const attempt = decodeJsonRecord(readFileSync(attempts[0], 'utf8'));
|
|
if (!attempt.ok) {
|
|
throw frameworkOutputError(
|
|
framework,
|
|
'produced invalid attempt JSON',
|
|
invocation,
|
|
attemptNames,
|
|
findJsonDiagnostics(trialsRoot),
|
|
{ attempt: attempt.evidence },
|
|
);
|
|
}
|
|
return { summary: summary.value, attempt: attempt.value };
|
|
}
|
|
|
|
export function findJsonDiagnostics(root, current = root, diagnostics = {}) {
|
|
if (!existsSync(current)) return diagnostics;
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(current, { withFileTypes: true });
|
|
} catch (error) {
|
|
const name = relative(root, current).split(sep).join('/') || '.';
|
|
diagnostics[name] = diagnosticIoError('list', error);
|
|
return diagnostics;
|
|
}
|
|
for (const entry of entries) {
|
|
const path = join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
findJsonDiagnostics(root, path, diagnostics);
|
|
continue;
|
|
}
|
|
if (!['preparation-error.json', 'result.json', 'trial.log'].includes(entry.name)) continue;
|
|
const name = relative(root, path).split(sep).join('/');
|
|
let content;
|
|
try {
|
|
content = readFileSync(path, 'utf8').trim();
|
|
} catch (error) {
|
|
diagnostics[name] = diagnosticIoError('read', error);
|
|
continue;
|
|
}
|
|
if (entry.name === 'trial.log') {
|
|
diagnostics[name] = tail(content);
|
|
continue;
|
|
}
|
|
const decoded = decodeJson(content);
|
|
if (!decoded.ok) {
|
|
diagnostics[name] = decoded.evidence;
|
|
continue;
|
|
}
|
|
const parsed = decoded.value;
|
|
diagnostics[name] =
|
|
entry.name === 'result.json' && isRecord(parsed)
|
|
? {
|
|
exceptionInfo: isRecord(parsed.exception_info)
|
|
? {
|
|
type: parsed.exception_info.exception_type,
|
|
message: tail(String(parsed.exception_info.exception_message ?? '')),
|
|
}
|
|
: null,
|
|
verifierResult: parsed.verifier_result ?? null,
|
|
}
|
|
: parsed;
|
|
}
|
|
return diagnostics;
|
|
}
|
|
|
|
export function run(command, args, environment, cwd) {
|
|
try {
|
|
return execFileSync(command, args, {
|
|
cwd,
|
|
env: environment,
|
|
encoding: 'utf8',
|
|
timeout: PROCESS_TIMEOUT_MS,
|
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
} catch (error) {
|
|
const stdout = String(error.stdout ?? '').trim();
|
|
const stderr = String(error.stderr ?? '').trim();
|
|
throw new Error(
|
|
`${command} ${args.join(' ')} failed${stdout ? `\nstdout:\n${stdout}` : ''}${stderr ? `\nstderr:\n${stderr}` : ''}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
}
|
|
|
|
export function runAllowingFailure(command, args, environment, cwd) {
|
|
try {
|
|
return { status: 0, stdout: run(command, args, environment, cwd), stderr: '' };
|
|
} catch (error) {
|
|
const cause = error.cause;
|
|
if (typeof cause?.status !== 'number') throw error;
|
|
return {
|
|
status: cause.status,
|
|
stdout: String(cause.stdout ?? ''),
|
|
stderr: String(cause.stderr ?? ''),
|
|
};
|
|
}
|
|
}
|
|
|
|
function findAttemptFiles(root) {
|
|
if (!existsSync(root)) return [];
|
|
const files = [];
|
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
const path = join(root, entry.name);
|
|
if (entry.isDirectory()) files.push(...findAttemptFiles(path));
|
|
else if (/^\d{6}\.json$/u.test(entry.name)) files.push(path);
|
|
}
|
|
return files.sort();
|
|
}
|
|
|
|
function frameworkOutputError(
|
|
framework,
|
|
reason,
|
|
invocation,
|
|
attempts,
|
|
frameworkDiagnostics,
|
|
extra = {},
|
|
) {
|
|
return new Error(
|
|
`${framework} ${reason}: ${JSON.stringify({
|
|
invocation: {
|
|
status: invocation.status,
|
|
stdout: tail(invocation.stdout),
|
|
stderr: tail(invocation.stderr),
|
|
},
|
|
attempts,
|
|
...extra,
|
|
frameworkDiagnostics,
|
|
})}`,
|
|
);
|
|
}
|
|
|
|
function decodeJsonRecord(content) {
|
|
const decoded = decodeJson(content);
|
|
if (!decoded.ok) return decoded;
|
|
if (!isRecord(decoded.value)) {
|
|
return {
|
|
ok: false,
|
|
evidence: { parseError: 'expected a JSON object', raw: tail(String(content).trim()) },
|
|
};
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
function decodeJson(content) {
|
|
const raw = String(content).trim();
|
|
try {
|
|
return { ok: true, value: JSON.parse(raw) };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
evidence: {
|
|
parseError: error instanceof Error ? error.message : String(error),
|
|
raw: tail(raw),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function diagnosticIoError(operation, error) {
|
|
const code = typeof error?.code === 'string' ? error.code : 'UNKNOWN';
|
|
return { diagnosticError: { operation, code } };
|
|
}
|
|
|
|
function tail(value) {
|
|
return String(value).slice(-MAX_DIAGNOSTIC_TEXT);
|
|
}
|