@@ -120,8 +120,13 @@ export interface ScanOptions {
120120 failureSeverity ?: SeverityLevel ;
121121 onOutputArchived ?: ( archiveDir : string ) => void ;
122122 onOutputDirReady ?: ( scanDir : string ) => void ;
123+ onAuthentication ?: ( authentication : ScanAuthentication ) => void ;
123124 onScanStarted ?: ( ) => void ;
124- onReconnect ?: ( attempt : number , maxAttempts : number ) => void ;
125+ onReconnect ?: (
126+ attempt : number ,
127+ maxAttempts : number ,
128+ details ?: ScanReconnectDetails ,
129+ ) => void ;
125130 onWorkerStatus ?: ( status : ScanWorkerStatus ) => void ;
126131 onObserverError ?: ( observer : ScanObserverName , error : unknown ) => void ;
127132 signal ?: AbortSignal ;
@@ -138,7 +143,13 @@ export type ScanAuthentication =
138143 verified : false ;
139144 } ;
140145
146+ export interface ScanReconnectDetails {
147+ reason : "rate_limit" | "network" | "authentication" | "authorization" ;
148+ retryAfterSeconds ?: number ;
149+ }
150+
141151type ScanObserverName =
152+ | "onAuthentication"
142153 | "onOutputArchived"
143154 | "onOutputDirReady"
144155 | "onScanStarted"
@@ -353,6 +364,12 @@ export class CodexSecurity {
353364 "OPENAI_API_KEY or CODEX_API_KEY for CI." ,
354365 ) ;
355366 }
367+ notifyObserver (
368+ "onAuthentication" ,
369+ options . onAuthentication ,
370+ options . onObserverError ,
371+ scanAuthentication ( this . #dependencies. environment ) ,
372+ ) ;
356373 const python = await (
357374 this . #dependencies. resolvePluginPython ?? resolvePluginPython
358375 ) ( {
@@ -940,7 +957,11 @@ interface ScanEventRunOptions {
940957 expectation : ScanExpectation ;
941958 onFinalize ?: ( ) => Promise < void > ;
942959 onScanStarted ?: ( ) => void ;
943- onReconnect ?: ( attempt : number , maxAttempts : number ) => void ;
960+ onReconnect ?: (
961+ attempt : number ,
962+ maxAttempts : number ,
963+ details ?: ScanReconnectDetails ,
964+ ) => void ;
944965 onWorkerStatus ?: ( status : ScanWorkerStatus ) => void ;
945966 onObserverError ?: ( observer : ScanObserverName , error : unknown ) => void ;
946967}
@@ -1005,6 +1026,7 @@ export async function runScanEvents(
10051026 options . onReconnect ,
10061027 options . onObserverError ,
10071028 ...reconnect ,
1029+ reconnectDetails ( message ) ,
10081030 ) ;
10091031 }
10101032 }
@@ -1264,6 +1286,71 @@ function reconnectAttempt(message: string): [number, number] | null {
12641286 return attempt <= maxAttempts ? [ attempt , maxAttempts ] : null ;
12651287}
12661288
1289+ function reconnectDetails ( message : string ) : ScanReconnectDetails | undefined {
1290+ const classification = classifyConnectionFailure ( message ) ;
1291+ if ( classification !== "rate_limited" ) {
1292+ if ( classification === "network_error" ) return { reason : "network" } ;
1293+ if ( classification === "unauthorized" ) return { reason : "authentication" } ;
1294+ if ( classification === "forbidden" ) return { reason : "authorization" } ;
1295+ return undefined ;
1296+ }
1297+ const delay =
1298+ / \b (?: t r y a g a i n | r e t r y ) \s + i n \s + ( \d { 1 , 6 } (?: \. \d { 1 , 3 } ) ? ) \s * (?: s \b | s e c o n d s ? \b ) / iu. exec (
1299+ message ,
1300+ ) ;
1301+ const retryAfterSeconds = delay === null ? NaN : Number ( delay [ 1 ] ) ;
1302+ return {
1303+ reason : "rate_limit" ,
1304+ ...( Number . isFinite ( retryAfterSeconds ) &&
1305+ retryAfterSeconds > 0 &&
1306+ retryAfterSeconds <= 3_600
1307+ ? { retryAfterSeconds }
1308+ : { } ) ,
1309+ } ;
1310+ }
1311+
1312+ export function classifyConnectionFailure (
1313+ error : unknown ,
1314+ ) :
1315+ | "rate_limited"
1316+ | "unauthorized"
1317+ | "forbidden"
1318+ | "network_error"
1319+ | "timeout"
1320+ | "unknown" {
1321+ const message = error instanceof Error ? error . message : String ( error ) ;
1322+ if (
1323+ / \b r a t e [ _ - ] ? l i m i t (?: e d | [ _ - ] e x c e e d e d ) ? \b | \b 4 2 9 \b | \b t o o m a n y r e q u e s t s \b / iu. test (
1324+ message ,
1325+ )
1326+ ) {
1327+ return "rate_limited" ;
1328+ }
1329+ if (
1330+ / \b 4 0 1 \b | \b u n a u t h o r i [ s z ] e d \b | \b i n v a l i d [ _ - ] (?: a p i [ _ - ] ? k e y | a u t h e n t i c a t i o n | t o k e n | c r e d e n t i a l s ? ) \b | \b (?: e x p i r e d | r e v o k e d ) [ _ - ] (?: a p i [ _ - ] ? k e y | t o k e n | c r e d e n t i a l s ? ) \b | \b (?: a p i [ _ - ] ? k e y | t o k e n | c r e d e n t i a l s ? ) (?: h a s ) ? (?: e x p i r e d | b e e n r e v o k e d ) \b / iu. test (
1331+ message ,
1332+ )
1333+ ) {
1334+ return "unauthorized" ;
1335+ }
1336+ if (
1337+ / \b 4 0 3 \b | \b f o r b i d d e n \b | \b p e r m i s s i o n d e n i e d \b | \b (?: m o d e l | o r g a n i z a t i o n | p r o j e c t ) a c c e s s \b | \b (?: a c c e s s d e n i e d | d o n o t h a v e a c c e s s | n o t a u t h o r i z e d | i n s u f f i c i e n t p e r m i s s i o n s ) \b | \b m o d e l [ _ - ] ? n o t [ _ - ] ? f o u n d \b / iu. test (
1338+ message ,
1339+ )
1340+ ) {
1341+ return "forbidden" ;
1342+ }
1343+ if (
1344+ / \b (?: E N O T F O U N D | E C O N N R E S E T | E C O N N R E F U S E D | E H O S T U N R E A C H | E T I M E D O U T ) \b | \b (?: n e t w o r k | c o n n e c t i o n | T L S | D N S ) \b | \b e r r o r s e n d i n g r e q u e s t \b / iu. test (
1345+ message ,
1346+ )
1347+ ) {
1348+ return "network_error" ;
1349+ }
1350+ if ( / \b (?: t i m e d ? o u t | t i m e o u t ) \b / iu. test ( message ) ) return "timeout" ;
1351+ return "unknown" ;
1352+ }
1353+
12671354export function scanRuntimeCodexConfig ( config : JsonObject ) : JsonObject {
12681355 const hardened = structuredClone ( config ) ;
12691356 delete hardened [ "sandbox_mode" ] ;
0 commit comments