@mono-agent/agent-runtime 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +36 -16
  2. package/package.json +14 -7
  3. package/src/agent/approval.js +52 -17
  4. package/src/agent/sandbox-seam.js +1 -0
  5. package/src/agent/tools/pi-bridge.js +2 -0
  6. package/src/agent/tools/shared/ripgrep.js +12 -8
  7. package/src/ai/index.js +8 -0
  8. package/src/ai/providers/claude-cli.js +109 -5
  9. package/src/ai/providers/claude-sandbox.js +71 -0
  10. package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
  11. package/src/ai/providers/claude-sdk-discovery.js +352 -0
  12. package/src/ai/providers/claude-sdk.js +313 -35
  13. package/src/ai/providers/codex-app.js +823 -78
  14. package/src/ai/providers/opencode-app.js +682 -96
  15. package/src/ai/providers/opencode-server.js +508 -0
  16. package/src/ai/runtime/capabilities.js +12 -0
  17. package/src/ai/runtime/context-windows.js +8 -0
  18. package/src/ai/runtime/registry.js +8 -2
  19. package/src/ai/runtime/router.js +627 -29
  20. package/src/ai/types.js +29 -2
  21. package/src/index.js +6 -0
  22. package/src/runtime.js +17 -1
  23. package/types/agent/approval.d.ts +4 -7
  24. package/types/agent/sandbox-seam.d.ts +5 -0
  25. package/types/ai/backend.d.ts +16 -0
  26. package/types/ai/index.d.ts +1 -0
  27. package/types/ai/providers/claude-cli.d.ts +116 -0
  28. package/types/ai/providers/claude-sandbox.d.ts +79 -0
  29. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
  30. package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
  31. package/types/ai/providers/claude-sdk.d.ts +81 -5
  32. package/types/ai/providers/codex-app.d.ts +11 -7
  33. package/types/ai/providers/opencode-app.d.ts +15 -16
  34. package/types/ai/providers/opencode-server.d.ts +20 -0
  35. package/types/ai/runtime/capabilities.d.ts +19 -0
  36. package/types/ai/runtime/context-windows.d.ts +1 -0
  37. package/types/ai/runtime/router.d.ts +24 -23
  38. package/types/ai/types.d.ts +75 -2
  39. package/types/index.d.ts +1 -0
@@ -7,6 +7,7 @@ import { estimateCost } from "../cost.js";
7
7
  import { codexModelSupportsFastMode, normalizeFastMode } from "../runtime/fast-mode.js";
8
8
  import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
9
9
  import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
10
+ import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
10
11
  import { createSessionRegistry } from "../runtime/sessions.js";
11
12
  import { createSessionLiveness } from "../runtime/session-liveness.js";
12
13
 
@@ -17,6 +18,417 @@ const MIN_THREAD_START_TIMEOUT_MS = 60_000;
17
18
  const MAX_THREAD_START_TIMEOUT_MS = 180_000;
18
19
  const THREAD_START_PROMPT_CHARS_PER_STEP = 50_000;
19
20
  const THREAD_START_TIMEOUT_STEP_MS = 30_000;
