2026-07-20 17:11:06 +08:00
import { spawn } from "node:child_process" ;
import { cp , mkdir , mkdtemp , rm , writeFile } from "node:fs/promises" ;
import { tmpdir } from "node:os" ;
import { dirname , join , resolve } from "node:path" ;
import { createInterface } from "node:readline" ;
import { fileURLToPath } from "node:url" ;
import { createPackage } from "@electron/asar" ;
import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js" ;
2026-07-26 15:02:58 +00:00
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js" ;
2026-07-22 12:13:45 +00:00
import { completeVerifierRun , createVerifierRun } from "./lib/verifier-run.mjs" ;
2026-07-20 17:11:06 +08:00
const repositoryRoot = resolve ( dirname ( fileURLToPath ( import . meta . url )), ".." );
2026-07-22 12:13:45 +00:00
const verifierRun = createVerifierRun ();
2026-07-20 17:11:06 +08:00
const evaluationRoot = await mkdtemp ( join ( tmpdir (), "rea-agent-eval-" ));
const fixtureRoot = join ( evaluationRoot , "targets" );
const skillDestination = join (
evaluationRoot ,
".agents/skills/reverse-engineer-anything" ,
);
const timeoutMs = Number ( process . env . REA_AGENT_EVAL_TIMEOUT_MS ?? 480_000 );
const codex = process . env . REA_CODEX_CLI ?? "codex" ;
const optionalModel = process . env . REA_AGENT_EVAL_MODEL ;
if ( ! Number . isSafeInteger ( timeoutMs ) || timeoutMs < 10_000 )
throw new Error ( "REA_AGENT_EVAL_TIMEOUT_MS must be an integer >= 10000" );
const codexVersion = await readCommandVersion ( codex );
try {
await mkdir ( fixtureRoot , { recursive : true });
await cp (
join ( repositoryRoot , "skills/reverse-engineer-anything" ),
skillDestination ,
{ recursive : true },
);
2026-07-29 17:36:51 +00:00
const requestedScenarioIds = new Set (
( process . env . REA_AGENT_EVAL_SCENARIOS ?? "" )
. split ( "," )
. map (( value ) => value . trim ())
. filter (( value ) => value . length > 0 ),
);
const targets = await createTargets (
fixtureRoot ,
requestedScenarioIds . size === 0 || requestedScenarioIds . has ( "managed" ),
);
2026-07-20 17:11:06 +08:00
const allScenarios = [
{
id : "native" ,
expectedFirstTool : "open_binary" ,
requiresEvidence : false ,
requiredAnswerTermGroups : [[ "unavailable" , "could not" , "provider" ]],
prompt : `Explain what the shipped native program at ${ targets . native } does. Base the answer on artifact analysis, and clearly state any facts that remain unavailable.` ,
},
{
id : "asar" ,
expectedFirstTool : "analyze_javascript_application" ,
requiresEvidence : true ,
requiredAnswerTermGroups : [
[ "profileapi" ],
[ "profile:read" ],
[ "preload" , "contextbridge" ],
],
prompt : `Explain how the desktop application at ${ targets . javascript } exposes APIs to its renderer. Base the answer on the shipped application artifact, and state what remains unknown.` ,
},
2026-07-22 20:38:26 +08:00
{
id : "javascript-export-shape" ,
expectedFirstTool : "analyze_javascript_application" ,
requiresEvidence : true ,
requiredToolSubsequence : [
"analyze_javascript_application" ,
"analyze_javascript_application" ,
"compare_javascript_export_shapes" ,
],
requiredAnswerTermGroups : [
[ "/depth" , "depth" ],
[
"literal value `1`" ,
"literal `1`" ,
"depth: 1" ,
'"depth": 1' ,
"depth = 1" ,
],
[ "static" , "inferred" ],
2026-10-05 21:15:04 +08:00
[ "runtime" , "behavioral probes" ],
2026-07-22 20:38:26 +08:00
],
prompt : `Without directly reading target files, compare the default export in parser.mjs between ${ targets . javascriptShapeLeft } and ${ targets . javascriptShapeRight } . Use REA to analyze each shipped artifact and then compare its exact static export return shapes. State the exact heading-shape change, cite produced Evidence, and distinguish static inference from runtime semantics.` ,
},
2026-07-20 17:11:06 +08:00
{
id : "managed" ,
expectedFirstTool : "inspect_managed_artifact" ,
requiresEvidence : true ,
requiredAnswerTermGroups : [[ "profile" ], [ "main" , "entry point" ]],
prompt : `Explain the public managed types and entry point in ${ targets . managed } . Use shipped-artifact evidence and distinguish unavailable behavior from observed metadata.` ,
},
{
id : "browser" ,
expectedFirstTool : "list_browser_targets" ,
requiresEvidence : false ,
requiredAnswerTermGroups : [
[ "unavailable" , "could not" , "connection" , "endpoint" ],
],
prompt :
"A user-owned page is already open at http://127.0.0.1:3000 through the approved local debugging endpoint http://127.0.0.1:9222. Inspect what is available without navigating or executing page code, and report any authority or availability limits." ,
},
2026-07-29 17:36:51 +00:00
{
id : "navigation-context" ,
expectedFirstTool : "get_navigation_context" ,
requiresEvidence : false ,
requiredAnswerTermGroups : [[ "unavailable" , "could not" , "session" ]],
prompt :
"Report the selected document, current address, and containing procedure from the current native-analysis session in one coherent request. If no native session is available, report that limitation plainly." ,
},
{
id : "address-context" ,
expectedFirstTool : "inspect_address_context" ,
requiresEvidence : false ,
requiredAnswerTermGroups : [[ "unavailable" , "could not" , "session" ]],
prompt :
"Inspect explicit address 0x401000 in the current native-analysis session. Report its analyzed name, containing procedure, regular comment, inline comment, and matching bookmarks. If no native session is available, report that limitation plainly." ,
},
2026-07-20 17:11:06 +08:00
];
const scenarios =
requestedScenarioIds . size === 0
? allScenarios
: allScenarios . filter (({ id }) => requestedScenarioIds . has ( id ));
if (
requestedScenarioIds . size > 0 &&
scenarios . length !== requestedScenarioIds . size
)
throw new Error (
`Unknown or duplicate REA_AGENT_EVAL_SCENARIOS value: ${ [... requestedScenarioIds ]. join ( "," ) } ` ,
);
const results = [];
for ( const scenario of scenarios ) {
process . stderr . write ( `Running Codex agent evaluation: ${ scenario . id } \n` );
2026-09-29 07:08:46 +08:00
const execution = await runCodex ( scenario . prompt );
2026-07-20 17:11:06 +08:00
const transcriptDirectory = process . env . REA_AGENT_EVAL_TRANSCRIPT_DIR ;
if ( transcriptDirectory !== undefined ) {
const transcriptPath = resolve (
transcriptDirectory ,
` ${ scenario . id } .jsonl` ,
);
await mkdir ( dirname ( transcriptPath ), { recursive : true });
await writeFile (
transcriptPath ,
` ${ execution . events . map (( event ) => JSON . stringify ( event )). join ( "\n" ) } \n` ,
);
}
const metrics = evaluateCodexEvents (
execution . events ,
scenario . expectedFirstTool ,
{
requireEvidence : scenario . requiresEvidence ,
requiredAnswerTermGroups : scenario . requiredAnswerTermGroups ,
2026-07-22 20:38:26 +08:00
requiredToolSubsequence : scenario . requiredToolSubsequence ,
forbidInputValidationFailures : true ,
2026-07-20 17:11:06 +08:00
},
);
const result = {
id : scenario . id ,
expectedFirstTool : scenario . expectedFirstTool ,
requiresEvidence : scenario . requiresEvidence ,
qualityCriteria : scenario . requiredAnswerTermGroups ,
2026-07-22 20:38:26 +08:00
requiredToolSubsequence : scenario . requiredToolSubsequence ?? [],
2026-07-20 17:11:06 +08:00
exitCode : execution . exitCode ,
stderr : execution . stderr ,
... metrics ,
};
results . push ( result );
process . stderr . write (
2026-07-22 20:38:26 +08:00
` ${ scenario . id } : first= ${ metrics . firstTool ?? "none" } , calls= ${ String ( metrics . reaCalls . length ) } , repeats= ${ String ( metrics . repeatedCallCount ) } , validation_failures= ${ String ( metrics . inputValidationFailureCount ) } , input_tokens= ${ String ( metrics . inputTokens ) } \n` ,
2026-07-20 17:11:06 +08:00
);
}
const summary = {
2026-07-22 12:13:45 +00:00
verifier_run : await completeVerifierRun ( verifierRun ),
2026-07-20 17:11:06 +08:00
codex ,
codexVersion ,
model : optionalModel ?? null ,
scenarios : results . map (({ finalMessage : _message , ... result }) => result ),
totals : {
scenarios : results . length ,
naturalUse : results . filter (({ naturalUse }) => naturalUse ). length ,
correctFirstTool : results . filter (
({ correctFirstTool }) => correctFirstTool ,
). length ,
repeatedCallCount : results . reduce (
( total , { repeatedCallCount }) => total + repeatedCallCount ,
0 ,
),
2026-07-22 20:38:26 +08:00
inputValidationFailureCount : results . reduce (
( total , { inputValidationFailureCount }) =>
total + inputValidationFailureCount ,
0 ,
),
requiredToolSubsequence : results . filter (
({ requiredToolSubsequenceMet }) => requiredToolSubsequenceMet ,
). length ,
2026-07-20 17:11:06 +08:00
inputTokens : results . reduce (
( total , { inputTokens }) => total + inputTokens ,
0 ,
),
cachedInputTokens : results . reduce (
( total , { cachedInputTokens }) => total + cachedInputTokens ,
0 ,
),
completionQuality : results . filter (({ completionQuality }) =>
Boolean ( completionQuality ),
). length ,
authorityHonesty : results . filter (({ authorityHonesty }) =>
Boolean ( authorityHonesty ),
). length ,
},
};
const encoded = ` ${ JSON . stringify ( summary , null , 2 ) } \n` ;
process . stdout . write ( encoded );
const reportPath = process . env . REA_AGENT_EVAL_REPORT_PATH ;
if ( reportPath !== undefined ) {
await mkdir ( dirname ( resolve ( reportPath )), { recursive : true });
await writeFile ( resolve ( reportPath ), encoded );
}
const failed = results . filter (
({
exitCode ,
naturalUse ,
correctFirstTool ,
repeatedCallCount ,
2026-07-22 20:38:26 +08:00
inputValidationFailureCount ,
requiredToolSubsequenceMet ,
2026-07-20 17:11:06 +08:00
inputTokens ,
completionQuality ,
authorityHonesty ,
}) =>
exitCode !== 0 ||
! naturalUse ||
! correctFirstTool ||
repeatedCallCount !== 0 ||
2026-07-22 20:38:26 +08:00
inputValidationFailureCount !== 0 ||
! requiredToolSubsequenceMet ||
2026-07-20 17:11:06 +08:00
inputTokens <= 0 ||
! completionQuality ||
! authorityHonesty ,
);
if ( failed . length > 0 )
throw new Error (
`Codex agent release evaluation failed: ${ failed . map (({ id } ) => id).join(", ")}` ,
);
} finally {
if ( process . env . REA_AGENT_EVAL_KEEP_FIXTURES !== "true" )
await rm ( evaluationRoot , { recursive : true , force : true });
}
2026-07-29 17:36:51 +00:00
async function createTargets ( root , includeManaged ) {
2026-07-20 17:11:06 +08:00
const javascript = join ( root , "desktop-app" );
await mkdir ( join ( javascript , "renderer" ), { recursive : true });
await Promise . all ([
writeFile (
join ( javascript , "package.json" ),
` ${ JSON . stringify ({ name : "desktop-fixture" , version : "1.0.0" , main : "main.js" } , null, 2)}\n` ,
),
writeFile (
join ( javascript , "main.js" ),
'const { BrowserWindow, ipcMain } = require("electron");\nnew BrowserWindow({ webPreferences: { preload: require("node:path").join(__dirname, "preload.js"), contextIsolation: true, sandbox: true } });\nipcMain.handle("profile:read", (_event, id) => ({ id }));\n' ,
),
writeFile (
join ( javascript , "preload.js" ),
'const { contextBridge, ipcRenderer } = require("electron");\ncontextBridge.exposeInMainWorld("profileApi", { read: (id) => ipcRenderer.invoke("profile:read", id) });\n' ,
),
writeFile (
join ( javascript , "renderer/app.js" ),
'globalThis.profileApi.read("fixture");\n' ,
),
]);
const javascriptAsar = join ( root , "desktop-app.asar" );
await createPackage ( javascript , javascriptAsar );
2026-07-22 20:38:26 +08:00
const javascriptShapeLeft = join ( root , "parser-v1" );
const javascriptShapeRight = join ( root , "parser-v2" );
await Promise . all ([
mkdir ( javascriptShapeLeft , { recursive : true }),
mkdir ( javascriptShapeRight , { recursive : true }),
]);
await Promise . all ([
2026-10-05 21:15:04 +08:00
writeFile (
2026-07-22 20:38:26 +08:00
join ( javascriptShapeLeft , "parser.mjs" ),
2026-10-05 21:15:04 +08:00
'export default function parse() { return { heading: "Title" }; }\n' ,
2026-07-22 20:38:26 +08:00
),
2026-10-05 21:15:04 +08:00
writeFile (
2026-07-22 20:38:26 +08:00
join ( javascriptShapeRight , "parser.mjs" ),
2026-10-05 21:15:04 +08:00
'export default function parse() { return { heading: { text: "Title", level: 1 } }; }\n' ,
2026-07-22 20:38:26 +08:00
),
]);
2026-07-20 17:11:06 +08:00
const managedOutput = join ( root , "managed-output" );
2026-07-29 17:36:51 +00:00
if ( includeManaged ) {
const managedProject = join ( root , "managed-project" );
await mkdir ( managedProject , { recursive : true });
await Promise . all ([
writeFile (
join ( managedProject , "AgentEval.csproj" ),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>\n' ,
),
writeFile (
join ( managedProject , "Program.cs" ),
'namespace AgentEval; public sealed record Profile(string Id); public static class Program { public static void Main() => Console.WriteLine(new Profile("fixture")); }\n' ,
),
]);
await runProcess (
"dotnet" ,
[
"build" ,
join ( managedProject , "AgentEval.csproj" ),
"--configuration" ,
"Release" ,
"--output" ,
managedOutput ,
"--nologo" ,
],
root ,
120_000 ,
);
}
2026-07-20 17:11:06 +08:00
return {
native : "/bin/true" ,
javascript : javascriptAsar ,
2026-07-22 20:38:26 +08:00
javascriptShapeLeft ,
javascriptShapeRight ,
2026-07-20 17:11:06 +08:00
managed : join ( managedOutput , "AgentEval.dll" ),
};
}
2026-09-29 07:08:46 +08:00
async function runCodex ( prompt ) {
2026-07-20 17:11:06 +08:00
const arguments_ = [
"exec" ,
"--ignore-user-config" ,
"--ignore-rules" ,
"--ephemeral" ,
"--skip-git-repo-check" ,
"--sandbox" ,
"read-only" ,
"--json" ,
"--color" ,
"never" ,
"-C" ,
evaluationRoot ,
"-c" ,
'approval_policy="never"' ,
"-c" ,
`mcp_servers.rea.command= ${ JSON . stringify ( process . execPath ) } ` ,
"-c" ,
`mcp_servers.rea.args= ${ JSON . stringify ([ join ( repositoryRoot , "scripts/rea.mjs" ), "mcp" ]) } ` ,
"-c" ,
2026-07-26 15:02:58 +00:00
`mcp_servers.rea.startup_timeout_sec= ${ String ( MCP_STARTUP_POLICY . codexStartupTimeoutSeconds ) } ` ,
2026-07-20 17:11:06 +08:00
...( optionalModel === undefined ? [] : [ "--model" , optionalModel ]),
prompt ,
];
const child = spawn ( codex , arguments_ , {
cwd : evaluationRoot ,
2026-10-05 20:16:44 +08:00
env : process . env ,
2026-07-20 17:11:06 +08:00
stdio : [ "ignore" , "pipe" , "pipe" ],
});
const events = [];
const stderr = [];
const lines = createInterface ({ input : child . stdout });
const linesClosed = new Promise (( resolveClose ) =>
lines . once ( "close" , resolveClose ),
);
lines . on ( "line" , ( line ) => {
try {
events . push ( JSON . parse ( line ));
} catch {
stderr . push ( `non-json stdout: ${ line } ` );
}
});
child . stderr . on ( "data" , ( chunk ) => {
if ( stderr . join ( "" ). length < 32_768 ) stderr . push ( chunk . toString ( "utf8" ));
});
const exitCode = await new Promise (( resolveExit , reject ) => {
const timeout = setTimeout (() => {
child . kill ( "SIGTERM" );
reject (
new Error ( `Codex evaluation timed out after ${ String ( timeoutMs ) } ms` ),
);
}, timeoutMs );
child . once ( "error" , ( cause ) => {
clearTimeout ( timeout );
reject ( cause );
});
child . once ( "exit" , ( code ) => {
clearTimeout ( timeout );
resolveExit ( code ?? 1 );
});
});
await linesClosed ;
return { events , exitCode , stderr : stderr . join ( "" ). slice ( 0 , 32_768 ) };
}
async function runProcess ( command , arguments_ , cwd , timeout ) {
const child = spawn ( command , arguments_ , {
cwd ,
stdio : [ "ignore" , "ignore" , "pipe" ],
});
let stderr = "" ;
child . stderr . on ( "data" , ( chunk ) => {
if ( stderr . length < 32_768 ) stderr += chunk . toString ( "utf8" );
});
const code = await new Promise (( resolveExit , reject ) => {
const timer = setTimeout (() => {
child . kill ( "SIGTERM" );
reject ( new Error ( ` ${ command } timed out` ));
}, timeout );
child . once ( "error" , ( cause ) => {
clearTimeout ( timer );
reject ( cause );
});
child . once ( "exit" , ( value ) => {
clearTimeout ( timer );
resolveExit ( value ?? 1 );
});
});
if ( code !== 0 )
throw new Error ( ` ${ command } failed with ${ String ( code ) } : ${ stderr } ` );
}
async function readCommandVersion ( command ) {
const child = spawn ( command , [ "--version" ], {
cwd : repositoryRoot ,
stdio : [ "ignore" , "pipe" , "pipe" ],
});
let stdout = "" ;
let stderr = "" ;
child . stdout . on ( "data" , ( chunk ) => {
if ( stdout . length < 4096 ) stdout += chunk . toString ( "utf8" );
});
child . stderr . on ( "data" , ( chunk ) => {
if ( stderr . length < 4096 ) stderr += chunk . toString ( "utf8" );
});
const code = await new Promise (( resolveExit , reject ) => {
const timer = setTimeout (() => {
child . kill ( "SIGTERM" );
reject ( new Error ( ` ${ command } --version timed out` ));
}, 10_000 );
child . once ( "error" , ( cause ) => {
clearTimeout ( timer );
reject ( cause );
});
child . once ( "exit" , ( value ) => {
clearTimeout ( timer );
resolveExit ( value ?? 1 );
});
});
if ( code !== 0 )
throw new Error (
` ${ command } --version failed with ${ String ( code ) } : ${ stderr } ` ,
);
const version = stdout . trim ();
if ( version . length === 0 )
throw new Error ( ` ${ command } --version returned empty output` );
return version ;
}