Skip to content

Commit bc1125a

Browse files
committed
Bound Codex instruction fetches and derive eval diagnostics from the real session
1 parent 84e5313 commit bc1125a

4 files changed

Lines changed: 62 additions & 16 deletions

File tree

‎scripts/eval-capability.test.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,21 +256,28 @@ describe("initEvalGitRepo", () => {
256256
});
257257

258258
describe("buildEvalDiagnostics", () => {
259-
test("non-Codex provider gets a null instructions hash and the advertised tool list", () => {
260-
const diagnostics = buildEvalDiagnostics(sampleConfig({ providerName: "openai" }));
259+
test("non-Codex provider gets a null instructions hash and the default orchestrator tool list", async () => {
260+
const diagnostics = await buildEvalDiagnostics(sampleConfig({ providerName: "openai" }));
261261
expect(diagnostics.codexInstructionsHash).toBeNull();
262262
expect(diagnostics.advertisedTools).toContain("read_file");
263263
expect(diagnostics.advertisedTools).toContain("run_shell");
264264
expect(diagnostics.reasoningEffort).toBeNull();
265265
});
266266

267-
test("Codex provider gets a non-null instructions hash", () => {
268-
const diagnostics = buildEvalDiagnostics(sampleConfig({ providerName: "codex/default" }));
267+
test("Codex provider gets a non-null instructions hash", async () => {
268+
const diagnostics = await buildEvalDiagnostics(sampleConfig({ providerName: "codex/default" }));
269269
expect(diagnostics.codexInstructionsHash).toMatch(/^[0-9a-f]{12}$/);
270270
});
271271

272-
test("echoes back the configured reasoning effort", () => {
273-
const diagnostics = buildEvalDiagnostics(sampleConfig({ reasoningEffort: "high" }));
272+
test("echoes back the configured reasoning effort", async () => {
273+
const diagnostics = await buildEvalDiagnostics(sampleConfig({ reasoningEffort: "high" }));
274274
expect(diagnostics.reasoningEffort).toBe("high");
275275
});
276+
277+
test("--director build reports the director's own advertised allowlist", async () => {
278+
const diagnostics = await buildEvalDiagnostics(sampleConfig({ director: "build" }));
279+
expect(diagnostics.advertisedTools).not.toEqual(
280+
(await buildEvalDiagnostics(sampleConfig({}))).advertisedTools,
281+
);
282+
});
276283
});

‎scripts/eval-capability.ts‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import { join, dirname, resolve } from "node:path";
1515
import { fileURLToPath } from "node:url";
1616
import { spawn } from "node:child_process";
1717
import { loadConfig, type Config } from "../src/config/index.js";
18-
import { runExec } from "../src/exec/runner.js";
18+
import { runExec, resolveExecDirectorOverlay } from "../src/exec/runner.js";
1919
import { SETTINGS_DIR_NAME } from "../src/branding.js";
2020
import { codexProfileFromProviderName } from "../src/config/codex-providers.js";
2121
import { codexInstructionsHash } from "../src/auth/codex/instructions.js";
2222
import { advertisedToolNamesForSessionMode } from "../src/agent/tool-search.js";
2323
import { detectLanguageServerAvailable } from "../src/agent/lsp-availability.js";
24+
import { resolveSessionMode } from "../src/config/session-mode.js";
25+
import { loadLocalSettings, localSettingsPath } from "../src/config/settings.js";
2426
import {
2527
loadEvalCases,
2628
filterCases,
@@ -539,14 +541,24 @@ async function resolveVariantLabels(
539541
/**
540542
* Per-cell diagnostics for debugging eval failures: which Codex instructions
541543
* text was pinned, which built-in tools the model was offered, and the
542-
* requested reasoning effort. Mirrors the exec runner's own session-mode/tool
543-
* gating (see src/agent/tool-search.ts) rather than re-running toolset setup.
544+
* requested reasoning effort. Reuses the exec runner's own resolution
545+
* (resolveSessionMode, resolveExecDirectorOverlay) rather than forking the
546+
* logic, so a --director overlay or a non-default session mode here reports
547+
* the same advertised list exec actually runs with.
548+
*
549+
* reasoningEffort echoes the configured value, not the provider's internal
550+
* default when unset — accepted as-is per review.
544551
*/
545-
export function buildEvalDiagnostics(config: Config): EvalDiagnostics {
552+
export async function buildEvalDiagnostics(config: Config): Promise<EvalDiagnostics> {
546553
const codexProfile = codexProfileFromProviderName(config.providerName);
547-
const advertisedTools = advertisedToolNamesForSessionMode("orchestrator", {
548-
languageServerAvailable: detectLanguageServerAvailable(config.cwd),
549-
});
554+
const localSettings = await loadLocalSettings(localSettingsPath(config.cwd)).catch(() => null);
555+
const sessionMode = resolveSessionMode(config.settings, localSettings) ?? "orchestrator";
556+
const overlay = resolveExecDirectorOverlay(config.director);
557+
const advertisedTools =
558+
overlay.advertisedAllow ??
559+
advertisedToolNamesForSessionMode(sessionMode, {
560+
languageServerAvailable: detectLanguageServerAvailable(config.cwd),
561+
});
550562
return {
551563
codexInstructionsHash: codexProfile !== undefined ? codexInstructionsHash() : null,
552564
advertisedTools,
@@ -657,7 +669,7 @@ async function runCase(
657669
);
658670
}
659671

660-
const diagnostics = buildEvalDiagnostics(config);
672+
const diagnostics = await buildEvalDiagnostics(config);
661673
const agentStarted = Date.now();
662674
// runExec runs the agent in-process (no child, unlike verify.sh below), so
663675
// the fixture origin must reach it via process.env directly for the

‎src/auth/codex/instructions.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ describe("refreshCodexInstructions", () => {
8989
expect(codexInstructions()).toBe(before);
9090
});
9191

92+
test("rejects within the timeout when a fetch never resolves (hung connection)", async () => {
93+
const before = codexInstructions();
94+
global.fetch = ((_input: RequestInfo | URL, init?: RequestInit) => {
95+
return new Promise((_resolve, reject) => {
96+
const signal = init?.signal;
97+
if (signal) {
98+
signal.addEventListener("abort", () => reject(signal.reason as Error));
99+
}
100+
});
101+
}) as unknown as typeof fetch;
102+
103+
const started = Date.now();
104+
await expect(refreshCodexInstructions()).rejects.toBeTruthy();
105+
expect(Date.now() - started).toBeLessThan(15_000);
106+
expect(codexInstructions()).toBe(before);
107+
}, 20_000);
108+
92109
test("codexInstructionsHash reflects the currently resolved instructions text", async () => {
93110
const hashBefore = codexInstructionsHash();
94111
expect(hashBefore).toMatch(/^[0-9a-f]{12}$/);

‎src/auth/codex/instructions.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const PROMPT_PATH = "codex-rs/core/gpt_5_codex_prompt.md";
2020
const PROMPT_SENTINEL = "You are Codex";
2121
const MIN_PROMPT_LENGTH = 1000;
2222

23+
// Bounds both network calls below so a black-holed connection can never hang
24+
// exec boot, which awaits refreshCodexInstructions before first inference.
25+
const CODEX_INSTRUCTIONS_TIMEOUT_MS = 10_000;
26+
2327
export function isValidCodexPrompt(text: string): boolean {
2428
return text.length >= MIN_PROMPT_LENGTH && text.startsWith(PROMPT_SENTINEL);
2529
}
@@ -52,7 +56,10 @@ export function codexInstructionsHash(): string {
5256
}
5357

5458
async function latestReleaseTag(): Promise<string> {
55-
const res = await fetch(RELEASES_LATEST, { headers: { accept: "application/vnd.github+json" } });
59+
const res = await fetch(RELEASES_LATEST, {
60+
headers: { accept: "application/vnd.github+json" },
61+
signal: AbortSignal.timeout(CODEX_INSTRUCTIONS_TIMEOUT_MS),
62+
});
5663
if (!res.ok) throw new Error(`Codex release lookup failed (HTTP ${String(res.status)}).`);
5764
const data = (await res.json()) as { tag_name?: unknown };
5865
if (typeof data.tag_name !== "string") throw new Error("Codex release lookup returned no tag.");
@@ -61,7 +68,10 @@ async function latestReleaseTag(): Promise<string> {
6168

6269
export async function refreshCodexInstructions(): Promise<void> {
6370
const tag = await latestReleaseTag();
64-
const res = await fetch(`https://raw.githubusercontent.com/openai/codex/${encodeURIComponent(tag)}/${PROMPT_PATH}`);
71+
const res = await fetch(
72+
`https://raw.githubusercontent.com/openai/codex/${encodeURIComponent(tag)}/${PROMPT_PATH}`,
73+
{ signal: AbortSignal.timeout(CODEX_INSTRUCTIONS_TIMEOUT_MS) },
74+
);
6575
if (!res.ok) throw new Error(`Codex prompt fetch failed (HTTP ${String(res.status)}).`);
6676
const text = await res.text();
6777
if (!isValidCodexPrompt(text)) throw new Error("Codex prompt fetch returned an unexpected body.");

0 commit comments

Comments
 (0)