21
+ const CODEX_DIAGNOSTIC_BYTES = 8 * 1024;
22
+ const CODEX_STDERR_TAIL_BYTES = 8 * 1024;
23
+ const CODEX_SHUTDOWN_GRACE_MS = 1_000;
24
+ const CODEX_KILL_GRACE_MS = 1_000;
25
+
26
+ const SENSITIVE_ASSIGNMENT_RE = /((?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,\r\n]+)/giu;
27
+ const SENSITIVE_HEADER_RE = /((?:(?:proxy-)?authorization|cookie|set-cookie)\s*[:=]\s*)[^\r\n]*/giu;
28
+ const SENSITIVE_JSON_LINE_RE = /("(?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)"\s*:\s*)[^\r\n]*/giu;
29
+ const SENSITIVE_ESCAPED_JSON_LINE_RE = /(\\"(?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)\\"\s*:\s*)[^\r\n]*/giu;
30
+
31
+ function normalizedSensitiveName(name) {
32
+ return String(name || "")
33
+ .replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
34
+ .replace(/[^A-Za-z0-9]+/gu, "_")
35
+ .replace(/^_+|_+$/gu, "")
36
+ .toLowerCase();
37
+ }
38
+
39
+ function isSensitivePayloadField(name) {
40
+ const normalized = normalizedSensitiveName(name);
41
+ return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)$/u.test(normalized);
42
+ }
43
+
44
+ function isSensitiveEnvironmentKey(name) {
45
+ const normalized = normalizedSensitiveName(name);
46
+ return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)(?:_|$)/u.test(normalized);
47
+ }
48
+
49
+ function isSensitiveCliFlag(name) {
50
+ const normalized = normalizedSensitiveName(String(name || "").replace(/^-+/u, ""));
51
+ return isSensitivePayloadField(normalized)
52
+ || /(?:^|_)(?:auth|private_key|access_key|signature|sig)$/u.test(normalized);
53
+ }
54
+
55
+ function boundedTimeout(value, fallback) {
56
+ const parsed = Number(value);
57
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.trunc(parsed) : fallback;
58
+ }
59
+
60
+ function sensitiveEnvironmentValues(env) {
61
+ return [...new Set(Object.entries(env || {})
62
+ .filter(([key, value]) => isSensitiveEnvironmentKey(key) && typeof value === "string" && value.length >= 8)
63
+ .map(([, value]) => value))]
64
+ .sort((left, right) => right.length - left.length);
65
+ }
66
+
67
+ function addOpaqueSensitiveValue(target, value, { splitCredentials = false } = {}) {
68
+ if (typeof value !== "string" || value.length < 8) return;
69
+ target.add(value);
70
+ if (!splitCredentials) return;
71
+ const schemeMatch = value.match(/^\s*(Bearer|Basic|Token)\s+(.+?)\s*$/iu);
72
+ const payload = schemeMatch?.[2];
73
+ if (payload?.length >= 8) target.add(payload);
74
+ if (schemeMatch?.[1]?.toLowerCase() === "basic" && payload && payload.length <= 16 * 1024) {
75
+ try {
76
+ const decoded = Buffer.from(payload, "base64").toString("utf8");
77
+ if (decoded && !decoded.includes("\uFFFD")) {
78
+ addOpaqueSensitiveValue(target, decoded);
79
+ const separator = decoded.indexOf(":");
80
+ if (separator >= 0) {
81
+ addOpaqueSensitiveValue(target, decoded.slice(0, separator));
82
+ addOpaqueSensitiveValue(target, decoded.slice(separator + 1));
83
+ }
84
+ }
85
+ } catch {
86
+ // The raw Basic payload remains protected even if it is malformed.
87
+ }
88
+ }
89
+ }
90
+
91
+ function addOpaqueSensitiveValues(target, values, { splitCredentials = false } = {}) {
92
+ if (!values || typeof values !== "object") return;
93
+ for (const value of Object.values(values)) {
94
+ if (Array.isArray(value)) {
95
+ value.forEach((entry) => addOpaqueSensitiveValue(target, entry, { splitCredentials }));
96
+ } else {
97
+ addOpaqueSensitiveValue(target, value, { splitCredentials });
98
+ }
99
+ }
100
+ }
101
+
102
+ function addEncodedCredentialValue(target, value) {
103
+ addOpaqueSensitiveValue(target, value);
104
+ try {
105
+ const decoded = decodeURIComponent(value);
106
+ addOpaqueSensitiveValue(target, decoded);
107
+ addOpaqueSensitiveValue(target, encodeURIComponent(decoded));
108
+ } catch {
109
+ // Invalid percent escapes are still covered by the original raw value.
110
+ }
111
+ }
112
+
113
+ function addUrlSensitiveValues(target, rawUrl) {
114
+ if (typeof rawUrl !== "string" || !rawUrl) return;
115
+ try {
116
+ const parsed = new URL(rawUrl);
117
+ addEncodedCredentialValue(target, parsed.username);
118
+ addEncodedCredentialValue(target, parsed.password);
119
+ for (const value of parsed.searchParams.values()) {
120
+ // Query parameter names are provider-defined. All opaque query values are
121
+ // treated as credentials rather than betting on a finite key allowlist.
122
+ addEncodedCredentialValue(target, value);
123
+ }
124
+ for (const part of parsed.search.slice(1).split("&")) {
125
+ if (part.includes("=")) addEncodedCredentialValue(target, part.slice(part.indexOf("=") + 1));
126
+ }
127
+ } catch {
128
+ // Non-URL templates are passed through unchanged and may still be covered
129
+ // by a surrounding secret-bearing CLI flag or payload-field redaction.
130
+ }
131
+ }
132
+
133
+ function addHeaderArgumentSensitiveValues(target, header) {
134
+ if (typeof header !== "string") return;
135
+ const separator = header.indexOf(":");
136
+ const value = separator >= 0 ? header.slice(separator + 1).trim() : header.trim();
137
+ addOpaqueSensitiveValue(target, value, { splitCredentials: true });
138
+ }
139
+
140
+ function addCliSensitiveValues(target, args) {
141
+ if (!Array.isArray(args)) return;
142
+ for (let index = 0; index < args.length; index += 1) {
143
+ const argument = args[index];
144
+ if (typeof argument !== "string") continue;
145
+ addUrlSensitiveValues(target, argument);
146
+ const equals = argument.indexOf("=");
147
+ if (equals > 0) {
148
+ const flag = argument.slice(0, equals);
149
+ const value = argument.slice(equals + 1);
150
+ // `--url=...` and `--endpoint=...` are not themselves secret flags, but
151
+ // their inline RHS can contain URL userinfo or query credentials.
152
+ addUrlSensitiveValues(target, value);
153
+ if (/^(?:-H|--header|--http-header)$/iu.test(flag)) {
154
+ addHeaderArgumentSensitiveValues(target, value);
155
+ continue;
156
+ }
157
+ if (isSensitiveCliFlag(flag)) {
158
+ addOpaqueSensitiveValue(target, value, { splitCredentials: true });
159
+ continue;
160
+ }
161
+ }
162
+ if (isSensitiveCliFlag(argument) && typeof args[index + 1] === "string") {
163
+ addOpaqueSensitiveValue(target, args[index + 1], { splitCredentials: true });
164
+ index += 1;
165
+ continue;
166
+ }
167
+ if (/^(?:-H|--header|--http-header)$/iu.test(argument) && typeof args[index + 1] === "string") {
168
+ addHeaderArgumentSensitiveValues(target, args[index + 1]);
169
+ index += 1;
170
+ }
171
+ }
172
+ }
173
+
174
+ function codexRequestSensitiveValues(options = {}) {
175
+ const values = new Set(sensitiveEnvironmentValues({
176
+ ...process.env,
177
+ ...(options.codexAppServerEnv || {}),
178
+ }));
179
+ for (const server of Object.values(options.mcpServers || {})) {
180
+ if (!server || typeof server !== "object") continue;
181
+ // MCP env/header names are provider-defined and need not contain words such
182
+ // as "token" or "secret". Treat every opaque value on these credential-
183
+ // bearing surfaces as sensitive instead of relying on a key-name heuristic.
184
+ addOpaqueSensitiveValues(values, server.env);
185
+ addOpaqueSensitiveValues(values, server.headers, { splitCredentials: true });
186
+ addUrlSensitiveValues(values, server.url);
187
+ addCliSensitiveValues(values, server.args);
188
+ }
189
+ addCliSensitiveValues(values, options.codexAppServerArgs);
190
+ return [...values].sort((left, right) => right.length - left.length);
191
+ }
192
+
193
+ function leadingSensitiveOverlap(text, sensitiveValue) {
194
+ const maxLength = Math.min(text.length, sensitiveValue.length, CODEX_STDERR_TAIL_BYTES);
195
+ if (maxLength < 8) return 0;
196
+ const pattern = text.slice(0, maxLength);
197
+ const failure = new Array(pattern.length).fill(0);
198
+ for (let index = 1, matched = 0; index < pattern.length; index += 1) {
199
+ while (matched > 0 && pattern[index] !== pattern[matched]) matched = failure[matched - 1];
200
+ if (pattern[index] === pattern[matched]) matched += 1;
201
+ failure[index] = matched;
202
+ }
203
+ let matched = 0;
204
+ for (const character of sensitiveValue.slice(-maxLength)) {
205
+ while (matched > 0 && character !== pattern[matched]) matched = failure[matched - 1];
206
+ if (character === pattern[matched]) matched += 1;
207
+ }
208
+ return matched >= 8 ? matched : 0;
209
+ }
210
+
211
+ function redactCodexDiagnostic(text, sensitiveValues, truncatedStart = false) {
212
+ let redacted = String(text || "");
213
+ for (const value of sensitiveValues) {
214
+ if (truncatedStart) {
215
+ const overlap = leadingSensitiveOverlap(redacted, value);
216
+ if (overlap > 0) redacted = `[REDACTED]${redacted.slice(overlap)}`;
217
+ }
218
+ redacted = redacted.split(value).join("[REDACTED]");
219
+ }
220
+ return redacted
221
+ .replace(SENSITIVE_HEADER_RE, "$1[REDACTED]")
222
+ .replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{12,}/giu, "Bearer [REDACTED]")
223
+ .replace(/\b(?:sk|pk|sess|oauth)[-_][A-Za-z0-9._-]{12,}\b/giu, "[REDACTED]")
224
+ .replace(SENSITIVE_ESCAPED_JSON_LINE_RE, '$1\\"[REDACTED]\\"')
225
+ .replace(SENSITIVE_JSON_LINE_RE, '$1"[REDACTED]"')
226
+ .replace(SENSITIVE_ASSIGNMENT_RE, "$1[REDACTED]");
227
+ }
228
+
229
+ function utf8Head(text, limit) {
230
+ if (limit <= 0) return "";
231
+ const bytes = Buffer.from(String(text || ""));
232
+ if (bytes.length <= limit) return bytes.toString("utf8");
233
+ let end = limit;
234
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
235
+ return bytes.subarray(0, end).toString("utf8");
236
+ }
237
+
238
+ function boundCodexDiagnostic(text, limit = CODEX_DIAGNOSTIC_BYTES) {
239
+ const value = String(text || "");
240
+ const byteLength = Buffer.byteLength(value);
241
+ if (byteLength <= limit) return value;
242
+ let droppedBytes = byteLength - limit;
243
+ let marker = `\n[truncated ${droppedBytes} later bytes]`;
244
+ let bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
245
+ droppedBytes = byteLength - bodyLimit;
246
+ marker = `\n[truncated ${droppedBytes} later bytes]`;
247
+ bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
248
+ return utf8Head(value, bodyLimit) + marker;
249
+ }
250
+
251
+ function safeDiagnosticString(value) {
252
+ if (typeof value === "string") return value;
253
+ if (value instanceof Error && typeof value.message === "string") return value.message;
254
+ try {
255
+ const serialized = JSON.stringify(value);
256
+ return typeof serialized === "string" ? serialized : String(value ?? "");
257
+ } catch {
258
+ try {
259
+ return String(value);
260
+ } catch {
261
+ return "Codex app-server diagnostic unavailable";
262
+ }
263
+ }
264
+ }
265
+
266
+ function sanitizeCodexDiagnostic(value, sensitiveValues, limit = CODEX_DIAGNOSTIC_BYTES) {
267
+ return boundCodexDiagnostic(
268
+ redactCodexDiagnostic(safeDiagnosticString(value), sensitiveValues),
269
+ limit,
270
+ );
271
+ }
272
+
273
+ function sanitizeCodexProtocolCode(value, sensitiveValues) {
274
+ return typeof value === "number"
275
+ ? value
276
+ : sanitizeCodexDiagnostic(value, sensitiveValues, 256);
277
+ }
278
+
279
+ function boundedCodexDiagnosticPayload(value, sensitiveValues, limit) {
280
+ const sanitized = redactCodexPayload(value, sensitiveValues);
281
+ try {
282
+ if (Buffer.byteLength(JSON.stringify(sanitized) || "") <= limit) return sanitized;
283
+ } catch {
284
+ // Fall through to a safe string summary for non-serializable values.
285
+ }
286
+ return sanitizeCodexDiagnostic(value, sensitiveValues, limit);
287
+ }
288
+
289
+ function redactCodexPayload(value, sensitiveValues, seen = new WeakSet(), depth = 0) {
290
+ if (typeof value === "string") return redactCodexDiagnostic(value, sensitiveValues);
291
+ if (value === null || typeof value !== "object") return value;
292
+ if (value instanceof Error) {
293
+ const errorCode = /** @type {any} */ (value).code;
294
+ return {
295
+ name: redactCodexDiagnostic(value.name || "Error", sensitiveValues),
296
+ message: sanitizeCodexDiagnostic(value.message || value, sensitiveValues),
297
+ ...(errorCode !== undefined
298
+ ? { code: sanitizeCodexProtocolCode(errorCode, sensitiveValues) }
299
+ : {}),
300
+ };
301
+ }
302
+ if (depth >= 20) return "[truncated nested Codex payload]";
303
+ if (seen.has(value)) return "[circular Codex payload]";
304
+ seen.add(value);
305
+ if (Array.isArray(value)) {
306
+ const result = value.map((entry) => redactCodexPayload(entry, sensitiveValues, seen, depth + 1));
307
+ seen.delete(value);
308
+ return result;
309
+ }
310
+ const result = {};
311
+ for (const [key, entry] of Object.entries(value)) {
312
+ result[key] = isSensitivePayloadField(key)
313
+ ? "[REDACTED]"
314
+ : redactCodexPayload(entry, sensitiveValues, seen, depth + 1);
315
+ }
316
+ seen.delete(value);
317
+ return result;
318
+ }
319
+
320
+ function sanitizeCodexResponseError(error, sensitiveValues) {
321
+ const sanitized = redactCodexPayload(error, sensitiveValues);
322
+ let serialized;
323
+ try {
324
+ serialized = JSON.stringify(sanitized);
325
+ } catch {
326
+ serialized = "";
327
+ }
328
+ if (Buffer.byteLength(serialized || "") <= CODEX_DIAGNOSTIC_BYTES) return sanitized;
329
+
330
+ const data = error && typeof error === "object" ? error.data : null;
331
+ const nestedError = data && typeof data === "object" ? data.error : null;
332
+ const info = data?.info ?? nestedError?.info ?? error?.info;
333
+ return {
334
+ ...(error?.code !== undefined
335
+ ? { code: sanitizeCodexProtocolCode(error.code, sensitiveValues) }
336
+ : {}),
337
+ message: sanitizeCodexDiagnostic(codexErrorMessage(error), sensitiveValues, 6 * 1024),
338
+ ...(info !== undefined
339
+ ? { data: { info: boundedCodexDiagnosticPayload(info, sensitiveValues, 1_024) } }
340
+ : {}),
341
+ diagnostic_truncated: true,
342
+ };
343
+ }
344
+
345
+ const CODEX_DIAGNOSTIC_NOTIFICATION_METHODS = new Set([
346
+ "warning",
347
+ "error",
348
+ "configWarning",
349
+ "guardianWarning",
350
+ ]);
351
+
352
+ function sanitizeCodexNotification(notification, sensitiveValues) {
353
+ const safe = redactCodexPayload(notification, sensitiveValues);
354
+ if (!safe || typeof safe !== "object") return safe;
355
+ if (CODEX_DIAGNOSTIC_NOTIFICATION_METHODS.has(safe.method)) {
356
+ const params = safe.params && typeof safe.params === "object" ? safe.params : {};
357
+ return {
358
+ ...safe,
359
+ params: {
360
+ ...(params.code !== undefined
361
+ ? { code: sanitizeCodexProtocolCode(params.code, sensitiveValues) }
362
+ : {}),
363
+ message: sanitizeCodexDiagnostic(params.message || params.error || params, sensitiveValues),
364
+ },
365
+ };
366
+ }
367
+ if (safe.method === "turn/completed" && safe.params?.turn?.error !== undefined) {
368
+ return {
369
+ ...safe,
370
+ params: {
371
+ ...safe.params,
372
+ turn: {
373
+ ...safe.params.turn,
374
+ error: sanitizeCodexResponseError(safe.params.turn.error, sensitiveValues),
375
+ },
376
+ },
377
+ };
378
+ }
379
+ if ((safe.method === "item/started" || safe.method === "item/completed") && safe.params?.item?.error !== undefined) {
380
+ return {
381
+ ...safe,
382
+ params: {
383
+ ...safe.params,
384
+ item: {
385
+ ...safe.params.item,
386
+ error: sanitizeCodexResponseError(safe.params.item.error, sensitiveValues),
387
+ },
388
+ },
389
+ };
390
+ }
391
+ return safe;
392
+ }
393
+
394
+ function utf8Tail(text, limit) {
395
+ if (limit <= 0) return "";
396
+ const bytes = Buffer.from(String(text || ""));
397
+ if (bytes.length <= limit) return bytes.toString("utf8");
398
+ let start = bytes.length - limit;
399
+ while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start += 1;
400
+ return bytes.subarray(start).toString("utf8");
401
+ }
402
+
403
+ function createCodexStderrTail(sensitiveValues, limit = CODEX_STDERR_TAIL_BYTES) {
404
+ let buffer = Buffer.alloc(0);
405
+ let bytesDropped = 0;
406
+ return {
407
+ push(chunk) {
408
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
409
+ if (incoming.length === 0) return;
410
+ if (incoming.length >= limit) {
411
+ bytesDropped += buffer.length + incoming.length - limit;
412
+ buffer = Buffer.from(incoming.subarray(incoming.length - limit));
413
+ return;
414
+ }
415
+ const overflow = Math.max(0, buffer.length + incoming.length - limit);
416
+ bytesDropped += overflow;
417
+ buffer = Buffer.concat([buffer.subarray(overflow), incoming], Math.min(limit, buffer.length + incoming.length));
418
+ },
419
+ toString() {
420
+ const redacted = redactCodexDiagnostic(
421
+ buffer.toString("utf8").replace(/^\uFFFD/u, ""),
422
+ sensitiveValues,
423
+ bytesDropped > 0,
424
+ ).trim();
425
+ if (bytesDropped === 0) return utf8Tail(redacted, limit);
426
+ const marker = `[truncated ${bytesDropped} earlier bytes]\n`;
427
+ const bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
428
+ return marker + utf8Tail(redacted, bodyLimit);
429
+ },
430
+ };
431
+ }
20
432
 
