diff --git a/README.md b/README.md
index e0344ac..c70d402 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ npm install @openai/codex-security
npx @openai/codex-security login
npx @openai/codex-security scan .
npx @openai/codex-security scan . --model gpt-5.6-terra --effort high
+npx @openai/codex-security scan . --mode deep --workers 2 --subagents 0 --stop-after-no-new 3 --max-discovery-runs 10
```
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";
const security = new CodexSecurity();
const result = await security.run(".");
+await security.run(".", {
+ mode: "deep",
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
+});
console.log(result.reportPath);
await security.close();
diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md
index 21f02ce..12aa405 100644
--- a/sdk/typescript/README.md
+++ b/sdk/typescript/README.md
@@ -183,6 +183,7 @@ npx @openai/codex-security scan /path/to/repository --output-dir /path/outside/r
npx @openai/codex-security scan /path/to/repository --dry-run
npx @openai/codex-security scan /path/to/repository --fail-on-severity high
npx @openai/codex-security scan /path/to/repository --max-cost 5
+npx @openai/codex-security scan /path/to/repository --mode deep --workers 2 --subagents 0 --stop-after-no-new 3 --max-discovery-runs 10
npx @openai/codex-security install-hook
npx @openai/codex-security bulk-scan
npx @openai/codex-security bulk-scan --model gpt-5.6-terra --effort high
@@ -224,6 +225,37 @@ to
Repeat `--knowledge-base PATH` for multiple files or directories. Directories are
searched recursively for Markdown, text, PDF, and Word (`.docx`) files.
+### Configure deep scans
+
+For `scan --mode deep`, `--workers` limits concurrent discovery workers,
+`--subagents` controls each worker's subagents, `--stop-after-no-new` stops after
+that many runs find no new issues, and `--max-discovery-runs` limits total runs.
+These options are also available on SDK scans:
+
+```ts
+await security.run("/path/to/repository", {
+ mode: "deep",
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
+});
+```
+
+Set defaults in `~/.codex/codex-security/config.toml`, or under `$CODEX_HOME`
+when it is configured. Explicit CLI and SDK settings override these defaults:
+
+```toml
+[deep_scan]
+workers = 2
+subagents = 0
+stop_after_no_new = 3
+max_discovery_runs = 10
+```
+
+`scan --workers` controls discovery workers within one deep scan;
+`bulk-scan --workers` controls how many repositories are scanned concurrently.
+
On macOS/Linux, an existing output directory must be private to the current
user (`chmod 700`).
diff --git a/sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py b/sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py
index e9b9d84..91df5ec 100644
--- a/sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py
+++ b/sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py
@@ -531,6 +531,30 @@ def begin_deep_scan_for_scan(
args: argparse.Namespace,
) -> dict[str, Any]:
scan_id = require_uuid(scan_id, "scan-id")
+ candidate = require_scan(connection, scan_id)
+ workspace = require_workspace(connection, candidate["workspace_id"])
+ if (
+ candidate["mode"] == "deep"
+ and candidate["status"] == "running"
+ and candidate["recipe_json"] is not None
+ and candidate["handoff_status"] == "delivered"
+ and candidate["deep_scan_owner_thread_id"] is None
+ and workspace["thread_id"] is None
+ ):
+ timestamp = now()
+ with connection:
+ claimed_workspace = connection.execute(
+ "UPDATE workspaces SET thread_id = ?, updated_at = ? "
+ "WHERE id = ? AND thread_id IS NULL",
+ (thread_id, timestamp, workspace["id"]),
+ )
+ claimed_scan = connection.execute(
+ "UPDATE scans SET deep_scan_owner_thread_id = ?, updated_at = ? "
+ "WHERE id = ? AND deep_scan_owner_thread_id IS NULL",
+ (thread_id, timestamp, scan_id),
+ )
+ if claimed_workspace.rowcount != 1 or claimed_scan.rowcount != 1:
+ raise SystemExit("A scan can only be orchestrated from its owning Codex thread.")
scan, _ = require_owned_scan(connection, scan_id, thread_id)
require_current_continuation(
scan,
diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts
index 32269c0..888431f 100644
--- a/sdk/typescript/src/api.ts
+++ b/sdk/typescript/src/api.ts
@@ -1,10 +1,23 @@
///
-import { chmod, lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
+import {
+ chmod,
+ lstat,
+ mkdir,
+ readFile,
+ realpath,
+ rm,
+ writeFile,
+} from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { homedir, tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
import { Codex, type CodexOptions } from "@openai/codex-sdk";
+import {
+ parse as parseToml,
+ stringify as stringifyToml,
+ type TomlTable,
+} from "smol-toml";
import {
accountStatus,
CodexLoginHandle,
@@ -119,7 +132,14 @@ interface PreparedRuntime {
effectiveConfig?: JsonObject;
}
-export interface ScanOptions {
+export interface DeepScanOptions {
+ workers?: number;
+ subagents?: number;
+ stopAfterNoNew?: number;
+ maxDiscoveryRuns?: number;
+}
+
+export interface ScanOptions extends DeepScanOptions {
auth?: ScanAuthMode;
target?: ScanTarget;
mode?: ScanMode;
@@ -174,7 +194,7 @@ type ScanObserverName =
| "onWorkerStatus"
| "onWarning";
-export interface ScanPreflight {
+export interface ScanPreflight extends DeepScanOptions {
repository: string;
target: NormalizedTarget;
mode: ScanMode;
@@ -219,6 +239,12 @@ const DEFAULT_DEPENDENCIES: ClientDependencies = {
};
const SCAN_PERMISSION_PROFILE = "codex_security_scan";
+const DEEP_SCAN_SETTINGS = [
+ ["workers", "workers", 1],
+ ["subagents", "subagents", 0],
+ ["stopAfterNoNew", "stop_after_no_new", 1],
+ ["maxDiscoveryRuns", "max_discovery_runs", 1],
+] as const;
export class CodexSecurity {
public readonly config: Readonly;
@@ -282,6 +308,7 @@ export class CodexSecurity {
repository: inputs.repository,
target: inputs.target,
mode: inputs.mode,
+ ...deepScanOptions(options),
...(options.knowledgeBasePaths?.length
? { knowledgeBasePaths: options.knowledgeBasePaths }
: {}),
@@ -409,6 +436,14 @@ export class CodexSecurity {
}
const runtimeHome = await realpath(runtime.codexHome);
requireOutputOutsideRepository(protectedRoot, runtimeHome, "runtime");
+ if (mode === "deep") {
+ await prepareDeepScanConfig(
+ runtimeHome,
+ this.#dependencies.environment,
+ options,
+ signal,
+ );
+ }
if (
options.expectedPluginVersion !== undefined &&
runtime.plugin.version !== options.expectedPluginVersion
@@ -579,6 +614,7 @@ export class CodexSecurity {
options.failureSeverity,
knowledgeBase?.sources,
options.maxCostUsd,
+ deepScanOptions(options),
);
const workbenchOptions: WorkbenchCommandOptions = {
python,
@@ -1200,6 +1236,7 @@ export class CodexSecurity {
options: ScanOptions,
signal?: AbortSignal,
): Promise {
+ deepScanOptions(options);
if (
options.maxCostUsd !== undefined &&
(!Number.isFinite(options.maxCostUsd) || options.maxCostUsd <= 0)
@@ -1340,6 +1377,71 @@ export class CodexSecurity {
}
}
+function deepScanOptions(options: ScanOptions): DeepScanOptions {
+ const selected: DeepScanOptions = {};
+ for (const [name, , minimum] of DEEP_SCAN_SETTINGS) {
+ const value = options[name];
+ if (value === undefined) continue;
+ if ((options.mode ?? "standard") !== "deep") {
+ throw new CodexSecurityError("Deep scan settings require deep mode.");
+ }
+ if (!Number.isSafeInteger(value) || value < minimum) {
+ throw new CodexSecurityError(
+ `Deep scan ${name} must be ${minimum === 0 ? "a non-negative" : "a positive"} integer.`,
+ );
+ }
+ selected[name] = value;
+ }
+ return selected;
+}
+
+async function prepareDeepScanConfig(
+ codexHome: string,
+ environment: ProcessEnvironment,
+ options: DeepScanOptions,
+ signal: AbortSignal,
+): Promise {
+ const ambientHome =
+ environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex");
+ const source = join(ambientHome, "codex-security", "config.toml");
+ let configured: TomlTable = {};
+ try {
+ configured = parseToml(
+ await readFile(source, { encoding: "utf8", signal }),
+ );
+ } catch (error) {
+ if (!isRecord(error) || error["code"] !== "ENOENT") {
+ throw new CodexSecurityError(
+ `Cannot read Codex Security configuration at ${source}.`,
+ { cause: error },
+ );
+ }
+ }
+ const existing = configured["deep_scan"];
+ if (existing !== undefined && !isRecord(existing)) {
+ throw new CodexSecurityError(
+ `Codex Security configuration [deep_scan] at ${source} must be a TOML table.`,
+ );
+ }
+ const overrides: TomlTable = {};
+ for (const [name, key] of DEEP_SCAN_SETTINGS) {
+ const value = options[name];
+ if (value !== undefined) overrides[key] = value;
+ }
+ if (existing === undefined && Object.keys(overrides).length === 0) return;
+ const destination = join(codexHome, "codex-security", "config.toml");
+ if (destination === source && Object.keys(overrides).length === 0) return;
+ await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
+ await writeFile(
+ destination,
+ stringifyToml({
+ ...configured,
+ deep_scan: { ...existing, ...overrides },
+ }),
+ { mode: 0o600, signal },
+ );
+}
+
export async function initialCredentialsAvailable(
environment: ProcessEnvironment,
ambientHome: string,
@@ -1552,6 +1654,11 @@ async function scanPrompt(
return [
`Use the installed $codex-security:${skillName} skill at "$CODEX_SECURITY_PLUGIN_ROOT/skills/${skillName}/SKILL.md".`,
"Run this Codex Security scan non-interactively.",
+ ...(mode === "deep"
+ ? [
+ '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.',
+ ]
+ : []),
...(skillName === "deep-security-scan"
? []
: [
@@ -1618,6 +1725,7 @@ function scanRecipe(
failOnSeverity?: SeverityLevel,
knowledgeBasePaths?: string[],
maxCostUsd?: number,
+ deepScan?: DeepScanOptions,
): JsonObject {
return {
repository,
@@ -1636,6 +1744,9 @@ function scanRecipe(
...(failOnSeverity === undefined ? {} : { failOnSeverity }),
...(knowledgeBasePaths === undefined ? {} : { knowledgeBasePaths }),
...(maxCostUsd === undefined ? {} : { maxCostUsd }),
+ ...(deepScan === undefined || Object.keys(deepScan).length === 0
+ ? {}
+ : { deepScan: { ...deepScan } }),
};
}
diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts
index 30dc6f4..4ff4f02 100644
--- a/sdk/typescript/src/cli.ts
+++ b/sdk/typescript/src/cli.ts
@@ -32,6 +32,7 @@ import {
classifyConnectionFailure,
CodexSecurity,
scanAuthentication,
+ type DeepScanOptions,
type ScanAuthMode,
type ScanAuthentication,
type ScanOptions,
@@ -173,6 +174,9 @@ const VALUE_OPTIONS = new Set([
"--fail-on-severity",
"--max-cost",
"--workers",
+ "--subagents",
+ "--stop-after-no-new",
+ "--max-discovery-runs",
"--max-attempts",
"--export-format",
"--output",
@@ -200,7 +204,34 @@ function effortOption() {
);
}
-interface ScanArguments {
+const DEEP_SCAN_OPTION_SCHEMAS = {
+ workers: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Maximum concurrent deep-scan discovery workers."),
+ subagents: z
+ .number()
+ .int()
+ .nonnegative()
+ .optional()
+ .describe("Subagents available to each deep-scan worker."),
+ stopAfterNoNew: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Stop after this many runs find no new issues."),
+ maxDiscoveryRuns: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Maximum deep-scan discovery runs."),
+};
+
+interface ScanArguments extends DeepScanOptions {
auth?: ScanAuthMode;
repository?: string;
paths: string[];
@@ -998,6 +1029,7 @@ export async function main(
.enum(["standard", "deep"])
.default("standard")
.describe("Scan mode; deep supports repository and path targets."),
+ ...DEEP_SCAN_OPTION_SCHEMAS,
model: optionValue("--model")
.optional()
.describe(
@@ -1062,6 +1094,15 @@ export async function main(
(options) =>
!options.archiveExisting || options.outputDir !== undefined,
{ message: "--archive-existing requires --output-dir." },
+ )
+ .refine(
+ (options) =>
+ options.mode === "deep" ||
+ (options.workers === undefined &&
+ options.subagents === undefined &&
+ options.stopAfterNoNew === undefined &&
+ options.maxDiscoveryRuns === undefined),
+ { message: "Deep scan settings require --mode deep." },
),
examples: [
{ args: { repository: "." } },
@@ -1101,6 +1142,10 @@ export async function main(
head: options.head,
base: options.base,
mode: options.mode,
+ workers: options.workers,
+ subagents: options.subagents,
+ stopAfterNoNew: options.stopAfterNoNew,
+ maxDiscoveryRuns: options.maxDiscoveryRuns,
model: options.model,
effort: options.effort,
outputDir: options.outputDir,
@@ -1835,6 +1880,24 @@ function scanArgumentsFromRecipe(
"The saved scan recipe contains an invalid cost limit.",
);
}
+ const deepScan = z
+ .object(DEEP_SCAN_OPTION_SCHEMAS)
+ .optional()
+ .safeParse(recipe["deepScan"]);
+ if (!deepScan.success) {
+ throw new CodexSecurityError(
+ "The saved scan recipe contains invalid deep scan settings.",
+ );
+ }
+ if (
+ mode !== "deep" &&
+ deepScan.data !== undefined &&
+ Object.keys(deepScan.data).length > 0
+ ) {
+ throw new CodexSecurityError(
+ "The saved scan recipe contains deep scan settings for a standard scan.",
+ );
+ }
return {
repository,
paths,
@@ -1844,6 +1907,7 @@ function scanArgumentsFromRecipe(
head: kind === "refs" ? head ?? "HEAD" : undefined,
base: kind === "working_tree" ? reference : undefined,
mode,
+ ...deepScan.data,
archiveExisting: false,
codex: [],
codexOverrides: config,
@@ -2615,6 +2679,10 @@ async function runScan(
target,
knowledgeBasePaths: arguments_.knowledgeBasePaths,
mode: arguments_.mode,
+ workers: arguments_.workers,
+ subagents: arguments_.subagents,
+ stopAfterNoNew: arguments_.stopAfterNoNew,
+ maxDiscoveryRuns: arguments_.maxDiscoveryRuns,
outputDir: arguments_.outputDir,
archiveExisting: arguments_.archiveExisting,
parentScanId: arguments_.parentScanId,
diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts
index dc1c64d..2ef1c60 100644
--- a/sdk/typescript/src/index.ts
+++ b/sdk/typescript/src/index.ts
@@ -3,6 +3,7 @@ export { estimateScanCost } from "./cost.js";
export type { ScanCost } from "./cost.js";
export type {
CodexSecurityMetadata,
+ DeepScanOptions,
ScanAuthMode,
ScanAuthentication,
ScanOptions,
diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts
index 0ce7f5b..6f11f48 100644
--- a/sdk/typescript/tests-ts/api.test.ts
+++ b/sdk/typescript/tests-ts/api.test.ts
@@ -729,6 +729,55 @@ describe("CodexSecurity orchestration", () => {
await client.close();
});
+ test("validates deep scan settings before initializing the runtime", async () => {
+ const root = await temporaryDirectory();
+ const repository = join(root, "repository");
+ await mkdir(repository);
+ let runtimeStarted = false;
+ const client = new TestClient(
+ {},
+ {
+ environment: {},
+ prepareRuntime: async () => {
+ runtimeStarted = true;
+ throw new Error("runtime should not initialize");
+ },
+ },
+ );
+
+ await expect(
+ client.preflight(repository, {
+ mode: "deep",
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
+ }),
+ ).resolves.toMatchObject({
+ mode: "deep",
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
+ });
+ await expect(client.preflight(repository, { workers: 1 })).rejects.toThrow(
+ "Deep scan settings require deep mode",
+ );
+ for (const invalid of [
+ { workers: 0 },
+ { workers: 1.5 },
+ { subagents: -1 },
+ { stopAfterNoNew: 0 },
+ { maxDiscoveryRuns: Number.POSITIVE_INFINITY },
+ ]) {
+ await expect(
+ client.preflight(repository, { mode: "deep", ...invalid }),
+ ).rejects.toThrow("integer");
+ }
+ expect(runtimeStarted).toBe(false);
+ await client.close();
+ });
+
test("validates knowledge-base documents before initializing the runtime", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
@@ -1697,6 +1746,85 @@ describe("CodexSecurity orchestration", () => {
await client.close();
});
+ test("applies deep scan overrides over the user's existing settings", async () => {
+ const root = await temporaryDirectory();
+ const repository = join(root, "repository");
+ const ambientHome = join(root, "ambient-home");
+ const codexHome = join(root, "runtime-home");
+ const scanDir = join(root, "scan");
+ await mkdir(repository);
+ await mkdir(join(ambientHome, "codex-security"), { recursive: true });
+ await mkdir(codexHome);
+ await mkdir(scanDir, { mode: 0o700 });
+ await writeFile(
+ join(ambientHome, "codex-security", "config.toml"),
+ [
+ "[deep_scan]",
+ "workers = 5",
+ "subagents = 2",
+ "stop_after_no_new = 7",
+ "max_discovery_runs = 60",
+ "[other]",
+ "enabled = true",
+ "",
+ ].join("\n"),
+ );
+ let recipe: Record | undefined;
+ const client = new TestClient(
+ {},
+ {
+ environment: { CODEX_HOME: ambientHome },
+ prepareRuntime: async () => preparedRuntime(codexHome),
+ resolvePluginPython: async () => "/managed/python",
+ prepareOutputDir: async () => scanDir,
+ repositoryRevision: async () => "deadbeef",
+ runWorkbench: async (_options: unknown, args: readonly string[]) => {
+ if (args[0] !== "register-cli-scan") {
+ return {
+ scanId: "scan_example_001",
+ targetId: "target_sha256_example",
+ falsePositives: [],
+ };
+ }
+ recipe = JSON.parse(args[args.indexOf("--recipe-json") + 1]!);
+ return mockScanRegistration(args);
+ },
+ createCodex: () => ({
+ startThread: () => ({
+ id: null,
+ async runStreamed() {
+ throw new Error("deep scan settings captured");
+ },
+ }),
+ }),
+ },
+ );
+
+ await expect(
+ client.run(repository, {
+ mode: "deep",
+ workers: 2,
+ subagents: 0,
+ maxDiscoveryRuns: 10,
+ }),
+ ).rejects.toThrow("deep scan settings captured");
+ const configuration = await readFile(
+ join(codexHome, "codex-security", "config.toml"),
+ "utf8",
+ );
+ expect(configuration).toContain("workers = 2");
+ expect(configuration).toContain("subagents = 0");
+ expect(configuration).toContain("stop_after_no_new = 7");
+ expect(configuration).toContain("max_discovery_runs = 10");
+ expect(configuration).toContain("[other]");
+ expect(configuration).toContain("enabled = true");
+ expect(recipe).toMatchObject({
+ mode: "deep",
+ deepScan: { workers: 2, subagents: 0, maxDiscoveryRuns: 10 },
+ });
+ await client.close();
+ });
+
test("rejects a scan registration without an authoritative target contract", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
@@ -3207,6 +3335,9 @@ describe("CodexSecurity orchestration", () => {
"prompt captured",
);
expect(prompt).toContain("$codex-security:deep-security-scan");
+ expect(prompt).toContain(
+ 'start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }',
+ );
expect(prompt).not.toContain(
"This exhaustive scan authorizes the delegated-worker phases",
);
diff --git a/sdk/typescript/tests-ts/cli-workbench.test.ts b/sdk/typescript/tests-ts/cli-workbench.test.ts
index 90156a4..aea574b 100644
--- a/sdk/typescript/tests-ts/cli-workbench.test.ts
+++ b/sdk/typescript/tests-ts/cli-workbench.test.ts
@@ -548,6 +548,12 @@ describe("CLI workbench", () => {
pluginVersion: "1.2.3",
failOnSeverity: "high",
knowledgeBasePaths: ["/original/security.md"],
+ deepScan: {
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
+ },
config: savedConfig,
},
}),
@@ -563,6 +569,10 @@ describe("CLI workbench", () => {
expectedPluginVersion: "1.2.3",
failureSeverity: "high",
knowledgeBasePaths: ["/original/security.md"],
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
});
const references: Array<[JsonObject, ReturnType]> =
diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts
index d16b6db..d716861 100644
--- a/sdk/typescript/tests-ts/cli.test.ts
+++ b/sdk/typescript/tests-ts/cli.test.ts
@@ -108,6 +108,10 @@ describe("CLI", () => {
properties: {
path: { type: "array" },
mode: { enum: ["standard", "deep"] },
+ workers: { type: "integer" },
+ subagents: { type: "integer" },
+ stopAfterNoNew: { type: "integer" },
+ maxDiscoveryRuns: { type: "integer" },
model: { type: "string" },
effort: { enum: ["minimal", "low", "medium", "high", "xhigh"] },
failOnSeverity: { enum: ["critical", "high", "medium", "low"] },
@@ -1462,6 +1466,10 @@ describe("CLI", () => {
expect(help.text()).toContain("Usage: codex-security scan [repository]");
expect(help.text()).toContain("--path ");
expect(help.text()).toContain("--max-cost ");
+ expect(help.text()).toContain("--workers ");
+ expect(help.text()).toContain("--subagents ");
+ expect(help.text()).toContain("--stop-after-no-new ");
+ expect(help.text()).toContain("--max-discovery-runs ");
expect(help.text()).toContain("--model ");
expect(help.text()).toContain(
`OpenAI model to use (default: ${DEFAULT_SCAN_MODEL_CONFIGURATION.model}).`,
@@ -1592,6 +1600,14 @@ describe("CLI", () => {
"--knowledge-base=/shared/threat-models",
"--mode",
"deep",
+ "--workers",
+ "2",
+ "--subagents",
+ "0",
+ "--stop-after-no-new",
+ "3",
+ "--max-discovery-runs",
+ "10",
"--plugin-path",
"plugin.zip",
"--python=/managed/python",
@@ -1612,6 +1628,10 @@ describe("CLI", () => {
expect(pathOptions).toMatchObject({
target: ["src", "--fixtures"],
knowledgeBasePaths: ["/shared/architecture.pdf", "/shared/threat-models"],
+ workers: 2,
+ subagents: 0,
+ stopAfterNoNew: 3,
+ maxDiscoveryRuns: 10,
});
expect(pathConfig).toMatchObject({
pluginPath: "plugin.zip",
@@ -1725,6 +1745,26 @@ describe("CLI", () => {
[["scan", ".", "--base", "HEAD"], "--base requires --working-tree"],
[["scan", ".", "--archive-existing"], "requires --output-dir"],
[["scan", ".", "--max-cost=0"], "expected number to be >0"],
+ [
+ ["scan", ".", "--workers", "2"],
+ "Deep scan settings require --mode deep",
+ ],
+ [
+ ["scan", ".", "--mode", "deep", "--workers", "0"],
+ "expected number to be >0",
+ ],
+ [
+ ["scan", ".", "--mode", "deep", "--subagents", "-1"],
+ "expected number to be >=0",
+ ],
+ [
+ ["scan", ".", "--mode", "deep", "--stop-after-no-new", "0"],
+ "expected number to be >0",
+ ],
+ [
+ ["scan", ".", "--mode", "deep", "--max-discovery-runs", "0"],
+ "expected number to be >0",
+ ],
[["scan", ".", "--path="], "--path must not be empty"],
[["scan", ".", "--model="], "--model must not be empty"],
[