Skip to content

Commit 0469e28

Browse files
committed
feat(artifacts): snapshot environment recipe provenance
1 parent b976506 commit 0469e28

13 files changed

Lines changed: 793 additions & 12 deletions

File tree

apps/cli/src/commands/eval/artifact-writer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path';
33
import {
44
type AdditionalResultArtifactsWriter,
55
type AggregateGradingArtifact,
6+
type EnvironmentSummaryWire,
67
type EvalTest,
78
type EvaluationResult,
89
type ExperimentArtifactMetadata,
@@ -64,6 +65,7 @@ export {
6465
export type {
6566
AggregateGradingArtifact,
6667
GradingArtifact,
68+
EnvironmentSummaryWire,
6769
IndexArtifactEntry,
6870
ResultIndexArtifact,
6971
RunConfigArtifact,

apps/cli/src/commands/eval/task-bundle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,10 @@ function serializeEnvironment(
812812
rewrites: ReadonlyMap<string, string>,
813813
): Record<string, unknown> {
814814
const {
815+
authoredReference: _authoredReference,
815816
recipeFilePath: _recipeFilePath,
817+
recipeFileSha256: _recipeFileSha256,
818+
recipeSha256: _recipeSha256,
816819
sourceDir: _sourceDir,
817820
...portableEnvironment
818821
} = environment;

apps/cli/test/commands/eval/artifact-writer.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,125 @@ describe('writeArtifactsFromResults', () => {
13161316
expect(indexLine.runtime_source).toBeUndefined();
13171317
});
13181318

1319+
it('writes host environment provenance as a sidecar with redacted setup inputs and logs', async () => {
1320+
const result = makeResult({
1321+
testId: 'host-env',
1322+
environmentProvenance: {
1323+
schemaVersion: 'agentv.environment_provenance.v1',
1324+
authoredKind: 'file',
1325+
authoredReference: 'file://.agentv/environments/host.yaml',
1326+
recipeFilePath: '/repo/.agentv/environments/host.yaml',
1327+
recipeFileSha256: 'f'.repeat(64),
1328+
recipeSha256: 'a'.repeat(64),
1329+
type: 'host',
1330+
sourceDir: '/repo/.agentv/environments',
1331+
workdir: '/repo/workspaces/app',
1332+
setup: {
1333+
command: ['node', 'setup.mjs', '--api-key', '<redacted>'],
1334+
args: {
1335+
repo: 'example/app',
1336+
commit: 'abc123',
1337+
api_key: '<redacted>',
1338+
},
1339+
},
1340+
setupExecutions: [
1341+
{
1342+
scope: 'environment',
1343+
name: 'setup',
1344+
status: 'success',
1345+
testId: '__environment_setup__',
1346+
workdir: '/repo/workspaces/app',
1347+
command: ['node', 'setup.mjs', '--api-key', '<redacted>'],
1348+
cwd: '/repo/.agentv/environments',
1349+
output:
1350+
'{"repo_provenance":{"repo":"example/app","commit":"abc123"}}\\nused <redacted>',
1351+
exitCode: 0,
1352+
},
1353+
],
1354+
repoProvenance: { repo: 'example/app', commit: 'abc123' },
1355+
},
1356+
});
1357+
1358+
const paths = await writeArtifactsFromResults([result], testDir, {
1359+
evalFile: 'evals/host.eval.yaml',
1360+
});
1361+
const [indexLine] = await readIndexLines(paths.indexPath);
1362+
const rowDir = expectRowDir(indexLine, 'host-env');
1363+
const environmentPath = path.join(testDir, indexLine.environment_path ?? '');
1364+
const environment = JSON.parse(await readFile(environmentPath, 'utf8'));
1365+
const resultJson = JSON.parse(
1366+
await readFile(path.join(testDir, rowDir, 'sample-1', 'result.json'), 'utf8'),
1367+
);
1368+
const summary: RunSummaryArtifact = JSON.parse(await readFile(paths.summaryPath, 'utf8'));
1369+
1370+
expect(indexLine.environment).toMatchObject({
1371+
schema_version: 'agentv.environment_summary.v1',
1372+
type: 'host',
1373+
workdir: '/repo/workspaces/app',
1374+
recipe_sha256: 'a'.repeat(64),
1375+
authored_reference: 'file://.agentv/environments/host.yaml',
1376+
setup_status: 'success',
1377+
});
1378+
expect(indexLine.environment_path).toBe(`${rowDir}/sample-1/environment.json`);
1379+
expect(indexLine.environment).not.toHaveProperty('setup_executions');
1380+
expect(JSON.stringify(indexLine)).not.toContain('used <redacted>');
1381+
expect(environment.setup.command).toEqual(['node', 'setup.mjs', '--api-key', '<redacted>']);
1382+
expect(environment.setup.args.api_key).toBe('<redacted>');
1383+
expect(environment.setup_executions[0].output).toContain('used <redacted>');
1384+
expect(environment.repo_provenance).toEqual({ repo: 'example/app', commit: 'abc123' });
1385+
expect(resultJson.environment_path).toBe('./environment.json');
1386+
expect(summary.metadata.environments?.[0]).toMatchObject({
1387+
type: 'host',
1388+
recipe_sha256: 'a'.repeat(64),
1389+
});
1390+
});
1391+
1392+
it('writes Docker environment provenance without setup logs in index rows', async () => {
1393+
const result = makeResult({
1394+
testId: 'docker-env',
1395+
environmentProvenance: {
1396+
schemaVersion: 'agentv.environment_provenance.v1',
1397+
authoredKind: 'inline',
1398+
recipeSha256: 'b'.repeat(64),
1399+
type: 'docker',
1400+
sourceDir: '/repo/evals',
1401+
workdir: '/app',
1402+
docker: {
1403+
context: '/repo/environment',
1404+
dockerfile: '/repo/environment/Dockerfile',
1405+
image: 'ghcr.io/example/app@sha256:1234567890abcdef',
1406+
imageDigest: 'sha256:1234567890abcdef',
1407+
},
1408+
},
1409+
});
1410+
1411+
const paths = await writeArtifactsFromResults([result], testDir, {
1412+
evalFile: 'evals/docker.eval.yaml',
1413+
});
1414+
const [indexLine] = await readIndexLines(paths.indexPath);
1415+
const environment = JSON.parse(
1416+
await readFile(path.join(testDir, indexLine.environment_path ?? ''), 'utf8'),
1417+
);
1418+
1419+
expect(indexLine.environment).toMatchObject({
1420+
type: 'docker',
1421+
workdir: '/app',
1422+
docker: {
1423+
context: '/repo/environment',
1424+
dockerfile: '/repo/environment/Dockerfile',
1425+
image: 'ghcr.io/example/app@sha256:1234567890abcdef',
1426+
image_digest: 'sha256:1234567890abcdef',
1427+
},
1428+
});
1429+
expect(indexLine.environment).not.toHaveProperty('setup_executions');
1430+
expect(environment).toMatchObject({
1431+
type: 'docker',
1432+
docker: {
1433+
image_digest: 'sha256:1234567890abcdef',
1434+
},
1435+
});
1436+
});
1437+
13191438
it('does not write experiment config metadata into public run artifacts', async () => {
13201439
const experimentMetadata = {
13211440
name: 'native-exp',

apps/web/src/content/docs/docs/next/reference/result-artifacts.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The default local layout is:
4545
graders/
4646
sample-1/
4747
result.json
48+
environment.json # optional environment recipe provenance
4849
grading.json
4950
metrics.json
5051
target-execution.json # optional target runtime envelope
@@ -97,6 +98,7 @@ reserved for rebuildable local state and are skipped by run discovery.
9798
| `summary.json` | Aggregate run metadata and rollups: run id, experiment label, tags, runtime source, counts, pass rate, score summaries, duration, token/cost totals, and writer metadata. | Listing runs, CI summaries, quick dashboards, trend cards, and validating that a run is complete enough to inspect. |
9899
| `.internal/index.jsonl` | Canonical per-run row index: one row per case/result aggregate, with identity fields, filter metadata, scores, status, and explicit run-relative paths to sidecars. | Filtering, compare/trend inputs, Dashboard detail routing, rerun/resume lookup, export adapters, and artifact discovery. |
99100
| `result.json` | Compact per-attempt manifest for one attempt directory, including AgentV `execution_status` and `verdict`. | Loading one attempt without scanning the whole run index. |
101+
| `environment.json` / `environment_path` | Redacted environment recipe provenance: authored inline/file reference, resolved recipe hash, host or Docker type, resolved workdir, setup command and typed args, setup log output/error, Docker context/image/digest fields when available, and repo provenance only when authored or emitted by setup. Index rows carry `environment_path` plus a compact `environment` summary; large setup logs stay in the sidecar. | Reproducing and reviewing the testbed without treating setup side effects as row metadata. Repository identity is opaque unless the environment recipe or setup output states it explicitly. |
100102
| `grading.json` | Grader outputs, `assertion_results`, rubric evidence, execution-metric grader facts, and scoring provenance. | Explaining why a row passed or failed. |
101103
| `metrics.json` | Duration, token usage, cost, execution status, trajectory, and derived executor behavior such as tool calls, files touched, shell commands, errors, turns, and output sizes. | Dashboard behavior views, cost/latency reporting, metric-style graders, adapter projections, and lightweight analysis. |
102104
| `target-execution.json` | Provider-neutral target runtime envelope, including command, cwd, timeout, exit code or signal, error kind, timestamps, log truncation metadata, and artifact paths. | Distinguishing target task failures, target crashes, timeouts, cancellation, malformed provider output, and sandbox/runner failures from AgentV orchestrator failures. |

0 commit comments

Comments
 (0)