21
433
  const CODEX_APP_CAPABILITIES = {
22
434
  kind: "codex-app",
@@ -108,17 +520,78 @@ function delay(ms, signal) {
108
520
  });
109
521
  }
110
522
 
111
- function sandboxForPermissionMode(permissionMode) {
112
- if (permissionMode === "bypassPermissions") return "danger-full-access";
113
- if (permissionMode === "plan") return "read-only";
523
+ function sandboxForRun(options) {
524
+ if (options.codexNoToolsProbe === true) return "read-only";
525
+ if (options.permissionMode === "bypassPermissions") return "danger-full-access";
526
+ if (options.permissionMode === "plan") return "read-only";
114
527
  return "workspace-write";
115
528
  }
116
529
 
117
- function approvalPolicyForPermissionMode(permissionMode) {
118
- if (permissionMode === "bypassPermissions") return "never";
119
- return "on-request";
530
+ function approvalPolicyForRun(options) {
531
+ // mono-agent channel turns are unattended: there is no interactive app-server
532
+ // approval UI on the other end of stdio. `never` lets Codex execute within the
533
+ // selected sandbox and deny escalations itself instead of waiting forever for
534
+ // a client response that cannot arrive.
535
+ return "never";
120
536
  }
121
537
 
538
+ function sandboxPolicyForRun(options) {
539
+ if (options.codexNoToolsProbe === true) return { type: "readOnly", networkAccess: false };
540
+ if (options.permissionMode === "bypassPermissions") return { type: "dangerFullAccess" };
541
+ if (options.permissionMode === "plan") return { type: "readOnly", networkAccess: false };
542
+ return {
543
+ type: "workspaceWrite",
544
+ writableRoots: [options.cwd || process.cwd()],
545
+ networkAccess: false,
546
+ excludeTmpdirEnvVar: false,
547
+ excludeSlashTmp: false,
548
+ };
549
+ }
550
+
551
+ function codexToolPolicyProblem(options) {
552
+ const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
553
+ const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
554
+ if (options.codexNoToolsProbe === true) {
555
+ const mcpServerCount = Object.keys(options.mcpServers || {}).length;
556
+ if (allowedTools?.length === 0 && disallowedTools.length === 0 && mcpServerCount === 0 && options.sessionKeepAlive !== true) {
557
+ return null;
558
+ }
559
+ return "Codex no-tool probe mode requires an empty tool policy, no MCP servers, and a disposable session.";
560
+ }
561
+ // `undefined` retains the public runtime's documented allow-all default.
562
+ // Once a caller specifies a policy, require the exact wildcard contract.
563
+ // Extra entries can conceal a caller's mistaken belief that Codex enforces a
564
+ // mixed allowlist, which the app-server cannot project.
565
+ const effectiveAllowAll = allowedTools === null || (allowedTools.length === 1 && allowedTools[0] === "*");
566
+ return effectiveAllowAll && disallowedTools.length === 0
567
+ ? null
568
+ : "Direct Codex cannot enforce allowedTools/disallowedTools. Use exact allow-all ([\"*\"] with no disallowedTools) or another runtime.";
569
+ }
570
+
571
+ const CODEX_NO_TOOL_ACTION_ITEMS = new Set([
572
+ "commandExecution",
573
+ "fileChange",
574
+ "mcpToolCall",
575
+ "dynamicToolCall",
576
+ "collabAgentToolCall",
577
+ "subAgentActivity",
578
+ "webSearch",
579
+ "imageView",
580
+ "sleep",
581
+ "imageGeneration",
582
+ ]);
583
+
584
+ const CODEX_NO_TOOL_REQUEST_METHODS = new Set([
585
+ "item/commandExecution/requestApproval",
586
+ "item/fileChange/requestApproval",
587
+ "item/tool/requestUserInput",
588
+ "item/permissions/requestApproval",
589
+ "item/tool/call",
590
+ "mcpServer/elicitation/request",
591
+ "applyPatchApproval",
592
+ "execCommandApproval",
593
+ ]);
594
+
122
595
  function codexMcpConfig(mcpServers = {}) {
123
596
  const servers = {};
124
597
  for (const [name, cfg] of Object.entries(mcpServers || {})) {
@@ -152,7 +625,7 @@ function codexErrorMessage(error) {
152
625
  if (info && typeof info === "object" && "activeTurnNotSteerable" in info) {
153
626
  return "Codex active turn is not steerable";
154
627
  }
155
- return error.message || data.message || JSON.stringify(error);
628
+ return error.message || data.message || safeDiagnosticString(error);
156
629
  }
157
630
 
158
631
  function isActiveTurnNotSteerable(error) {
@@ -169,17 +642,21 @@ function isCodexRequestTimeout(error, method = null) {
169
642
  && (!method || error.method === method);
170
643
  }
171
644
 
172
- function codexErrorDiagnostics(error) {
645
+ function codexErrorDiagnostics(error, sensitiveValues = []) {
173
646
  if (!error) return {};
174
647
  if (isCodexRequestTimeout(error)) {
175
648
  return {
176
649
  codex_error_code: "codex_app_server_request_timeout",
177
- codex_request_method: error.method || null,
650
+ codex_request_method: error.method ? sanitizeCodexDiagnostic(error.method, sensitiveValues, 256) : null,
178
651
  codex_request_timeout_ms: error.timeoutMs || null,
179
- ...(error.stderrTail ? { stderr_tail: error.stderrTail } : {}),
652
+ ...(error.stderrTail
653
+ ? { stderr_tail: sanitizeCodexDiagnostic(error.stderrTail, sensitiveValues) }
654
+ : {}),
180
655
  };
181
656
  }
182
- return error.code ? { codex_error_code: String(error.code) } : {};
657
+ return error.code
658
+ ? { codex_error_code: sanitizeCodexDiagnostic(error.code, sensitiveValues, 256) }
659
+ : {};
183
660
  }
184
661
 
185
662
  function withoutCodexRequestErrorDiagnostics(diagnostics) {
@@ -224,7 +701,7 @@ function codexCollaborationModePayload(nativeSubagents, { model, effort, systemP
224
701
  }
225
702
 
226
703
  /**
227
- * @param {{command?: string, args?: string[], cwd?: any, env?: any, onNotification?: (msg: any) => void}} [options]
704
+ * @param {{command?: string, args?: string[], cwd?: any, env?: any, redactionValues?: string[], onNotification?: (msg: any) => void, onServerRequest?: (msg: any) => Promise<any> | any, shutdownGraceMs?: number, killGraceMs?: number}} [options]
228
705
  */
229
706
  export function createCodexAppServerClient({
230
707
  command = "codex",
@@ -233,17 +710,37 @@ export function createCodexAppServerClient({
233
710
  args = ["app-server", "--listen", "stdio://", "-c", "project_doc_max_bytes=0"],
234
711
  cwd,
235
712
  env = {},
713
+ redactionValues = [],
236
714
  onNotification = () => {},
715
+ onServerRequest = (message) => {
716
+ throw new Error(`Unsupported Codex app-server request: ${String(message?.method || "unknown")}`);
717
+ },
718
+ shutdownGraceMs = CODEX_SHUTDOWN_GRACE_MS,
719
+ killGraceMs = CODEX_KILL_GRACE_MS,
237
720
  } = {}) {
721
+ const childEnv = { ...process.env, ...env };
722
+ const configuredSensitiveValues = new Set();
723
+ for (const value of redactionValues) {
724
+ addOpaqueSensitiveValue(configuredSensitiveValues, value, { splitCredentials: true });
725
+ }
726
+ const sensitiveValues = [...new Set([
727
+ ...sensitiveEnvironmentValues(childEnv),
728
+ ...configuredSensitiveValues,
729
+ ])].sort((left, right) => right.length - left.length);
238
730
  const child = spawn(command, args, {
239
731
  cwd,
240
- env: { ...process.env, ...env },
732
+ env: childEnv,
241
733
  stdio: ["pipe", "pipe", "pipe"],
242
734
  });
243
735
  const pending = new Map();
244
- const stderr = [];
736
+ const stderrTail = createCodexStderrTail(sensitiveValues);
737
+ const shutdownTimers = new Set();
245
738
  let nextId = 1;
246
739
  let closed = false;
740
+ let processSettled = false;
741
+ let closing = false;
742
+ /** @type {Promise<void> | null} */
743
+ let closePromise = null;
247
744
  let resolveClosed;
248
745
  const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
249
746
 
@@ -255,22 +752,56 @@ export function createCodexAppServerClient({
255
752
  pending.clear();
256
753
  }
257
754
 
258
- function stderrTail() {
259
- const text = stderr.join("").trim();
260
- if (!text) return "";
261
- return text.length > 8_192 ? text.slice(text.length - 8_192) : text;
755
+ function safeTransportError(error) {
756
+ const message = sanitizeCodexDiagnostic(error?.message || error || "codex app-server failed", sensitiveValues);
757
+ const safe = new Error(message || "codex app-server failed");
758
+ return error?.code === undefined ? safe : Object.assign(safe, { code: error.code });
262
759
  }
263
760
 
264
- child.stderr.on("data", (chunk) => stderr.push(chunk.toString()));
761
+ function onStderrData(chunk) {
762
+ stderrTail.push(chunk);
763
+ }
764
+
765
+ child.stderr.on("data", onStderrData);
766
+
767
+ function writeProtocolMessage(payload) {
768
+ if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) return;
769
+ child.stdin.write(`${JSON.stringify(payload)}\n`, () => {});
770
+ }
771
+
772
+ function respondToServerRequest(message) {
773
+ // Preserve request visibility for the normal event/fail-fast path, then
774
+ // always settle the JSON-RPC request. Never leave the app-server blocked on
775
+ // an inbound request that this unattended client cannot service.
776
+ const safeMessage = redactCodexPayload(message, sensitiveValues);
777
+ onNotification(safeMessage);
778
+ Promise.resolve()
779
+ .then(() => onServerRequest(safeMessage))
780
+ .then(
781
+ (result) => writeProtocolMessage({ id: message.id, result: result ?? {} }),
782
+ () => writeProtocolMessage({
783
+ id: message.id,
784
+ error: { code: -32601, message: `Unsupported Codex app-server request: ${String(message.method || "unknown")}` },
785
+ }),
786
+ );
787
+ }
265
788
 
266
789
  const rl = createInterface({ input: child.stdout });
267
- rl.on("line", (line) => {
790
+ function onLine(line) {
268
791
  if (!line.trim()) return;
269
792
  let message;
270
793
  try {
271
794
  message = JSON.parse(line);
272
795
  } catch {
273
- onNotification({ method: "warning", params: { message: `Malformed Codex app-server output: ${line}` } });
796
+ onNotification({
797
+ method: "warning",
798
+ params: {
799
+ message: sanitizeCodexDiagnostic(
800
+ `Malformed Codex app-server output: ${line}`,
801
+ sensitiveValues,
802
+ ),
803
+ },
804
+ });
274
805
  return;
275
806
  }
276
807
  if (Object.prototype.hasOwnProperty.call(message, "id") && (message.result !== undefined || message.error !== undefined)) {
@@ -278,25 +809,74 @@ export function createCodexAppServerClient({
278
809
  if (!entry) return;
279
810
  pending.delete(message.id);
280
811
  clearTimeout(entry.timer);
281
- if (message.error) entry.reject(Object.assign(new Error(codexErrorMessage(message.error)), { responseError: message.error }));
812
+ if (message.error) {
813
+ const responseError = sanitizeCodexResponseError(message.error, sensitiveValues);
814
+ entry.reject(Object.assign(
815
+ new Error(sanitizeCodexDiagnostic(codexErrorMessage(responseError), sensitiveValues)),
816
+ { responseError },
817
+ ));
818
+ }
282
819
  else entry.resolve(message.result);
283
820
  return;
284
821
  }
285
- if (message.method) onNotification(message);
286
- });
822
+ if (Object.prototype.hasOwnProperty.call(message, "id") && message.method) {
823
+ respondToServerRequest(message);
824
+ return;
825
+ }
826
+ if (message.method) onNotification(sanitizeCodexNotification(message, sensitiveValues));
827
+ }
828
+ rl.on("line", onLine);
829
+
830
+ function cleanupTransport() {
831
+ for (const timer of shutdownTimers) clearTimeout(timer);
832
+ shutdownTimers.clear();
833
+ rl.off("line", onLine);
834
+ try { rl.close(); } catch {}
835
+ child.stderr?.off?.("data", onStderrData);
836
+ child.off("error", onChildError);
837
+ child.off("close", onChildClose);
838
+ try { child.stdin?.destroy?.(); } catch {}
839
+ try { child.stdout?.destroy?.(); } catch {}
840
+ try { child.stderr?.destroy?.(); } catch {}
841
+ }
287
842
 
288
- child.on("error", (err) => {
843
+ function settleClosed(error) {
844
+ if (processSettled) return;
845
+ processSettled = true;
289
846
  closed = true;
290
- rejectAll(err);
291
- resolveClosed(err);
292
- });
293
- child.on("close", (code) => {
847
+ rejectAll(error);
848
+ cleanupTransport();
849
+ resolveClosed(error);
850
+ }
851
+
852
+ function onChildError(error) {
853
+ const safe = safeTransportError(error);
294
854
  closed = true;
295
- const detail = stderr.join("").trim();
296
- const err = new Error(detail || `codex app-server exited ${code}`);
297
- rejectAll(err);
298
- resolveClosed(err);
299
- });
855
+ rejectAll(safe);
856
+ // A spawn failure has no live process and may not emit `close`. By contrast,
857
+ // ChildProcess also emits `error` when signaling a live child fails (EPERM,
858
+ // ESRCH races). Only `close` proves that such a process actually exited.
859
+ if (child.pid === undefined) {
860
+ settleClosed(safe);
861
+ return;
862
+ }
863
+ if (!closing) void close();
864
+ }
865
+
866
+ function onChildClose(code, signal) {
867
+ if (closing) {
868
+ settleClosed(new Error("codex app-server closed"));
869
+ return;
870
+ }
871
+ const summary = signal === null
872
+ ? `codex app-server exited ${code ?? "unknown"}`
873
+ : `codex app-server terminated by ${signal}`;
874
+ const detail = stderrTail.toString();
875
+ settleClosed(new Error(detail ? `${summary}: ${detail}` : summary));
876
+ }
877
+
878
+ child.on("error", onChildError);
879
+ child.once("close", onChildClose);
300
880
 
301
881
  function request(method, params, { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
302
882
  if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) {
@@ -311,7 +891,7 @@ export function createCodexAppServerClient({
311
891
  code: "CODEX_APP_SERVER_REQUEST_TIMEOUT",
312
892
  method,
313
893
  timeoutMs,
314
- stderrTail: stderrTail(),
894
+ stderrTail: stderrTail.toString(),
315
895
  }));
316
896
  }, timeoutMs);
317
897
  timer.unref?.();
@@ -320,17 +900,51 @@ export function createCodexAppServerClient({
320
900
  if (!err) return;
321
901
  pending.delete(id);
322
902
  clearTimeout(timer);
323
- reject(err);
903
+ reject(safeTransportError(err));
324
904
  });
325
905
  });
326
906
  }
327
907
 
908
+ function waitForProcessClose(timeoutMs) {
909
+ if (processSettled) return Promise.resolve(true);
910
+ return new Promise((resolve) => {
911
+ let settled = false;
912
+ let timer;
913
+ const finish = (didClose) => {
914
+ if (settled) return;
915
+ settled = true;
916
+ if (timer !== undefined) {
917
+ clearTimeout(timer);
918
+ shutdownTimers.delete(timer);
919
+ }
920
+ resolve(didClose);
921
+ };
922
+ timer = setTimeout(() => finish(false), timeoutMs);
923
+ timer.unref?.();
924
+ shutdownTimers.add(timer);
925
+ closedPromise.then(() => finish(true));
926
+ });
927
+ }
928
+
328
929
  function close() {
329
- if (closed) return;
330
- closed = true;
331
- try { child.stdin?.end?.(); } catch {}
332
- try { child.kill("SIGTERM"); } catch {}
333
- rejectAll(new Error("codex app-server closed"));
930
+ if (closePromise !== null) return closePromise;
931
+ closePromise = (async () => {
932
+ closing = true;
933
+ closed = true;
934
+ rejectAll(new Error("codex app-server closed"));
935
+ if (processSettled) return;
936
+
937
+ try { child.stdin?.end?.(); } catch {}
938
+ try { child.kill("SIGTERM"); } catch {}
939
+ if (await waitForProcessClose(boundedTimeout(shutdownGraceMs, CODEX_SHUTDOWN_GRACE_MS))) return;
940
+
941
+ try { child.kill("SIGKILL"); } catch {}
942
+ if (await waitForProcessClose(boundedTimeout(killGraceMs, CODEX_KILL_GRACE_MS))) return;
943
+
944
+ try { child.unref?.(); } catch {}
945
+ settleClosed(new Error("codex app-server did not exit after SIGKILL"));
946
+ })();
947
+ return closePromise;
334
948
  }
335
949
 
336
950
  return { request, close, child, closed: closedPromise };
@@ -436,11 +1050,21 @@ function usageFromTokenUsage(tokenUsage) {
436
1050
 
437
1051
  const noopNotificationHandler = () => {};
438
1052
 
1053
+ async function closeCodexClient(client) {
1054
+ if (!client?.close) return;
1055
+ try {
1056
+ await client.close();
1057
+ } catch {
1058
+ // Teardown is best-effort at result boundaries, but the returned promise is
1059
+ // always observed so a custom client cannot create an unhandled rejection.
1060
+ }
1061
+ }
1062
+
439
1063
  // Live keep-alive sessions keyed by codex thread id.
440
1064
  const codexSessions = createSessionRegistry({
441
1065
  isBusy: (entry) => entry.busy === true,
442
1066
  onEvict: async (entry) => {
443
- try { entry.client.close(); } catch {}
1067
+ await closeCodexClient(entry.client);
444
1068
  },
445
1069
  });
446
1070
  // Synchronous liveness primitives over the registry. Codex only needs the
@@ -452,9 +1076,17 @@ const codexLiveness = createSessionLiveness(codexSessions);
452
1076
  export async function generateCodexAppResponse(systemPrompt, options = {}) {
453
1077
  const start = Date.now();
454
1078
  const resolved = options.model;
1079
+ // Resolve every credential-bearing value before the app-server client is
1080
+ // constructed. The same set protects transport errors and provider events,
1081
+ // including MCP servers whose custom env/header names are not recognizable
1082
+ // through key-name heuristics.
1083
+ const sensitiveValues = codexRequestSensitiveValues(options);
1084
+ const safeDiagnostic = (value, limit) => sanitizeCodexDiagnostic(value, sensitiveValues, limit);
1085
+ const safeResponseError = (error) => sanitizeCodexResponseError(error, sensitiveValues);
455
1086
  // Test seam: lets tests drive the bridge with a stub app-server client.
456
1087
  const makeClient = options.codexClientFactory || createCodexAppServerClient;
457
1088
  const keepAlive = options.sessionKeepAlive === true;
1089
+ const noToolsProbe = options.codexNoToolsProbe === true;
458
1090
  // The bridge TTL is a backstop behind the host's session policy; the grace
459
1091
  // keeps the host's lazy expiry firing first so eviction stays host-driven.
460
1092
  const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
@@ -464,11 +1096,12 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
464
1096
  ? options.sessionId
465
1097
  : null;
466
1098
  const prompt = promptFromMessages(options.messages);
467
- // Effort is expected to be pre-normalized by core/ai.js#generateResponse
468
- // before reaching this provider. We trust options.effort verbatim.
469
- const normalizedEffort = typeof options.effort === "string" && options.effort.trim()
1099
+ // Effort arrives pre-normalized; codex has no "max" reasoning tier, so clamp
1100
+ // to its ceiling here instead of failing the app-server turn.
1101
+ const requestedEffort = typeof options.effort === "string" && options.effort.trim()
470
1102
  ? options.effort
471
1103
  : null;
1104
+ const normalizedEffort = requestedEffort === "max" ? "xhigh" : requestedEffort;
472
1105
  const events = [];
473
1106
  const texts = [];
474
1107
  const agentTextByItem = new Map();
@@ -479,6 +1112,8 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
479
1112
  let failureKind = null;
480
1113
  let usage = {};
481
1114
  let codexDiagnostics = {};
1115
+ let noToolsViolation = null;
1116
+ let serverRequestViolation = null;
482
1117
  let resolveTurn;
483
1118
  let resolveTurnReady;
484
1119
  let turnReadyResolved = false;
@@ -508,17 +1143,80 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
508
1143
 
509
1144
  function emitEvent(event) {
510
1145
  if (!event) return;
511
- events.push(event);
512
- options.onEvent?.(event);
1146
+ const safeEvent = redactCodexPayload(event, sensitiveValues);
1147
+ events.push(safeEvent);
1148
+ options.onEvent?.(safeEvent);
513
1149
  }
514
1150
 
515
1151
  function handleAgentText(text) {
516
- pushUniqueText(texts, text);
517
- emitEvent({ type: "assistant", message: { content: [{ type: "text", text }] } });
1152
+ const safeText = redactCodexDiagnostic(text, sensitiveValues);
1153
+ pushUniqueText(texts, safeText);
1154
+ emitEvent({ type: "assistant", message: { content: [{ type: "text", text: safeText }] } });
1155
+ }
1156
+
1157
+ function failNoToolsProbe(action) {
1158
+ if (!noToolsProbe || noToolsViolation) return;
1159
+ const safeAction = safeDiagnostic(action, 512);
1160
+ noToolsViolation = safeAction;
1161
+ errorMessage = `Codex attempted ${safeAction} during a no-tool readiness probe`;
1162
+ failureKind = "tool_policy_violation";
1163
+ codexDiagnostics = { ...codexDiagnostics, codex_error_code: "codex_no_tools_violation", codex_tool_action: safeAction };
1164
+ emitEvent({
1165
+ type: "runtime_warning",
1166
+ warning_kind: "codex_no_tools_violation",
1167
+ message: "Codex attempted a tool action during the no-tool readiness probe; the turn was interrupted.",
1168
+ });
1169
+ if (threadId && activeTurnId && !interruptSent) {
1170
+ interruptSent = true;
1171
+ client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
1172
+ }
1173
+ turnCompleted = true;
1174
+ resolveTurn({ id: activeTurnId, status: "interrupted" });
1175
+ }
1176
+
1177
+ function failUnsupportedServerRequest(method) {
1178
+ if (noToolsProbe) {
1179
+ failNoToolsProbe(method);
1180
+ return;
1181
+ }
1182
+ if (serverRequestViolation) return;
1183
+ const safeMethod = safeDiagnostic(method, 512);
1184
+ serverRequestViolation = safeMethod;
1185
+ errorMessage = `Codex requested unsupported client interaction (${safeMethod}); the unattended turn was stopped.`;
1186
+ failureKind = "skipped_capability_mismatch";
1187
+ codexDiagnostics = {
1188
+ ...codexDiagnostics,
1189
+ codex_error_code: "codex_server_request_unsupported",
1190
+ codex_server_request_method: safeMethod,
1191
+ };
1192
+ emitEvent({
1193
+ type: "runtime_warning",
1194
+ warning_kind: "codex_server_request_unsupported",
1195
+ message: errorMessage,
1196
+ });
1197
+ turnCompleted = true;
1198
+ resolveTurn({ id: activeTurnId, status: "interrupted" });
1199
+ }
1200
+
1201
+ function assertNoUnsupportedServerRequest() {
1202
+ if (serverRequestViolation) {
1203
+ throw new Error(errorMessage || `Unsupported Codex app-server request: ${serverRequestViolation}`);
1204
+ }
518
1205
  }
519
1206
 
520
1207
  function handleNotification(notification) {
521
- const { method, params = {} } = notification;
1208
+ const safeNotification = sanitizeCodexNotification(notification, sensitiveValues);
1209
+ const { method, params = {} } = safeNotification;
1210
+ if (noToolsProbe) {
1211
+ const itemType = params.item?.type;
1212
+ if (
1213
+ CODEX_NO_TOOL_REQUEST_METHODS.has(method)
1214
+ || ((method === "item/started" || method === "item/completed") && CODEX_NO_TOOL_ACTION_ITEMS.has(itemType))
1215
+ ) {
1216
+ failNoToolsProbe(typeof itemType === "string" ? itemType : method);
1217
+ return;
1218
+ }
1219
+ }
522
1220
  if (method === "turn/started") {
523
1221
  setActiveTurnId(params.turn?.id, { steerReady: true });
524
1222
  emitEvent({ type: "cli_event", raw: { type: "turn_started", turn: params.turn } });
@@ -528,10 +1226,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
528
1226
  setActiveTurnId(params.turn?.id);
529
1227
  turnCompleted = true;
530
1228
  if (params.turn?.status === "failed") {
531
- errorMessage = params.turn?.error?.message || params.turn?.error || "Codex turn failed";
1229
+ errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
532
1230
  failureKind = "provider_unavailable";
533
1231
  }
534
- emitEvent({ type: "cli_event", raw: { type: "turn_completed", turn: params.turn } });
1232
+ const safeTurn = params.turn?.error === undefined
1233
+ ? params.turn
1234
+ : { ...params.turn, error: safeResponseError(params.turn.error) };
1235
+ emitEvent({ type: "cli_event", raw: { type: "turn_completed", turn: safeTurn } });
535
1236
  resolveTurn(params.turn);
536
1237
  return;
537
1238
  }
@@ -552,7 +1253,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
552
1253
  emitEvent({
553
1254
  type: "runtime_warning",
554
1255
  warning_kind: method.replace(/\W+/g, "_"),
555
- message: params.message || params.error || JSON.stringify(params),
1256
+ message: safeDiagnostic(params.message || params.error || params),
556
1257
  });
557
1258
  return;
558
1259
  }
@@ -582,7 +1283,15 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
582
1283
  args: options.codexAppServerArgs,
583
1284
  cwd: options.cwd,
584
1285
  env: options.codexAppServerEnv,
585
- onNotification: (notification) => notificationTarget.handler(notification),
1286
+ redactionValues: sensitiveValues,
1287
+ onNotification: (notification) => notificationTarget.handler(
1288
+ sanitizeCodexNotification(notification, sensitiveValues),
1289
+ ),
1290
+ onServerRequest: (request) => {
1291
+ const method = typeof request?.method === "string" ? request.method : "unknown";
1292
+ failUnsupportedServerRequest(method);
1293
+ throw new Error(`Unsupported Codex app-server request: ${method}`);
1294
+ },
586
1295
  });
587
1296
  }
588
1297
 
@@ -592,6 +1301,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
592
1301
  clientInfo: { name: brand.clientInfoName, title: brand.clientInfoTitle, version: "0" },
593
1302
  capabilities: { experimentalApi: true },
594
1303
  });
1304
+ assertNoUnsupportedServerRequest();
595
1305
  }
596
1306
 
597
1307
  async function requestThreadStart(params) {
@@ -605,6 +1315,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
605
1315
  }
606
1316
  try {
607
1317
  const thread = await client.request("thread/start", params, { timeoutMs: policy.timeoutMs });
1318
+ assertNoUnsupportedServerRequest();
608
1319
  codexDiagnostics = {
609
1320
  ...withoutCodexRequestErrorDiagnostics(codexDiagnostics),
610
1321
  codex_thread_start_attempts: attempt,
@@ -617,7 +1328,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
617
1328
  lastError = err;
618
1329
  codexDiagnostics = {
619
1330
  ...codexDiagnostics,
620
- ...codexErrorDiagnostics(err),
1331
+ ...codexErrorDiagnostics(err, sensitiveValues),
621
1332
  codex_thread_start_attempts: attempt,
622
1333
  codex_thread_start_timeout_ms: policy.timeoutMs,
623
1334
  codex_thread_start_duration_ms: Date.now() - startedAt,
@@ -631,7 +1342,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
631
1342
  warning_kind: "codex_thread_start_retry",
632
1343
  message: `Codex app-server thread/start timed out after ${policy.timeoutMs}ms; retrying with a fresh app-server.`,
633
1344
  });
634
- client.close();
1345
+ await closeCodexClient(client);
635
1346
  client = null;
636
1347
  await delay(policy.backoffMs, options.abortSignal);
637
1348
  }
@@ -647,7 +1358,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
647
1358
  }
648
1359
  // Resumed sessions stay alive across an interrupt; only fresh runs tear
649
1360
  // down their subprocess on abort.
650
- if (!resumeEntry) client?.close();
1361
+ if (!resumeEntry) void closeCodexClient(client);
651
1362
  };
652
1363
 
653
1364
  async function steerLiveInput() {
@@ -688,11 +1399,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
688
1399
  activeTurnId = response?.turnId || activeTurnId;
689
1400
  continue;
690
1401
  } catch (retryErr) {
691
- const retryProviderError = retryErr?.responseError;
1402
+ const retryProviderError = retryErr?.responseError
1403
+ ? safeResponseError(retryErr.responseError)
1404
+ : null;
692
1405
  emitEvent({
693
1406
  type: "runtime_warning",
694
1407
  warning_kind: isActiveTurnNotSteerable(retryProviderError) ? "active_turn_not_steerable" : "live_input_rejected",
695
- message: codexErrorMessage(retryProviderError || retryErr),
1408
+ message: safeDiagnostic(codexErrorMessage(retryProviderError || retryErr)),
696
1409
  });
697
1410
  continue;
698
1411
  }
@@ -700,7 +1413,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
700
1413
  emitEvent({
701
1414
  type: "runtime_warning",
702
1415
  warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
703
- message: codexErrorMessage(providerError || err),
1416
+ message: safeDiagnostic(codexErrorMessage(
1417
+ providerError ? safeResponseError(providerError) : err,
1418
+ )),
704
1419
  });
705
1420
  }
706
1421
  }
@@ -737,6 +1452,23 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
737
1452
  };
738
1453
  }
