Skip to content

Commit 808e9bb

Browse files
ianw-oaicopyberry
authored andcommitted
chore(codex-security): sync public projection
- [codex] track scan costs and enforce scan budgets (#11885... GitOrigin-Timestamp=2026-07-26T15:03:53-07:00 GitOrigin-RevId: d90e2acf1a5e04c91e45f41c0dfedd7534045e7e
1 parent 2779e05 commit 808e9bb

21 files changed

Lines changed: 1266 additions & 27 deletions

sdk/typescript/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ npx codex-security scan /path/to/repository --output-dir /path/outside/repositor
104104
npx codex-security scan /path/to/repository --output-dir /path/outside/repository/results --archive-existing
105105
npx codex-security scan /path/to/repository --dry-run
106106
npx codex-security scan /path/to/repository --fail-on-severity high
107+
npx codex-security scan /path/to/repository --max-cost 5
107108
npx codex-security bulk-scan
108109
npx codex-security bulk-scan repositories.csv --output-dir /path/outside/repositories/security-scans --workers 4
109110
npx codex-security scans list /path/to/repository
@@ -159,9 +160,19 @@ options for other Codex settings, such as
159160
Scan progress identifies the requested paths and reports actual ranking,
160161
file-review, validation, and attack-path phases as they become available.
161162
Completion summarizes findings, severity, coverage, elapsed time, available
162-
token and worker counts, the results directory, and the next useful command.
163+
token and worker counts, estimated cost, the results directory, and the next
164+
useful command.
163165
Progress and summaries use stderr; structured scan results remain on stdout.
164166

167+
Each scan records its model, tokens, and estimated cost in its JSON result,
168+
scan history, and bulk-scan receipt. Estimates use
169+
[standard API token prices](https://developers.openai.com/api/docs/models/compare),
170+
including cached input and cache writes; fees and surcharges are not included.
171+
172+
Use `--max-cost USD` to stop a scan, including its delegated workers, when its
173+
running cost exceeds the limit. Partial results are preserved. Requests
174+
already in progress can finish above the limit.
175+
165176
Run `npx codex-security scan --help` or `npx codex-security bulk-scan --help`
166177
for the complete CLI references.
167178

sdk/typescript/_bundled_plugin/scripts/workbench_cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def parse_args(description: str) -> argparse.Namespace:
208208
complete_scan = subparsers.add_parser("complete-scan")
209209
complete_scan.add_argument("--scan-id", required=True)
210210
complete_scan.add_argument("--claim-token")
211+
complete_scan.add_argument("--cost-json")
211212

212213
cancel_scan = subparsers.add_parser("cancel-scan")
213214
cancel_scan.add_argument("--scan-id", required=True)
@@ -217,6 +218,7 @@ def parse_args(description: str) -> argparse.Namespace:
217218
fail_scan.add_argument("--scan-id", required=True)
218219
fail_scan.add_argument("--message", required=True)
219220
fail_scan.add_argument("--claim-token")
221+
fail_scan.add_argument("--cost-json")
220222

221223
mark_handoff_delivered = subparsers.add_parser("mark-handoff-delivered")
222224
mark_handoff_delivered.add_argument("--scan-id", required=True)

sdk/typescript/_bundled_plugin/scripts/workbench_db.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
capability_preflight_input,
107107
capability_preflight_json,
108108
optional_text,
109+
parse_scan_cost,
109110
require_occurrence,
110111
require_uuid,
111112
)
@@ -1453,12 +1454,16 @@ def pin_legacy_manifest_digest(
14531454

14541455
def complete_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
14551456
scan_id = require_uuid(args.scan_id, "scan-id")
1457+
cost_json = parse_scan_cost(args.cost_json)
14561458
with scan_completion_lock(scan_id):
1457-
return complete_scan_locked(connection, scan_id, args.claim_token)
1459+
return complete_scan_locked(connection, scan_id, args.claim_token, cost_json)
14581460

14591461

14601462
def complete_scan_locked(
1461-
connection: sqlite3.Connection, scan_id: str, claim_token: str | None
1463+
connection: sqlite3.Connection,
1464+
scan_id: str,
1465+
claim_token: str | None,
1466+
cost_json: str | None,
14621467
) -> dict[str, Any]:
14631468
scan = require_scan(connection, scan_id)
14641469
if scan["status"] == "complete":
@@ -1551,10 +1556,10 @@ def complete_scan_locked(
15511556
"""
15521557
UPDATE scans
15531558
SET status = 'complete', phase = 'reporting', completed_at = ?, updated_at = ?,
1554-
seal_manifest_digest = ?
1559+
seal_manifest_digest = ?, cost_json = ?
15551560
WHERE id = ? AND status = 'running'
15561561
""",
1557-
(timestamp, timestamp, manifest_digest, scan["id"]),
1562+
(timestamp, timestamp, manifest_digest, cost_json, scan["id"]),
15581563
)
15591564
if updated.rowcount != 1:
15601565
raise SystemExit("Only a running scan can be completed.")
@@ -1750,6 +1755,7 @@ def coverage_for_comparison(scan: sqlite3.Row) -> dict[str, Any]:
17501755

17511756
def fail_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
17521757
scan_id = require_uuid(args.scan_id, "scan-id")
1758+
cost_json = parse_scan_cost(args.cost_json)
17531759
connection.execute("BEGIN IMMEDIATE")
17541760
try:
17551761
timestamp = now()
@@ -1767,10 +1773,17 @@ def fail_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[
17671773
updated = connection.execute(
17681774
"""
17691775
UPDATE scans
1770-
SET status = 'failed', failure_message = ?, completed_at = ?, updated_at = ?
1776+
SET status = 'failed', failure_message = ?, completed_at = ?, updated_at = ?,
1777+
cost_json = ?
17711778
WHERE id = ? AND status = 'running'
17721779
""",
1773-
(optional_text(args.message, maximum=2400), timestamp, timestamp, scan["id"]),
1780+
(
1781+
optional_text(args.message, maximum=2400),
1782+
timestamp,
1783+
timestamp,
1784+
cost_json,
1785+
scan["id"],
1786+
),
17741787
)
17751788
if updated.rowcount != 1:
17761789
raise SystemExit("Only a running scan can be marked failed.")
@@ -3025,6 +3038,11 @@ def scan_result(
30253038
return {
30263039
"artifacts": artifacts,
30273040
"canceledAt": scan["canceled_at"],
3041+
**(
3042+
{"cost": json.loads(scan["cost_json"], parse_constant=reject_non_finite_json)}
3043+
if scan["cost_json"] is not None
3044+
else {}
3045+
),
30283046
"contract": scan_contract(scan),
30293047
"continuationThreadId": scan["continuation_thread_id"],
30303048
"failureMessage": scan["failure_message"],

sdk/typescript/_bundled_plugin/scripts/workbench_scan_history.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def list_scans(
216216
{
217217
"completedAt": row["completed_at"],
218218
"continuationThreadId": row["continuation_thread_id"],
219+
**({"cost": json.loads(row["cost_json"])} if row["cost_json"] else {}),
219220
"findingCount": row["finding_count"],
220221
"handoffStatus": row["handoff_status"],
221222
"mode": row["mode"],

sdk/typescript/_bundled_plugin/scripts/workbench_schema.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,13 @@
595595
ON scan_comparison_matches(after_occurrence_id);
596596
""",
597597
),
598+
(
599+
24,
600+
"persist scan cost estimates",
601+
"""
602+
ALTER TABLE scans ADD COLUMN cost_json TEXT;
603+
""",
604+
),
598605
)
599606

600607

sdk/typescript/_bundled_plugin/scripts/workbench_validation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ def reject_nonstandard_json_number(value: str) -> None:
3636
raise ValueError(f"invalid JSON number {value}")
3737

3838

39+
def parse_scan_cost(value: str | None) -> str | None:
40+
if value is None:
41+
return None
42+
if len(value.encode("utf-8")) > 8192:
43+
raise SystemExit("Scan cost must be no larger than 8 KiB.")
44+
try:
45+
cost = json.loads(value, parse_constant=reject_nonstandard_json_number)
46+
except (TypeError, UnicodeError, ValueError) as exc:
47+
raise SystemExit("Scan cost must be a valid JSON object.") from exc
48+
token_keys = ("inputTokens", "cachedInputTokens", "cacheWriteInputTokens", "outputTokens")
49+
if (
50+
not isinstance(cost, dict)
51+
or not isinstance(cost.get("model"), str)
52+
or not cost["model"]
53+
or any(type(cost.get(key)) is not int or cost[key] < 0 for key in token_keys)
54+
or cost["cachedInputTokens"] + cost["cacheWriteInputTokens"] > cost["inputTokens"]
55+
or type(cost.get("estimatedUsd")) not in (int, float)
56+
or not math.isfinite(cost["estimatedUsd"])
57+
or cost["estimatedUsd"] < 0
58+
):
59+
raise SystemExit(
60+
"Scan cost must include a model, nonnegative token counts, and an estimated USD amount."
61+
)
62+
return json.dumps(cost, separators=(",", ":"), allow_nan=False)
63+
64+
3965
def capability_preflight_json(
4066
value: str | None,
4167
*,

sdk/typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"generate:models": "node scripts/generate-models.cjs",
4747
"generate:models:check": "node scripts/generate-models.cjs --check",
4848
"lint": "tsc --noEmit",
49-
"test": "bun test --timeout 30000 tests-ts/api.test.ts tests-ts/auth.test.ts tests-ts/bulk-scan-discovery.test.ts tests-ts/cli-export.test.ts tests-ts/cli.test.ts tests-ts/config.test.ts tests-ts/contract.test.ts tests-ts/knowledge-base.test.ts tests-ts/multiscan.test.ts tests-ts/result.test.ts tests-ts/runtime.test.ts tests-ts/scan-comparison.test.ts tests-ts/scan-history-renderer.test.ts tests-ts/skeleton.test.ts tests-ts/targets.test.ts tests-ts/trusted-executable.test.ts tests-ts/worker-progress.test.ts",
49+
"test": "bun test --timeout 30000 tests-ts/api.test.ts tests-ts/auth.test.ts tests-ts/bulk-scan-discovery.test.ts tests-ts/cli-export.test.ts tests-ts/cli.test.ts tests-ts/config.test.ts tests-ts/contract.test.ts tests-ts/cost.test.ts tests-ts/knowledge-base.test.ts tests-ts/multiscan.test.ts tests-ts/result.test.ts tests-ts/runtime.test.ts tests-ts/scan-comparison.test.ts tests-ts/scan-history-renderer.test.ts tests-ts/skeleton.test.ts tests-ts/targets.test.ts tests-ts/trusted-executable.test.ts tests-ts/worker-progress.test.ts",
5050
"types": "pnpm run generate:models:check && tsc --noEmit"
5151
},
5252
"dependencies": {

sdk/typescript/scripts/check-package.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const distFiles = new Set(
140140
"cli",
141141
"config",
142142
"contract",
143+
"cost",
143144
"errors",
144145
"index",
145146
"knowledge-base",

0 commit comments

Comments
 (0)