Skip to content

Commit 62f550c

Browse files
authored
ci(cli): validate installed Eval frameworks (#3188)
* ci(cli): validate installed Eval frameworks Run Harbor and Pier against the same immutable CLI tarball after cross-platform spec and prerequisite checks. Use deterministic local tasks so the release gate exercises the installed relay, Docker environment, verifier result, artifacts, and cleanup without provider credentials. Preserve Pier's framework-owned log mounts when adding user mounts; otherwise an explicit empty mount list replaces the paths required for subject scope, rewards, and collected artifacts. Generated-by: OpenAI Codex * fix(eval): harden release validation failures Reserve Pier's framework-owned log subtrees before Docker composition so configured mounts cannot shadow verifier rewards or collected artifacts. Keep framework failures authoritative when summary or diagnostic evidence is malformed or unreadable, and create the deterministic Git fixture under the same isolated environment as the installed candidate. Generated-by: OpenAI Codex * test(eval): assert reserved mount errors exactly Use an exact predicate for the observable Pier mount rejection so path punctuation cannot weaken the regression check.
1 parent 32e3cbb commit 62f550c

8 files changed

Lines changed: 908 additions & 14 deletions

.github/workflows/cli-package-validation.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,36 @@ jobs:
144144
path: packages/cli/release
145145
- name: Validate the installed tarball
146146
run: node scripts/smoke-release-cli-package.mjs
147+
148+
eval:
149+
name: Validate installed CLI Eval
150+
needs: build
151+
runs-on: ubuntu-24.04
152+
timeout-minutes: 30
153+
steps:
154+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
155+
with:
156+
persist-credentials: false
157+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
158+
with:
159+
node-version: '24'
160+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
161+
with:
162+
python-version: '3.12'
163+
- name: Select the release npm toolchain
164+
run: npm install --global --no-audit --no-fund npm@11.12.1
165+
- name: Install pinned Eval frameworks
166+
run: |
167+
python -m venv "$RUNNER_TEMP/maka-harbor"
168+
"$RUNNER_TEMP/maka-harbor/bin/python" -m pip install --disable-pip-version-check 'harbor==0.20.0'
169+
python -m venv "$RUNNER_TEMP/maka-pier"
170+
"$RUNNER_TEMP/maka-pier/bin/python" -m pip install --disable-pip-version-check 'datacurve-pier==0.3.0'
171+
echo "MAKA_RELEASE_HARBOR_PYTHON=$RUNNER_TEMP/maka-harbor/bin/python" >> "$GITHUB_ENV"
172+
echo "MAKA_RELEASE_PIER_PYTHON=$RUNNER_TEMP/maka-pier/bin/python" >> "$GITHUB_ENV"
173+
- name: Download the release candidate
174+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
175+
with:
176+
name: cli-release-candidate
177+
path: packages/cli/release
178+
- name: Validate real Harbor and Pier cells
179+
run: npm run release:cli:eval

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@
4444
"check:cli-third-party-notices": "node scripts/generate-third-party-notices.mjs --target cli --check",
4545
"release:cli:pack": "node scripts/release-cli-package.mjs",
4646
"release:cli:smoke": "node scripts/smoke-release-cli-package.mjs",
47+
"release:cli:eval": "node scripts/release-cli-eval-package.mjs",
4748
"generate:windows-cargo-notices": "node scripts/generate-windows-cargo-notices.mjs",
4849
"check:windows-cargo-notices": "node scripts/generate-windows-cargo-notices.mjs --check",
49-
"check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && node --test scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs",
50+
"check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && node --test scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs",
5051
"package:macos-arm64": "node scripts/package-macos-arm64.mjs",
5152
"verify:macos-arm64": "node scripts/verify-macos-arm64-dmg.mjs",
5253
"package:windows-x64": "node scripts/package-windows-x64.mjs",

packages/eval/src/__tests__/lifecycle-boundaries.test.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,37 @@ test('pier cannot declare an egress proxy it never enforces', () => {
12951295
);
12961296
});
12971297