739
1454
 
1455
+ if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
1456
+ return sessionUnavailableResult(
1457
+ "skipped_capability_mismatch",
1458
+ "Direct Codex cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.",
1459
+ "codex_sandbox_policy_unsupported",
1460
+ );
1461
+ }
1462
+
1463
+ const toolPolicyProblem = codexToolPolicyProblem(options);
1464
+ if (toolPolicyProblem) {
1465
+ return sessionUnavailableResult(
1466
+ "skipped_capability_mismatch",
1467
+ toolPolicyProblem,
1468
+ "codex_tool_policy_unsupported",
1469
+ );
1470
+ }
1471
+
740
1472
  if (resumeSessionId) {
741
1473
  // Await-free busy claim (get -> busy check -> set-busy in one span). A miss
742
1474
  // fails fast: the host sent no conversation history for a resume, so silently
@@ -776,11 +1508,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
776
1508
  else options.abortSignal.addEventListener("abort", abortHandler, { once: true });
777
1509
  }
778
1510
  if (!resumeEntry) await initializeClient(client);
779
- let collaborationMode = codexCollaborationModePayload(options.nativeSubagents, {
780
- model: resolved.model,
781
- effort: normalizedEffort,
782
- systemPrompt,
783
- });
1511
+ let collaborationMode = noToolsProbe
1512
+ ? null
1513
+ : codexCollaborationModePayload(options.nativeSubagents, {
1514
+ model: resolved.model,
1515
+ effort: normalizedEffort,
1516
+ systemPrompt,
1517
+ });
784
1518
  if (collaborationMode) {
785
1519
  try {
786
1520
  await client.request("collaborationMode/list", {}, { timeoutMs: 5_000 });
@@ -788,20 +1522,26 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
788
1522
  emitEvent({
789
1523
  type: "runtime_warning",
790
1524
  warning_kind: "codex_collaboration_mode_unavailable",
791
- message: codexErrorMessage(err?.responseError || err),
1525
+ message: safeDiagnostic(codexErrorMessage(
1526
+ err?.responseError ? safeResponseError(err.responseError) : err,
1527
+ )),
792
1528
  });
793
1529
  collaborationMode = null;
794
1530
  }
795
1531
  }
