Repository navigation
test(evals): compare production Standard scan time budgets #1679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mldangelo-oai
wants to merge
23
commits into
main
Choose a base branch
from
mdangelo/codex/max-time
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
8d7ef89
feat(scan): add unified max-time investigation budgets
codex dacdff4
test(scan): give invalid minute cases unique names
codex 12abd7d
test(deep-scan): trace minute limits through worker launches
codex 55d8598
fix(cli): stabilize fractional-minute cap diagnostics
codex 618c5bd
test(evals): compare production Standard scan time budgets
codex 92d4ff0
fix(evals): require primary locations for recall
codex a36b4c2
test(deep-scan): keep worker coverage plugin-local
codex 0b8be06
test(evals): record evaluator source provenance
codex 1f0760b
Merge commit '05ff204ee048ac9eba5504cd85beea8c332b3153' into HEAD
mldangelo-oai 99b66c3
Merge commit '1f0760bdb6d48e4f5bab5075c88c0433eb3d342e' into HEAD
mldangelo-oai 998816e
Merge main into Standard scan time-budget evaluations
mldangelo-oai 0edcda9
Merge commit '004b9092b7356f4e6c9572844ee6074f9ddd8e1d' into mdangelo…
mldangelo-oai 9ef8865
Merge main owner suggestions
mldangelo-oai e90f93c
chore: merge main into Standard evaluation
mldangelo-oai 1813138
fix(evals): stop after state cleanup failures and record executable p…
mldangelo-oai 6e13449
test(ci): allow Windows diagnostic shards to finish
mldangelo-oai 9e8eead
Merge main into PR branch
mldangelo-oai 30393bc
fix(sdk): preserve exact checked file identities
mldangelo-oai 813019a
fix(sdk): preserve exact scan directory identities
mldangelo-oai bcae3d5
Merge main into PR branch
mldangelo-oai 6021c49
Merge latest main into PR branch
mldangelo-oai e4aa592
Merge latest main smoke timeout fixes into PR branch
mldangelo-oai 467392c
merge: refresh main container base
mldangelo-oai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /reports/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # Standard Scan time allowance comparison | ||
|
|
||
| Compare production Standard scans with and without `maxTimeMinutes`. Both | ||
| arms call `CodexSecurity.run`, including setup, delegation, canonical artifact | ||
| validation, report generation, and completion. The treatment uses the shared | ||
| [investigation-time-budget.md](../../plugins/codex-security/references/investigation-time-budget.md) | ||
| policy, not an eval-only prompt. | ||
|
|
||
| The SDK stamps one shared UTC deadline after setup, immediately before the | ||
| first scan turn. The allowance is advisory: there is no abort timer, automatic | ||
| continuation, or minimum runtime. Finalization is outside the allowance. | ||
|
|
||
| ## Run | ||
|
|
||
| Use the repository's dependency and | ||
| [native-artifact setup](../../plugins/codex-security/native/README.md), then run | ||
| from the repository root: | ||
|
|
||
| ```sh | ||
| pnpm --dir sdk/typescript run build:plugin | ||
| pnpm --dir sdk/typescript run build:evals | ||
| node --experimental-strip-types evals/standard-time-budget/run.mts | ||
| ``` | ||
|
|
||
| Rebuild the bundle after plugin changes. This developer eval requires an existing | ||
| `OPENAI_API_KEY` or `CODEX_API_KEY` environment variable; saved Codex or | ||
| `codex-security login` credentials are intentionally unsupported. It uses the | ||
| bundled Codex SDK without reading saved logins, provisioning credentials, or | ||
| changing persistent configuration. Production SDK and CLI login support is | ||
| unchanged. **Model runs consume usage and have no enforced time or spend cap.** | ||
|
|
||
| [experiment.json](experiment.json) selects an explicit model, two pairs | ||
| (four scans including delegated work), and five advisory minutes. Both arms | ||
| use `xhigh` reasoning and production worker settings. Pairs alternate | ||
| baseline-first and treatment-first order. Override settings with a JSON file: | ||
|
|
||
| ```sh | ||
| node --experimental-strip-types evals/standard-time-budget/run.mts /path/to/experiment.json | ||
| ``` | ||
|
|
||
| The fields are `model`, `pairs`, and `budgetMinutes`; positive fractional | ||
| minutes are accepted. Production deadline validation runs before either arm | ||
| starts. Ctrl+C saves the interrupted attempt, stops future arms, and waits for | ||
| child processes before cleaning temporary state. | ||
|
|
||
| ## Fixture and isolation | ||
|
|
||
| The synthetic application has tenant authorization, SQL injection, file | ||
| traversal, and reflected HTML bugs, each with a safe control. Source and | ||
| deployment context go to the model; gold labels and expected locations stay | ||
| in the evaluator. | ||
|
|
||
| Each arm runs in an opaque temporary directory outside the artifact tree; pair | ||
| and variant labels appear only in evaluator reports and archives. After the | ||
| client closes, its scan output is archived and its state is removed before the | ||
| next arm. If state removal fails, the failed attempt is recorded and the | ||
| comparison stops before another arm starts. Gold labels and prior-arm results | ||
| are not next to the model's working directory or included in prompts. This is layout separation, not a filesystem | ||
| security boundary: production API-key authentication and root-read permissions | ||
| remain unchanged. This eval does not test resumption or external publication. | ||
|
|
||
| ## Results | ||
|
|
||
| Artifacts are saved under ignored `reports/`, updated after every attempt: | ||
|
|
||
| - `experiment.json`: settings, evaluator/shared-helper, source, full-plugin, | ||
| and built-SDK file hashes, plus SDK/Codex package versions and the selected | ||
| Codex executable paths and SHA-256 hashes, including a `CODEX_CLI_PATH` override. | ||
| npm launchers also record their native executable. Generated reports | ||
| are excluded from evaluator hashes. | ||
| - `fixture.json`: synthetic source and held-out gold labels for review. | ||
| - `pair-<number>-<variant>/prompt.txt`: the actual first dispatched scan prompt. | ||
| - Per-arm `scan/`, `result.json`, and `report.json`: canonical scan artifacts, | ||
| semantic output, grading, usage, elapsed time, and exact failure text. | ||
| - `runs.json` and `summary.json`: all attempts and the paired comparison. | ||
|
|
||
| Elapsed time is monotonic and includes SDK setup, investigation, finalization, | ||
| scan archival, cleanup, and grading, but not subsequent evaluator JSON writing. | ||
| A canonical scan that misses a bug still completed successfully. The runner | ||
| exits nonzero for execution failures, not low recall. | ||
|
|
||
| The grader counts unique gold cases with matching CWE, a `root_control` or | ||
| `sink` source location, and authentic source evidence. Other or missing roles | ||
| do not earn automatic recall; unmatched findings require adjudication. It | ||
| consumes the production contract's repository-relative POSIX paths. Duplicates | ||
| do not increase recall. | ||
| Safe-control overlaps, unmatched findings, and citation errors are separate | ||
| signals; overlaps and unmatched findings require adjudication, not automatic | ||
| false-positive labels. Free-text location roles can flag benign context | ||
| citations. Preserve raw scores and record adjudication separately: source | ||
| matching is a heuristic, not independent exploit validation. | ||
|
|
||
| The summary separates execution success from completed-run quality. Paired | ||
| deltas are treatment minus baseline and include only complete pairs; excluded | ||
| pairs remain explicit. Each attempt's top-level `usage` includes delegated | ||
| workers, even when completion or grading fails. Missing usage stays unknown, | ||
| cached input is a subset of input tokens, and no dollar cost is inferred. | ||
|
|
||
| ## Interpretation and checks | ||
|
|
||
| Two pairs are a smoke comparison, not statistical evidence. Review findings | ||
| without variant labels and compare grounded recall, adjudicated false | ||
| positives, completion, tokens, and elapsed time together. More findings or a | ||
| longer run alone is not an improvement, and these arms do not have matched | ||
| spend. Expand the corpus if both arms saturate the fixture; use more pairs and | ||
| pinned public repository revisions before drawing quality/cost conclusions. | ||
|
|
||
| After the builds above, run deterministic checks without model calls: | ||
|
|
||
| ```sh | ||
| node --experimental-strip-types --test evals/standard-time-budget/test_*.mts evals/secret-discovery/test_*.mts | ||
| ``` | ||
|
|
||
| The integration test uses the real SDK and canonical artifact helpers with | ||
| mocked model/workbench boundaries, verifying isolated state, completion, prompt | ||
| capture, and parent-plus-worker usage. CI reuses its host native build, builds | ||
| the plugin with `build_mcp_app.mjs --native host`, and links it at | ||
| `sdk/typescript/_bundled_plugin`; universal release artifacts are not required. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "model": "gpt-5.6-sol", | ||
| "pairs": 2, | ||
| "budgetMinutes": 5 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import { createHash } from "node:crypto"; | ||
| import { readdir, readFile } from "node:fs/promises"; | ||
| import { join } from "node:path"; | ||
| import type { GradeReport, Result } from "./grade.mts"; | ||
| import type { PreparedStandardEval } from "./fixtures.mts"; | ||
| import type { StandardRunner, StandardRunOptions } from "./production.mts"; | ||
|
|
||
| export interface ExperimentConfig { | ||
| model: string; | ||
| pairs: number; | ||
| budgetMinutes: number; | ||
| } | ||
| export type Variant = "baseline" | "time-aware"; | ||
| /** Retained arm state must not be exposed to a later arm. */ | ||
| export class StateCleanupError extends AggregateError {} | ||
| export type RunReport = GradeReport & { | ||
| scanId?: string; | ||
| scanDir?: string; | ||
| reportPath?: string; | ||
| }; | ||
| interface RunMetadata { | ||
| pair: number; | ||
| variant: Variant; | ||
| startedAt: string; | ||
| advisoryBudgetMinutes: number | null; | ||
| promptSha256: string | null; | ||
| elapsedMs: number; | ||
| usage: unknown; | ||
| } | ||
| type RunOutcome = | ||
| | { status: "completed"; report: RunReport; error?: never } | ||
| | { status: "failed"; error: string; report?: never }; | ||
| export type RunRecord = RunMetadata & RunOutcome; | ||
| export type CompletedRunRecord = Extract<RunRecord, { status: "completed" }>; | ||
| interface ComparisonOptions { | ||
| prepared: PreparedStandardEval; | ||
| config: ExperimentConfig; | ||
| signal?: AbortSignal; | ||
| run: StandardRunner; | ||
| elapsedNow?: () => number; | ||
| onStart?: (record: Pick<RunMetadata, "pair" | "variant">) => Promise<unknown>; | ||
| onResult?: ( | ||
| record: RunRecord, | ||
| result: Result | undefined, | ||
| records: RunRecord[], | ||
| prompt: string | undefined, | ||
| ) => Promise<unknown>; | ||
| } | ||
|
|
||
| export function validateExperiment(config: ExperimentConfig) { | ||
| if (typeof config?.model !== "string" || !config.model.trim()) { | ||
| throw new Error("The experiment requires an explicit model."); | ||
| } | ||
| if (!Number.isSafeInteger(config.pairs) || config.pairs < 1) { | ||
| throw new Error("Experiment pairs must be a positive integer."); | ||
| } | ||
| if (!Number.isFinite(config.budgetMinutes) || config.budgetMinutes <= 0) { | ||
| throw new Error("Experiment budgetMinutes must be a positive number."); | ||
| } | ||
|
mldangelo-oai marked this conversation as resolved.
|
||
| return { | ||
| model: config.model, | ||
| pairs: config.pairs, | ||
| budgetMinutes: config.budgetMinutes, | ||
| }; | ||
| } | ||
|
|
||
| export function hashText(text: string | Uint8Array) { | ||
| return createHash("sha256").update(text).digest("hex"); | ||
| } | ||
|
|
||
| export async function snapshotEvaluator(directory = import.meta.dirname) { | ||
| const files: Record<string, string> = {}; | ||
| for (const path of [ | ||
| "run.mts", | ||
| "production.mts", | ||
| "experiment.mts", | ||
| "fixtures.mts", | ||
| "grade.mts", | ||
| "summary.mts", | ||
| "../secret-discovery/fixtures.mts", | ||
| "../secret-discovery/runtime.mts", | ||
| ]) { | ||
| files[path] = hashText(await readFile(join(directory, path))); | ||
| } | ||
| return files; | ||
| } | ||
|
|
||
| export async function snapshotFiles(directory: string, prefix = "") { | ||
| const files: Record<string, string> = {}; | ||
| const entries = await readdir(directory, { withFileTypes: true }); | ||
| for (const entry of entries.sort((first, second) => | ||
| first.name.localeCompare(second.name), | ||
| )) { | ||
| const relative = prefix ? `${prefix}/${entry.name}` : entry.name; | ||
| const path = join(directory, entry.name); | ||
| if (entry.isDirectory()) { | ||
| Object.assign(files, await snapshotFiles(path, relative)); | ||
| } else { | ||
| files[relative] = hashText(await readFile(path)); | ||
| } | ||
| } | ||
| return files; | ||
| } | ||
|
|
||
| export async function runComparison({ | ||
| prepared, | ||
| config, | ||
| signal, | ||
| run, | ||
| elapsedNow = () => performance.now(), | ||
| onStart = async () => {}, | ||
| onResult = async () => {}, | ||
| }: ComparisonOptions) { | ||
| config = validateExperiment(config); | ||
| const records: RunRecord[] = []; | ||
| for (let pair = 1; pair <= config.pairs; pair += 1) { | ||
| const variants: Variant[] = | ||
| pair % 2 === 1 ? ["baseline", "time-aware"] : ["time-aware", "baseline"]; | ||
| for (const variant of variants) { | ||
| signal?.throwIfAborted(); | ||
| await onStart({ pair, variant }); | ||
| signal?.throwIfAborted(); | ||
| const startedAt = new Date().toISOString(); | ||
| const startedElapsed = elapsedNow(); | ||
| const observation: StandardRunOptions["observation"] = {}; | ||
| let outcome: RunOutcome; | ||
| let semanticResult: Result | undefined; | ||
| let cleanupFailure: StateCleanupError | undefined; | ||
| try { | ||
| const result = await run(prepared, { | ||
| model: config.model, | ||
| pair, | ||
| variant, | ||
| signal, | ||
| ...(variant === "time-aware" | ||
| ? { maxTimeMinutes: config.budgetMinutes } | ||
| : {}), | ||
| observation, | ||
| }); | ||
| outcome = { status: "completed", report: result.report }; | ||
| semanticResult = result.semanticResult; | ||
| } catch (error) { | ||
| if (error instanceof StateCleanupError) cleanupFailure = error; | ||
| outcome = { | ||
| status: "failed", | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| const elapsedMs = elapsedNow() - startedElapsed; | ||
| const record: RunRecord = { | ||
| pair, | ||
| variant, | ||
| startedAt, | ||
| advisoryBudgetMinutes: | ||
| variant === "time-aware" ? config.budgetMinutes : null, | ||
| promptSha256: | ||
| observation.prompt === undefined | ||
| ? null | ||
| : hashText(observation.prompt), | ||
| ...outcome, | ||
| elapsedMs, | ||
| usage: observation.usage ?? null, | ||
| }; | ||
| records.push(record); | ||
| await onResult(record, semanticResult, records, observation.prompt); | ||
| if (cleanupFailure !== undefined) throw cleanupFailure; | ||
| signal?.throwIfAborted(); | ||
| } | ||
| } | ||
| return records; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.