1298-
test('launched trial environment does not inherit MAKA_EVAL_FRAMEWORK', {
1298+
test('Pier rejects configured mounts that collide with framework log ownership', () => {
1299+
const root = join(tmpdir(), 'maka-test-pier-reserved-mount');
1300+
const restoreEnvironment = setEnvironment({
1301+
MAKA_TEST_MOUNT: join(root, 'mount'),
1302+
MAKA_TEST_PYTHON: join(root, 'python'),
1303+
MAKA_TEST_TASKS: join(root, 'tasks'),
1304+
MAKA_TEST_TRIALS: join(root, 'trials'),
1305+
});
1306+
try {
1307+
for (const target of ['/logs/agent/../agent', '/logs/verifier/reward.txt']) {
1308+
assert.throws(
1309+
() =>
1310+
createPierExecutor(
1311+
{
1312+
...executorConfig(),
1313+
tasksRootEnv: 'MAKA_TEST_TASKS',
1314+
mounts: [{ sourceEnv: 'MAKA_TEST_MOUNT', target, readOnly: true }],
1315+
},
1316+
'experiment.json',
1317+
),
1318+
(error) =>
1319+
error instanceof Error &&
1320+
error.message === `Pier mount target ${target} is reserved for framework logs`,
1321+
);
1322+
}
1323+
} finally {
1324+
restoreEnvironment();
1325+
}
1326+
});
1327+
1328+
test('Pier preserves its log mounts without inheriting MAKA_EVAL_FRAMEWORK', {
12991329
timeout: 10_000,
13001330
}, async () => {
13011331
const root = await mkdtemp(join(tmpdir(), 'maka-eval-framework-env-'));
@@ -1309,6 +1339,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
13091339
const config = JSON.parse(await readFile(process.argv.at(-1), 'utf8'));
13101340
await writeFile(process.env.MAKA_TEST_ENV, JSON.stringify({
13111341
framework: process.env.MAKA_EVAL_FRAMEWORK ?? null,
1342+
mounts: config.environment.mounts,
1343+
trialName: config.trial_name,
13121344
}));
13131345
const socket = connect(config.agent.kwargs.relay_port, config.agent.kwargs.relay_host);
13141346
socket.setEncoding('utf8');
@@ -1344,14 +1376,33 @@ socket.end();
13441376
MAKA_TEST_PYTHON: executable,
13451377
MAKA_TEST_TRIALS: root,
13461378
MAKA_TEST_ENV: envDump,
1379+
MAKA_TEST_MOUNT: root,
1380+
MAKA_TEST_TASKS: root,
13471381
MAKA_EVAL_FRAMEWORK: 'pier',
13481382
});
13491383
try {
1384+
const spec: ExperimentSpec = {
1385+
...experiment(),
1386+
executor: {
1387+
kind: 'pier',
1388+
config: {
1389+
...executorConfig(),
1390+
tasksRootEnv: 'MAKA_TEST_TASKS',
1391+
mounts: [{ sourceEnv: 'MAKA_TEST_MOUNT', target: '/input', readOnly: true }],
1392+
},
1393+
},
1394+
tasks: [{ id: 'task', input: 'solve', config: { pier: { path: 'task' } } }],
1395+
};
13501396
const results = await runExperiment({
1351-
spec: experiment(),
1397+
spec,
13521398
store: new FileAttemptStore(join(root, 'attempts')),
1353-
executor: createHarborExecutor(
1354-
{ ...executorConfig(), preparationEnvironment: ['MAKA_TEST_ENV'] },
1399+
executor: createPierExecutor(
1400+
{
1401+
...executorConfig(),
1402+
tasksRootEnv: 'MAKA_TEST_TASKS',
1403+
preparationEnvironment: ['MAKA_TEST_ENV'],
1404+
mounts: [{ sourceEnv: 'MAKA_TEST_MOUNT', target: '/input', readOnly: true }],
1405+
},
13551406
join(root, 'experiment.json'),
13561407
),
13571408
subjects: [
@@ -1372,7 +1423,26 @@ socket.end();
13721423
],
13731424
});
13741425
assert.equal(results.get('task::1::external')?.result.status, 'completed');
1375-
assert.deepEqual(JSON.parse(await readFile(envDump, 'utf8')), { framework: null });
1426+
const launched = JSON.parse(await readFile(envDump, 'utf8')) as {
1427+
framework: string | null;
1428+
mounts: Array<{ source: string; target: string }>;
1429+
trialName: string;
1430+
};
1431+
assert.equal(launched.framework, null);
1432+
assert.deepEqual(launched.mounts, [
1433+
{ type: 'bind', source: root, target: '/input', read_only: true },
1434+
{ type: 'bind', source: join(root, launched.trialName, 'agent'), target: '/logs/agent' },
1435+
{
1436+
type: 'bind',
1437+
source: join(root, launched.trialName, 'verifier'),
1438+
target: '/logs/verifier',
1439+
},
1440+
{
1441+
type: 'bind',
1442+
source: join(root, launched.trialName, 'artifacts'),
1443+
target: '/logs/artifacts',
1444+
},
1445+
]);
13761446
} finally {
13771447
restoreEnvironment();
13781448
await rm(root, { recursive: true, force: true });

packages/eval/src/harness-executor.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { once } from 'node:events';
44
import { createReadStream } from 'node:fs';
55
import { chmod, lstat, mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
66
import { createServer, type Server, type Socket } from 'node:net';
7-
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
7+
import { basename, dirname, join, posix, relative, resolve, sep } from 'node:path';
88
import { createInterface } from 'node:readline';
99
import { decodeJsonObject, type ExperimentCell, type JsonObject } from './experiment.js';
1010
import {
@@ -33,6 +33,12 @@ import type { EvalResult } from './result.js';
3333
export type HarnessFramework = 'harbor' | 'pier';
3434
type RelayTransportStage = 'ready' | 'execute' | 'receive' | 'decision';
3535

36+
const PIER_FRAMEWORK_LOG_MOUNTS = Object.freeze([
37+
{ directory: 'agent', target: '/logs/agent' },
38+
{ directory: 'verifier', target: '/logs/verifier' },
39+
{ directory: 'artifacts', target: '/logs/artifacts' },
40+
]);
41+
3642
interface RelayTransportFailure {
3743
readonly stage: RelayTransportStage;
3844
readonly category:
@@ -195,7 +201,12 @@ async function runHarnessAttempt(
195201
: await waitForTrial(state.child, { phase: 'completion' });
196202
finalizationEvidence = completed;
197203
if (!finalizationConfirmed(completed)) throw new Error('Trial did not finalize cleanly');
198-
const verification = await readVerification(state, cell, Boolean(options.egressProxy));
204+
const verification = await readVerification(
205+
state,
206+
cell,
207+
framework,
208+
Boolean(options.egressProxy),
209+
);
199210
verificationConfirmedBeforeCancellation = !hostCancellationObserved;
200211
return verification;
201212
},
@@ -381,7 +392,7 @@ async function startTrial(
381392
const task = decodeTask(framework, options, cell);
382393
const timeoutMultiplier = positive(cell.budget.timeoutMultiplier, 'budget.timeoutMultiplier');
383394
const egressPaths = await resolveEgressPaths(options);
384-
const environmentConfig = resolveEnvironmentConfig(options, egressPaths);
395+
const environmentConfig = resolveEnvironmentConfig(options, egressPaths, framework, trialPath);
385396
const networkPolicyPath = egressPaths?.networkPolicyPath;
386397
const executionEnvironment = {
387398
...UNATTENDED_EXECUTION_ENVIRONMENT,
@@ -751,6 +762,7 @@ function inspectEgressAudit(audit: Buffer): {
751762
async function readVerification(
752763
state: RelayState,
753764
cell: ExperimentCell,
765+
framework: HarnessFramework,
754766
expectEgressAudit: boolean,
755767
): Promise<ExecutorVerification> {
756768
const result = JSON.parse(await readFile(join(state.trialPath, 'result.json'), 'utf8')) as {
@@ -777,7 +789,7 @@ async function readVerification(
777789
failureReason: `failed to read egress audit log ${egressAuditPath}${code ? ` (${code})` : ''}`,
778790
artifacts: [
779791
{ kind: 'trial', framework: cell.executor.kind, trialName: state.trialName },
780-
...(await collectedArtifactInventory(state.trialPath)),
792+
...(await collectedArtifactInventory(state.trialPath, framework)),
781793
{ kind: 'egress-audit-unreadable', path: EGRESS_AUDIT_ARTIFACT_PATH },
782794
],
783795
};
@@ -796,14 +808,20 @@ async function readVerification(
796808
failureReason: audit.failureReason ?? (score === null ? 'verifier produced no reward' : null),
797809
artifacts: [
798810
{ kind: 'trial', framework: cell.executor.kind, trialName: state.trialName },
799-
...(await collectedArtifactInventory(state.trialPath)),
811+
...(await collectedArtifactInventory(state.trialPath, framework)),
800812
...audit.artifacts,
801813
],
802814
};
803815
}
804816

805-
async function collectedArtifactInventory(trialPath: string): Promise<JsonObject[]> {
806-
const root = join(trialPath, 'artifacts', 'logs', 'artifacts');
817+
async function collectedArtifactInventory(
818+
trialPath: string,
819+
framework: HarnessFramework,
820+
): Promise<JsonObject[]> {
821+
const root =
822+
framework === 'pier'
823+
? join(trialPath, 'artifacts')
824+
: join(trialPath, 'artifacts', 'logs', 'artifacts');
807825
const files: JsonObject[] = [];
808826
const targets = [
809827
join(root, basename(MAKA_RUNTIME_ARTIFACT_PATH)),
@@ -910,6 +928,18 @@ function decodeHarnessOptions(value: JsonObject, framework: HarnessFramework): H
910928
? { tasksRootEnv: machinePathEnv(options.tasksRootEnv, 'tasksRootEnv') }
911929
: {}),
912930
};
931+
if (framework === 'pier') {
932+
const reservedTargets = PIER_FRAMEWORK_LOG_MOUNTS.map((mount) => mount.target);
933+
const collision = decoded.mounts.find((mount) => {
934+
const target = posix.normalize(mount.target);
935+
return reservedTargets.some(
936+
(reserved) => target === reserved || target.startsWith(`${reserved}/`),
937+
);
938+
});
939+
if (collision) {
940+
throw new Error(`Pier mount target ${collision.target} is reserved for framework logs`);
941+
}
942+
}
913943
for (const name of [
914944
decoded.pythonPathEnv,
915945
decoded.trialsRootEnv,
@@ -980,8 +1010,22 @@ interface ResolvedEgressPaths {
9801010
function resolveEnvironmentConfig(
9811011
options: HarnessOptions,
9821012
egressPaths: ResolvedEgressPaths | undefined,
1013+
framework: HarnessFramework,
1014+
trialPath: string,
9831015
): JsonObject {
984-
const base = { ...options.environment, mounts: resolveMounts(options.mounts) };
1016+
const configuredMounts = resolveMounts(options.mounts);
1017+
const mounts =
1018+
framework === 'pier'
1019+
? [
1020+
...configuredMounts,
1021+
...PIER_FRAMEWORK_LOG_MOUNTS.map(({ directory, target }) => ({
1022+
type: 'bind',
1023+
source: join(trialPath, directory),
1024+
target,
1025+
})),
1026+
]
1027+
: configuredMounts;
1028+
const base = { ...options.environment, mounts };
9851029
if (!options.egressProxy) return base;
9861030
if (!egressPaths) throw new Error('egress proxy paths are unavailable');
9871031
return { ...base, extra_docker_compose: [egressPaths.composePath] };

0 commit comments

Comments
 (0)