Skip to content

Commit a8fc009

Browse files
authored
feat: expose deep scan controls (#188)
1 parent 3bf2621 commit a8fc009

9 files changed

Lines changed: 429 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ npm install @openai/codex-security
1616
npx @openai/codex-security login
1717
npx @openai/codex-security scan .
1818
npx @openai/codex-security scan . --model gpt-5.6-terra --effort high
19+
npx @openai/codex-security scan . --mode deep --workers 2 --subagents 0 --stop-after-no-new 3 --max-discovery-runs 10
1920
```
2021

2122
For CI, set `OPENAI_API_KEY` or `CODEX_API_KEY` instead of signing in. Environment API keys are
@@ -58,6 +59,13 @@ import { CodexSecurity } from "@openai/codex-security";
5859

5960
const security = new CodexSecurity();
6061
const result = await security.run(".");
62+
await security.run(".", {
63+
mode: "deep",
64+
workers: 2,
65+
subagents: 0,
66+
stopAfterNoNew: 3,
67+
maxDiscoveryRuns: 10,
68+
});
6169

6270
console.log(result.reportPath);
6371
await security.close();

sdk/typescript/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ npx @openai/codex-security scan /path/to/repository --output-dir /path/outside/r
183183
npx @openai/codex-security scan /path/to/repository --dry-run
184184
npx @openai/codex-security scan /path/to/repository --fail-on-severity high
185185
npx @openai/codex-security scan /path/to/repository --max-cost 5
186+
npx @openai/codex-security scan /path/to/repository --mode deep --workers 2 --subagents 0 --stop-after-no-new 3 --max-discovery-runs 10
186187
npx @openai/codex-security install-hook
187188
npx @openai/codex-security bulk-scan
188189
npx @openai/codex-security bulk-scan --model gpt-5.6-terra --effort high
@@ -224,6 +225,37 @@ to
224225
Repeat `--knowledge-base PATH` for multiple files or directories. Directories are
225226
searched recursively for Markdown, text, PDF, and Word (`.docx`) files.
226227

228+
### Configure deep scans
229+
230+
For `scan --mode deep`, `--workers` limits concurrent discovery workers,
231+
`--subagents` controls each worker's subagents, `--stop-after-no-new` stops after
232+
that many runs find no new issues, and `--max-discovery-runs` limits total runs.
233+
These options are also available on SDK scans:
234+
235+
```ts
236+
await security.run("/path/to/repository", {
237+
mode: "deep",
238+
workers: 2,
239+
subagents: 0,
240+
stopAfterNoNew: 3,
241+
maxDiscoveryRuns: 10,
242+
});
243+
```
244+
245+
Set defaults in `~/.codex/codex-security/config.toml`, or under `$CODEX_HOME`
246+
when it is configured. Explicit CLI and SDK settings override these defaults:
247+
248+
```toml
249+
[deep_scan]
250+
workers = 2
251+
subagents = 0
252+
stop_after_no_new = 3
253+
max_discovery_runs = 10
254+
```
255+
256+
`scan --workers` controls discovery workers within one deep scan;
257+
`bulk-scan --workers` controls how many repositories are scanned concurrently.
258+
227259
On macOS/Linux, an existing output directory must be private to the current
228260
user (`chmod 700`).
229261

sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,30 @@ def begin_deep_scan_for_scan(
531531
args: argparse.Namespace,
532532
) -> dict[str, Any]:
533533
scan_id = require_uuid(scan_id, "scan-id")
534+
candidate = require_scan(connection, scan_id)
535+
workspace = require_workspace(connection, candidate["workspace_id"])
536+
if (
537+
candidate["mode"] == "deep"
538+
and candidate["status"] == "running"
539+
and candidate["recipe_json"] is not None
540+
and candidate["handoff_status"] == "delivered"
541+
and candidate["deep_scan_owner_thread_id"] is None
542+
and workspace["thread_id"] is None
543+
):
544+
timestamp = now()
545+
with connection:
546+
claimed_workspace = connection.execute(
547+
"UPDATE workspaces SET thread_id = ?, updated_at = ? "
548+
"WHERE id = ? AND thread_id IS NULL",
549+
(thread_id, timestamp, workspace["id"]),
550+
)
551+
claimed_scan = connection.execute(
552+
"UPDATE scans SET deep_scan_owner_thread_id = ?, updated_at = ? "
553+
"WHERE id = ? AND deep_scan_owner_thread_id IS NULL",
554+
(thread_id, timestamp, scan_id),
555+
)
556+
if claimed_workspace.rowcount != 1 or claimed_scan.rowcount != 1:
557+
raise SystemExit("A scan can only be orchestrated from its owning Codex thread.")
534558
scan, _ = require_owned_scan(connection, scan_id, thread_id)
535559
require_current_continuation(
536560
scan,

sdk/typescript/src/api.ts

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
/// <reference lib="esnext.disposable" preserve="true" />
22

3-
import { chmod, lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
3+
import {
4+
chmod,
5+
lstat,
6+
mkdir,
7+
readFile,
8+
realpath,
9+
rm,
10+
writeFile,
11+
} from "node:fs/promises";
412
import { randomUUID } from "node:crypto";
513
import { homedir, tmpdir } from "node:os";
614
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
715
import { Codex, type CodexOptions } from "@openai/codex-sdk";
16+
import {
17+
parse as parseToml,
18+
stringify as stringifyToml,
19+
type TomlTable,
20+
} from "smol-toml";
821
import {
922
accountStatus,
1023
CodexLoginHandle,
@@ -119,7 +132,14 @@ interface PreparedRuntime {
119132
effectiveConfig?: JsonObject;
120133
}
121134

122-
export interface ScanOptions {
135+
export interface DeepScanOptions {
136+
workers?: number;
137+
subagents?: number;
138+
stopAfterNoNew?: number;
139+
maxDiscoveryRuns?: number;
140+
}
141+
142+
export interface ScanOptions extends DeepScanOptions {
123143
auth?: ScanAuthMode;
124144
target?: ScanTarget;
125145
mode?: ScanMode;
@@ -174,7 +194,7 @@ type ScanObserverName =
174194
| "onWorkerStatus"
175195
| "onWarning";
176196

177-
export interface ScanPreflight {
197+
export interface ScanPreflight extends DeepScanOptions {
178198
repository: string;
179199
target: NormalizedTarget;
180200
mode: ScanMode;
@@ -219,6 +239,12 @@ const DEFAULT_DEPENDENCIES: ClientDependencies = {
219239
};
220240

221241
const SCAN_PERMISSION_PROFILE = "codex_security_scan";
242+
const DEEP_SCAN_SETTINGS = [
243+
["workers", "workers", 1],
244+
["subagents", "subagents", 0],
245+
["stopAfterNoNew", "stop_after_no_new", 1],
246+
["maxDiscoveryRuns", "max_discovery_runs", 1],
247+
] as const;
222248

223249
export class CodexSecurity {
224250
public readonly config: Readonly<CodexSecurityConfig>;
@@ -282,6 +308,7 @@ export class CodexSecurity {
282308
repository: inputs.repository,
283309
target: inputs.target,
284310
mode: inputs.mode,
311+
...deepScanOptions(options),
285312
...(options.knowledgeBasePaths?.length
286313
? { knowledgeBasePaths: options.knowledgeBasePaths }
287314
: {}),
@@ -409,6 +436,14 @@ export class CodexSecurity {
409436
}
410437
const runtimeHome = await realpath(runtime.codexHome);
411438
requireOutputOutsideRepository(protectedRoot, runtimeHome, "runtime");
439+
if (mode === "deep") {
440+
await prepareDeepScanConfig(
441+
runtimeHome,
442+
this.#dependencies.environment,
443+
options,
444+
signal,
445+
);
446+
}
412447
if (
413448
options.expectedPluginVersion !== undefined &&
414449
runtime.plugin.version !== options.expectedPluginVersion
@@ -579,6 +614,7 @@ export class CodexSecurity {
579614
options.failureSeverity,
580615
knowledgeBase?.sources,
581616
options.maxCostUsd,
617+
deepScanOptions(options),
582618
);
583619
const workbenchOptions: WorkbenchCommandOptions = {
584620
python,
@@ -1200,6 +1236,7 @@ export class CodexSecurity {
12001236
options: ScanOptions,
12011237
signal?: AbortSignal,
12021238
): Promise<LocalScanInputs> {
1239+
deepScanOptions(options);
12031240
if (
12041241
options.maxCostUsd !== undefined &&
12051242
(!Number.isFinite(options.maxCostUsd) || options.maxCostUsd <= 0)
@@ -1340,6 +1377,71 @@ export class CodexSecurity {
13401377
}
13411378
}
13421379

1380+
function deepScanOptions(options: ScanOptions): DeepScanOptions {
1381+
const selected: DeepScanOptions = {};
1382+
for (const [name, , minimum] of DEEP_SCAN_SETTINGS) {
1383+
const value = options[name];
1384+
if (value === undefined) continue;
1385+
if ((options.mode ?? "standard") !== "deep") {
1386+
throw new CodexSecurityError("Deep scan settings require deep mode.");
1387+
}
1388+
if (!Number.isSafeInteger(value) || value < minimum) {
1389+
throw new CodexSecurityError(
1390+
`Deep scan ${name} must be ${minimum === 0 ? "a non-negative" : "a positive"} integer.`,
1391+
);
1392+
}
1393+
selected[name] = value;
1394+
}
1395+
return selected;
1396+
}
1397+
1398+
async function prepareDeepScanConfig(
1399+
codexHome: string,
1400+
environment: ProcessEnvironment,
1401+
options: DeepScanOptions,
1402+
signal: AbortSignal,
1403+
): Promise<void> {
1404+
const ambientHome =
1405+
environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex");
1406+
const source = join(ambientHome, "codex-security", "config.toml");
1407+
let configured: TomlTable = {};
1408+
try {
1409+
configured = parseToml(
1410+
await readFile(source, { encoding: "utf8", signal }),
1411+
);
1412+
} catch (error) {
1413+
if (!isRecord(error) || error["code"] !== "ENOENT") {
1414+
throw new CodexSecurityError(
1415+
`Cannot read Codex Security configuration at ${source}.`,
1416+
{ cause: error },
1417+
);
1418+
}
1419+
}
1420+
const existing = configured["deep_scan"];
1421+
if (existing !== undefined && !isRecord(existing)) {
1422+
throw new CodexSecurityError(
1423+
`Codex Security configuration [deep_scan] at ${source} must be a TOML table.`,
1424+
);
1425+
}
1426+
const overrides: TomlTable = {};
1427+
for (const [name, key] of DEEP_SCAN_SETTINGS) {
1428+
const value = options[name];
1429+
if (value !== undefined) overrides[key] = value;
1430+
}
1431+
if (existing === undefined && Object.keys(overrides).length === 0) return;
1432+
const destination = join(codexHome, "codex-security", "config.toml");
1433+
if (destination === source && Object.keys(overrides).length === 0) return;
1434+
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
1435+
await writeFile(
1436+
destination,
1437+
stringifyToml({
1438+
...configured,
1439+
deep_scan: { ...existing, ...overrides },
1440+
}),
1441+
{ mode: 0o600, signal },
1442+
);
1443+
}
1444+
13431445
export async function initialCredentialsAvailable(
13441446
environment: ProcessEnvironment,
13451447
ambientHome: string,
@@ -1552,6 +1654,11 @@ async function scanPrompt(
15521654
return [
15531655
`Use the installed $codex-security:${skillName} skill at "$CODEX_SECURITY_PLUGIN_ROOT/skills/${skillName}/SKILL.md".`,
15541656
"Run this Codex Security scan non-interactively.",
1657+
...(mode === "deep"
1658+
? [
1659+
'The SDK has already registered this scan. Call start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }; never pass targetPath or create another scan.',
1660+
]
1661+
: []),
15551662
...(skillName === "deep-security-scan"
15561663
? []
15571664
: [
@@ -1618,6 +1725,7 @@ function scanRecipe(
16181725
failOnSeverity?: SeverityLevel,
16191726
knowledgeBasePaths?: string[],
16201727
maxCostUsd?: number,
1728+
deepScan?: DeepScanOptions,
16211729
): JsonObject {
16221730
return {
16231731
repository,
@@ -1636,6 +1744,9 @@ function scanRecipe(
16361744
...(failOnSeverity === undefined ? {} : { failOnSeverity }),
16371745
...(knowledgeBasePaths === undefined ? {} : { knowledgeBasePaths }),
16381746
...(maxCostUsd === undefined ? {} : { maxCostUsd }),
1747+
...(deepScan === undefined || Object.keys(deepScan).length === 0
1748+
? {}
1749+
: { deepScan: { ...deepScan } }),
16391750
};
16401751
}
16411752

0 commit comments

Comments
 (0)