Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions scripts/agent-benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,20 @@ node scripts/agent-benchmark/driver.mjs selftest-launch-crash
node scripts/agent-benchmark/driver.mjs selftest-android
node scripts/agent-benchmark/driver.mjs selftest-runner-timeout
```

## Export reviewed evidence

`scripts/export-benchmark-viewer.mjs` verifies the retained transcript, app proof,
Settings screenshot, recording, and cleanup before creating portable website
artifacts. It does not modify coordinator records.
Directory and worktree inventories are omitted from public command output;
the command and its timing remain visible.

A reviewed executable-lookup session correction can be supplied beside a run as
`lookup-audit-correction.json`. It binds the run ID, original record hash,
command-log hash and count, and the corrected `run-guards.mjs` source hash using
`schemaVersion`, `runId`, `originalRecordSha256`, `commandsSha256`, `commandCount`,
and `correctionSourceSha256`. Export matches every command against the retained
events and reruns the current session audit. It can clear only the sole
`agent-device-run-session-not-applied` reason; every evidence check still applies.
Keep the original verdict and correction provenance private, outside Git.
67 changes: 64 additions & 3 deletions scripts/export-benchmark-viewer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { userInfo } from 'node:os';
import { fileURLToPath } from 'node:url';
import { stripVTControlCharacters } from 'node:util';
import { launchCrashDiagnosis, launchCrashRecovery } from './launch-crash-benchmark.mjs';
import { topLevelShellCommand } from './agent-benchmark/run-guards.mjs';
import { agentDeviceIsolationInvalidReasons, topLevelShellCommand } from './agent-benchmark/run-guards.mjs';

const modelPricing = {
'gpt-5.6-luna': {
Expand Down Expand Up @@ -54,7 +54,8 @@ const agentDeviceBundlePattern = /\b(?:[A-Za-z0-9-]+\.)+[A-Za-z0-9.-]*agentdevic
const processInspectionPattern = /\b(?:ps|pgrep)(?:\s|$)/;
const deviceInventoryPattern = /\b(?:agent-device devices|xcrun simctl list devices)\b/;
const machineStoragePattern = /\b(?:df|diskutil)(?:\s|$)/;
const branchInventoryPattern = /\bgit\s+(?:branch|for-each-ref)(?:\s|$)/;
const branchInventoryPattern = /\bgit\s+(?:branch|for-each-ref|worktree\s+list)(?:\s|$)/;
const directoryInventoryPattern = /\b(?:ls|find|tree)(?:\s|$)/;
const interactiveShellPattern = /^(?:bash|sh|zsh)$/;
const adbPublicKeyMessagePattern = /(\bSending adb public key \[)[A-Za-z0-9+/=]{80,}(?:\s+[^\]\r\n]*)?(\])/g;
const adbPublicKeyBootArgumentPattern = /(\bandroidboot\.qemu\.adb\.pubkey=)[A-Za-z0-9+/=]{80,}/g;
Expand Down Expand Up @@ -213,6 +214,9 @@ export function sanitizeCommandOutput(command, value, replacements = []) {
if (branchInventoryPattern.test(unwrapped)) {
return '<branch inventory omitted from public artifact>';
}
if (directoryInventoryPattern.test(unwrapped)) {
return '<directory inventory omitted from public artifact>';
}
if (processInspectionPattern.test(unwrapped)) {
return '<process output omitted from public artifact>';
}
Expand Down Expand Up @@ -928,6 +932,63 @@ function validateLaunchCrashRecord(runDir, record, meta) {
return { diagnosis, recovery, diagnosisUsage, screenReadySeconds };
}

function publicationRecord(runDir, meta) {
const recordPath = join(runDir, 'run.json');
const record = readJson(recordPath);
const correctionPath = join(runDir, 'lookup-audit-correction.json');
if (record.valid || !existsSync(correctionPath)) return record;
const correction = readJson(correctionPath);
const commandsPath = join(runDir, 'commands.log');
const guardPath = fileURLToPath(new URL('./agent-benchmark/run-guards.mjs', import.meta.url));
if (
correction.schemaVersion !== 1 ||
correction.runId !== record.runId ||
correction.originalRecordSha256 !== fileSha256(recordPath) ||
correction.correctionSourceSha256 !== fileSha256(guardPath) ||
!existsSync(commandsPath) ||
correction.commandsSha256 !== fileSha256(commandsPath) ||
record.invalidReasons?.length !== 1 ||
record.invalidReasons[0] !== 'agent-device-run-session-not-applied' ||
!meta.agentDevice?.stateDir ||
meta.agentDevice.session !== meta.runId
)
return record;
const commands = readFileSync(commandsPath, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
const invocations = readFileSync(join(runDir, 'events.jsonl'), 'utf8')
.trim()
.split('\n')
.filter(Boolean)
.flatMap((line) => {
const stamped = JSON.parse(line);
let event;
try {
event = JSON.parse(stamped.line);
} catch {
return [];
}
if (event.type === 'item.started' && event.item?.type === 'command_execution') {
return [{ id: event.item.id, command: event.item.command }];
}
return event.type === 'assistant'
? (event.message?.content ?? [])
.filter((block) => block.type === 'tool_use' && block.name === 'Bash')
.map((block) => ({ id: block.id, command: block.input?.command }))
: [];
});
if (
commands.length !== record.commandCount ||
commands.length !== correction.commandCount ||
commands.length !== invocations.length ||
commands.some(
(command, index) => command.id !== invocations[index].id || command.command !== invocations[index].command,
)
)
return record;
const prefix = `env AGENT_DEVICE_STATE_DIR=${meta.agentDevice.stateDir} AGENT_DEVICE_SESSION=${meta.agentDevice.session} agent-device `;
if (agentDeviceIsolationInvalidReasons(commands, prefix).length) return record;
return { ...record, valid: true, invalidReasons: [] };
}

export function exportBenchmark(stageDir, outputPath, proofDir, machine = {}) {
const absoluteStageDir = resolve(stageDir);
const stage = basename(absoluteStageDir);
Expand All @@ -940,8 +1001,8 @@ export function exportBenchmark(stageDir, outputPath, proofDir, machine = {}) {
.filter((runDir) => existsSync(join(runDir, 'run.json')) && existsSync(join(runDir, 'meta.json')));
const records = runDirs
.map((runDir) => {
const record = readJson(join(runDir, 'run.json'));
const meta = readJson(join(runDir, 'meta.json'));
const record = publicationRecord(runDir, meta);
return {
runDir,
record,
Expand Down
117 changes: 116 additions & 1 deletion scripts/export-benchmark-viewer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,23 @@ describe('benchmark viewer export', () => {
expect(
sanitizeCommandOutput('git status --short; git branch -a', 'remotes/origin/@janic/issue-1-clear-filters'),
).toBe('<branch inventory omitted from public artifact>');
expect(
sanitizeCommandOutput('git worktree list && git status', 'worktree/private-diagnostic [private-branch]'),
).toBe('<branch inventory omitted from public artifact>');
});

it('omits coordinator listings and unrelated AVD names without changing ordinary command output', () => {
for (const command of [
'ls -la /private/benchmark; echo ---; ls -la /private/benchmark/state',
'ls ~/.android/avd',
'find /private/benchmark/results -maxdepth 2 -type d',
'/bin/ls -la /private/benchmark',
]) {
expect(sanitizeCommandOutput(command, 'private-run-id\nPersonal_Device.avd\nprivate-audit.json')).toBe(
'<directory inventory omitted from public artifact>',
);
}
expect(sanitizeCommandOutput('cat build.log', 'BUILD SUCCESSFUL')).toBe('BUILD SUCCESSFUL');
});

it('redacts a user-scoped remote branch outside an inventory', () => {
Expand Down Expand Up @@ -626,7 +643,7 @@ describe('benchmark viewer export', () => {
expect(readdirSync(proofDir)).toEqual(['fixture-control.png']);
});

it('requires integrity-bound Android readiness and cleanup evidence', () => {
it('requires integrity-bound Android evidence even when correcting a session audit', () => {
const root = mkdtempSync(join(tmpdir(), 'stim-android-export-'));
tempDirs.push(root);
const stageDir = join(root, 'results', 'sol-android');
Expand Down Expand Up @@ -750,6 +767,104 @@ describe('benchmark viewer export', () => {
expect(payload.runs).toHaveLength(1);
expect(payload.runs[0].commands[0].output).toBe('emulator <simulator-udid>');

const eventsPath = join(runDir, 'events.jsonl');
const metaPath = join(runDir, 'meta.json');
const originalEvents = readFileSync(eventsPath, 'utf8');
const originalMeta = readFileSync(metaPath, 'utf8');
const prefix = 'env AGENT_DEVICE_STATE_DIR=state AGENT_DEVICE_SESSION=private-run-id agent-device ';
const scopedEvents = originalEvents
.split('\n')
.filter(Boolean)
.map((line) => {
const stamped = JSON.parse(line);
const event = JSON.parse(stamped.line);
event.item.command = event.item.command.replace(/^agent-device /, prefix);
stamped.line = JSON.stringify(event);
return stamped;
});
writeFileSync(eventsPath, scopedEvents.map((event) => JSON.stringify(event)).join('\n') + '\n');
writeFileSync(
metaPath,
JSON.stringify({ ...JSON.parse(originalMeta), agentDevice: { stateDir: 'state', session: 'private-run-id' } }),
);
const commandsPath = join(runDir, 'commands.log');
const scopedCommands = scopedEvents
.map((event) => JSON.parse(event.line))
.filter((event) => event.type === 'item.started')
.map(({ item }) => ({ id: item.id, command: item.command }));
writeFileSync(commandsPath, scopedCommands.map((command) => JSON.stringify(command)).join('\n') + '\n');
const rejectedRecord = {
...record,
valid: false,
invalidReasons: ['agent-device-run-session-not-applied'],
evidenceSha256: { ...record.evidenceSha256, events: sha256(eventsPath) },
};
writeFileSync(recordPath, JSON.stringify(rejectedRecord));
const correctionPath = join(runDir, 'lookup-audit-correction.json');
const correction = {
schemaVersion: 1,
runId: record.runId,
originalRecordSha256: sha256(recordPath),
correctionSourceSha256: sha256(join(process.cwd(), 'scripts/agent-benchmark/run-guards.mjs')),
commandsSha256: sha256(commandsPath),
commandCount: scopedCommands.length,
};
writeFileSync(correctionPath, JSON.stringify(correction));
expect(exportBenchmark(stageDir, join(root, 'benchmark.json'), join(root, 'public-proof')).runs[0]).toMatchObject({
valid: true,
settingsReadySeconds: 8,
});
expect(JSON.parse(readFileSync(recordPath))).toEqual(rejectedRecord);
for (const change of [
{ originalRecordSha256: 'stale' },
{ commandsSha256: 'stale' },
{ correctionSourceSha256: 'stale' },
{ commandCount: 0 },
]) {
writeFileSync(correctionPath, JSON.stringify({ ...correction, ...change }));
expect(() => exportBenchmark(stageDir, join(root, 'benchmark.json'), join(root, 'public-proof'))).toThrow(
'no valid benchmark runs found',
);
}
const otherFailure = {
...rejectedRecord,
invalidReasons: [...rejectedRecord.invalidReasons, 'benchmark-run-timeout'],
};
writeFileSync(recordPath, JSON.stringify(otherFailure));
writeFileSync(correctionPath, JSON.stringify({ ...correction, originalRecordSha256: sha256(recordPath) }));
expect(() => exportBenchmark(stageDir, join(root, 'benchmark.json'), join(root, 'public-proof'))).toThrow(
'no valid benchmark runs found',
);
writeFileSync(recordPath, JSON.stringify(rejectedRecord));
writeFileSync(correctionPath, JSON.stringify(correction));
writeFileSync(
commandsPath,
scopedCommands
.map((command, index) =>
JSON.stringify(index === 0 ? { ...command, command: 'agent-device snapshot' } : command),
)
.join('\n') + '\n',
);
writeFileSync(correctionPath, JSON.stringify({ ...correction, commandsSha256: sha256(commandsPath) }));
expect(() => exportBenchmark(stageDir, join(root, 'benchmark.json'), join(root, 'public-proof'))).toThrow(
'no valid benchmark runs found',
);
writeFileSync(commandsPath, scopedCommands.map((command) => JSON.stringify(command)).join('\n') + '\n');
writeFileSync(correctionPath, JSON.stringify(correction));
writeFileSync(
metaPath,
JSON.stringify({
...JSON.parse(originalMeta),
agentDevice: { stateDir: 'wrong-state', session: 'private-run-id' },
}),
);
expect(() => exportBenchmark(stageDir, join(root, 'benchmark.json'), join(root, 'public-proof'))).toThrow(
'no valid benchmark runs found',
);
writeFileSync(metaPath, originalMeta);
writeFileSync(eventsPath, originalEvents);
writeFileSync(recordPath, JSON.stringify(record));

const completeRecording = readFileSync(recordingPath);
const shortRecording = Buffer.from(completeRecording);
const movieHeader = shortRecording.indexOf(Buffer.from('mvhd'));
Expand Down
Loading
Loading