796
1532
  const fastMode = codexModelSupportsFastMode(resolved.model) && normalizeFastMode(options.fastMode, true);
797
1533
  if (!resumeEntry) {
798
- const mcpServers = codexMcpConfig(options.mcpServers);
1534
+ const mcpServers = noToolsProbe ? {} : codexMcpConfig(options.mcpServers);
799
1535
  // Incrementally assembled config handed across the codex app-server
800
1536
  // boundary; the reasoning fields below are attached conditionally.
801
1537
  const config = /** @type {any} */ ({
802
1538
  ...(fastMode ? { service_tier: "fast" } : {}),
803
1539
  features: { fast_mode: fastMode },
804
- ...(Object.keys(mcpServers).length ? { mcp_servers: mcpServers } : {}),
1540
+ ...(noToolsProbe
1541
+ ? { mcp_servers: {} }
1542
+ : Object.keys(mcpServers).length
1543
+ ? { mcp_servers: mcpServers }
1544
+ : {}),
805
1545
  });
806
1546
  if (normalizedEffort) {
807
1547
  config.model_reasoning_effort = normalizedEffort;
@@ -816,13 +1556,14 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
816
1556
  modelProvider: "openai",
817
1557
  ...(fastMode ? { serviceTier: "fast" } : {}),
818
1558
  cwd: options.cwd || process.cwd(),
819
- approvalPolicy: approvalPolicyForPermissionMode(options.permissionMode),
820
- sandbox: sandboxForPermissionMode(options.permissionMode),
1559
+ approvalPolicy: approvalPolicyForRun(options),
1560
+ sandbox: sandboxForRun(options),
821
1561
  config,
822
1562
  serviceName: (options.toolContext?.runtimeBrand ?? readRuntimeBrand()).serviceName,
823
1563
  developerInstructions: systemPrompt,
824
1564
  ephemeral: true,
825
1565
  sessionStartSource: "startup",
1566
+ ...(noToolsProbe ? { environments: [], dynamicTools: [], selectedCapabilityRoots: [] } : {}),
826
1567
  experimentalRawEvents: false,
827
1568
  persistExtendedHistory: false,
828
1569
  });
