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" ;
412import { randomUUID } from "node:crypto" ;
513import { homedir , tmpdir } from "node:os" ;
614import { basename , dirname , isAbsolute , join , relative , sep } from "node:path" ;
715import { Codex , type CodexOptions } from "@openai/codex-sdk" ;
16+ import {
17+ parse as parseToml ,
18+ stringify as stringifyToml ,
19+ type TomlTable ,
20+ } from "smol-toml" ;
821import {
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
221241const 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
223249export 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+
13431445export 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