@@ -835,15 +1576,15 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
835
1576
  emitEvent({
836
1577
  type: "runtime_warning",
837
1578
  warning_kind: "live_input_failed",
838
- message: err?.message || String(err),
1579
+ message: safeDiagnostic(err?.message || err),
839
1580
  });
840
1581
  });
841
1582
  const turnParams = {
842
1583
  threadId,
843
1584
  input: userTextInput(prompt),
844
1585
  cwd: options.cwd || process.cwd(),
845
- approvalPolicy: approvalPolicyForPermissionMode(options.permissionMode),
846
- sandboxPolicy: options.permissionMode === "bypassPermissions" ? { type: "dangerFullAccess" } : null,
1586
+ approvalPolicy: approvalPolicyForRun(options),
1587
+ sandboxPolicy: sandboxPolicyForRun(options),
847
1588
  model: resolved.model,
848
1589
  ...(fastMode ? { serviceTier: "fast" } : {}),
849
1590
  effort: normalizedEffort,
@@ -854,16 +1595,20 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
854
1595
  let turn;
855
1596
  try {
856
1597
  turn = await client.request("turn/start", turnParams);
1598
+ assertNoUnsupportedServerRequest();
857
1599
  } catch (err) {
858
1600
  if (!collaborationMode) throw err;
859
1601
  emitEvent({
860
1602
  type: "runtime_warning",
861
1603
  warning_kind: "codex_collaboration_mode_rejected",
862
- message: codexErrorMessage(err?.responseError || err),
1604
+ message: safeDiagnostic(codexErrorMessage(
1605
+ err?.responseError ? safeResponseError(err.responseError) : err,
1606
+ )),
863
1607
  });
864
1608
  const fallbackParams = { ...turnParams };
865
1609
  delete fallbackParams.collaborationMode;
866
1610
  turn = await client.request("turn/start", fallbackParams);
1611
+ assertNoUnsupportedServerRequest();
867
1612
  }
868
1613
  setActiveTurnId(turn?.turn?.id);
869
1614
 
@@ -902,7 +1647,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
902
1647
  ]);
903
1648
  } catch (err) {
904
1649
  if (prematureClose && !errorMessage) {
905
- errorMessage = err?.message || "codex app-server stream closed before turn completed";
1650
+ errorMessage = safeDiagnostic(err?.message || "codex app-server stream closed before turn completed");
906
1651
  failureKind = "provider_unavailable";
907
1652
  } else if (!prematureClose) {
908
1653
  throw err;
@@ -1010,11 +1755,11 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1010
1755
  providerSessionId: threadId || null,
1011
1756
  provider_session_id: threadId || null,
1012
1757
  cancelled: !!options.abortSignal?.aborted,
1013
- error: err?.message || String(err),
1758
+ error: safeDiagnostic(err?.message || err),
1014
1759
  failureKind: failureKind || "provider_unavailable",
1015
1760
  diagnostics: {
1016
1761
  ...codexDiagnostics,
1017
- ...codexErrorDiagnostics(err),
1762
+ ...codexErrorDiagnostics(err, sensitiveValues),
1018
1763
  ...(events.length > 0 || texts.length > 0 ? { had_partial_progress: true } : {}),
1019
1764
  },
1020
1765
  capabilitiesUsed: buildCapabilitiesUsed({
@@ -1035,7 +1780,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1035
1780
  resumeEntry.notificationTarget.handler = noopNotificationHandler;
1036
1781
  resumeEntry.closedTarget.handler = null;
1037
1782
  }
1038
- if (!sessionRetained) client?.close();
1783
+ if (!sessionRetained) await closeCodexClient(client);
1039
1784
  }
1040
1785
  }
1041
1786