@mono-agent/agent-runtime 0.20.11 → 0.20.14

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 (101) hide show
  1. package/MIGRATION.md +264 -25
  2. package/README.md +139 -448
  3. package/package.json +7 -43
  4. package/src/agent/tools/agent-tool.js +5 -5
  5. package/src/agent/tools/bash.js +1 -24
  6. package/src/agent/tools/codex-subscription-search.js +1 -1
  7. package/src/agent/tools/index.js +7 -0
  8. package/src/agent/tools/monitor.js +140 -0
  9. package/src/agent/tools/pi-bridge.js +92 -7
  10. package/src/agent/tools/shared/bash-environment.js +31 -0
  11. package/src/agent/tools/shared/monitors.js +274 -0
  12. package/src/agent/tools/shared/process-runner.js +23 -5
  13. package/src/ai/cost.js +13 -68
  14. package/src/ai/index.js +4 -17
  15. package/src/ai/pi-interop.js +65 -1
  16. package/src/ai/pi-oauth-compat.js +1 -1
  17. package/src/ai/providers/codex/app-server-client.js +592 -0
  18. package/src/ai/providers/pi-models.js +18 -10
  19. package/src/ai/providers/pi-native/compaction-driver.js +49 -21
  20. package/src/ai/providers/pi-native/harness-adapter.js +338 -0
  21. package/src/ai/providers/pi-native/result-builder.js +10 -10
  22. package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
  23. package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
  24. package/src/ai/providers/pi-native/turn-runner.js +34 -15
  25. package/src/ai/providers/pi-native.js +48 -22
  26. package/src/ai/runtime/capabilities.js +11 -56
  27. package/src/ai/runtime/model-refs.js +118 -153
  28. package/src/ai/runtime/registry.js +22 -56
  29. package/src/ai/runtime/router.js +48 -408
  30. package/src/ai/runtime/session-liveness.js +3 -4
  31. package/src/ai/runtime/sessions.js +4 -5
  32. package/src/ai/runtime/tool-policy.js +0 -2
  33. package/src/ai/types.js +12 -108
  34. package/src/index.js +0 -6
  35. package/src/runtime.js +8 -14
  36. package/types/agent/tools/agent-tool.d.ts +1 -2
  37. package/types/agent/tools/codex-subscription-search.d.ts +1 -1
  38. package/types/agent/tools/index.d.ts +1 -0
  39. package/types/agent/tools/monitor.d.ts +44 -0
  40. package/types/agent/tools/pi-bridge.d.ts +3 -2
  41. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  42. package/types/agent/tools/shared/monitors.d.ts +83 -0
  43. package/types/agent/tools/shared/process-runner.d.ts +11 -2
  44. package/types/ai/cost.d.ts +1 -2
  45. package/types/ai/index.d.ts +1 -4
  46. package/types/ai/pi-interop.d.ts +20 -0
  47. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  48. package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
  49. package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
  50. package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
  51. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  52. package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
  53. package/types/ai/providers/pi-native.d.ts +21 -4
  54. package/types/ai/runtime/capabilities.d.ts +21 -70
  55. package/types/ai/runtime/model-refs.d.ts +0 -24
  56. package/types/ai/runtime/router.d.ts +3 -10
  57. package/types/ai/runtime/tool-policy.d.ts +0 -2
  58. package/types/ai/types.d.ts +28 -250
  59. package/types/index.d.ts +0 -1
  60. package/src/ai/providers/acp-client.js +0 -1149
  61. package/src/ai/providers/acp-privacy.js +0 -124
  62. package/src/ai/providers/acp-public.js +0 -21
  63. package/src/ai/providers/acp-session-tokens.js +0 -282
  64. package/src/ai/providers/acp-transport.js +0 -356
  65. package/src/ai/providers/acp.js +0 -543
  66. package/src/ai/providers/claude-cli.js +0 -883
  67. package/src/ai/providers/claude-sandbox.js +0 -71
  68. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  69. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  70. package/src/ai/providers/claude-sdk.js +0 -1127
  71. package/src/ai/providers/claude-subagent-activity.js +0 -719
  72. package/src/ai/providers/claude-subagents.js +0 -88
  73. package/src/ai/providers/codex-app.js +0 -2946
  74. package/src/ai/providers/opencode-app.js +0 -1109
  75. package/src/ai/providers/opencode-discovery.js +0 -39
  76. package/src/ai/providers/opencode-server.js +0 -508
  77. package/src/ai/runtime/context-windows.js +0 -46
  78. package/src/ai/runtime/fast-mode.js +0 -8
  79. package/src/ai/streaming/codex-events.js +0 -146
  80. package/src/ai/streaming/opencode-events.js +0 -59
  81. package/types/ai/providers/acp-client.d.ts +0 -227
  82. package/types/ai/providers/acp-privacy.d.ts +0 -25
  83. package/types/ai/providers/acp-public.d.ts +0 -7
  84. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  85. package/types/ai/providers/acp-transport.d.ts +0 -45
  86. package/types/ai/providers/acp.d.ts +0 -93
  87. package/types/ai/providers/claude-cli.d.ts +0 -305
  88. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  89. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  90. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  91. package/types/ai/providers/claude-sdk.d.ts +0 -138
  92. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  93. package/types/ai/providers/claude-subagents.d.ts +0 -18
  94. package/types/ai/providers/codex-app.d.ts +0 -151
  95. package/types/ai/providers/opencode-app.d.ts +0 -96
  96. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  97. package/types/ai/providers/opencode-server.d.ts +0 -20
  98. package/types/ai/runtime/context-windows.d.ts +0 -9
  99. package/types/ai/runtime/fast-mode.d.ts +0 -2
  100. package/types/ai/streaming/codex-events.d.ts +0 -40
  101. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -0,0 +1,592 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+
4
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
5
+ const CODEX_DIAGNOSTIC_BYTES = 8 * 1024;
6
+ const CODEX_STDERR_TAIL_BYTES = 8 * 1024;
7
+ const CODEX_SHUTDOWN_GRACE_MS = 1_000;
8
+ const CODEX_KILL_GRACE_MS = 1_000;
9
+ export const CODEX_APP_SERVER_ARGS = ["app-server", "--listen", "stdio://"];
10
+ export const CODEX_APP_SERVER_ISOLATED_ARGS = [
11
+ ...CODEX_APP_SERVER_ARGS,
12
+ "-c",
13
+ "project_doc_max_bytes=0",
14
+ ];
15
+
16
+ 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;
17
+ const SENSITIVE_HEADER_RE = /((?:(?:proxy-)?authorization|cookie|set-cookie)\s*[:=]\s*)[^\r\n]*/giu;
18
+ 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;
19
+ 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;
20
+
21
+ export function normalizedSensitiveName(name) {
22
+ return String(name || "")
23
+ .replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
24
+ .replace(/[^A-Za-z0-9]+/gu, "_")
25
+ .replace(/^_+|_+$/gu, "")
26
+ .toLowerCase();
27
+ }
28
+
29
+ export function isSensitivePayloadField(name) {
30
+ const normalized = normalizedSensitiveName(name);
31
+ return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)$/u.test(normalized);
32
+ }
33
+
34
+ function isSensitiveEnvironmentKey(name) {
35
+ const normalized = normalizedSensitiveName(name);
36
+ return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)(?:_|$)/u.test(normalized);
37
+ }
38
+
39
+ function boundedTimeout(value, fallback) {
40
+ const parsed = Number(value);
41
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.trunc(parsed) : fallback;
42
+ }
43
+
44
+ export function sensitiveEnvironmentValues(env) {
45
+ return [...new Set(Object.entries(env || {})
46
+ .filter(([key, value]) => isSensitiveEnvironmentKey(key) && typeof value === "string" && value.length >= 8)
47
+ .map(([, value]) => value))]
48
+ .sort((left, right) => right.length - left.length);
49
+ }
50
+
51
+ export function addOpaqueSensitiveValue(target, value, { splitCredentials = false } = {}) {
52
+ if (typeof value !== "string" || value.length < 8) return;
53
+ target.add(value);
54
+ if (!splitCredentials) return;
55
+ const schemeMatch = value.match(/^\s*(Bearer|Basic|Token)\s+(.+?)\s*$/iu);
56
+ const payload = schemeMatch?.[2];
57
+ if (payload?.length >= 8) target.add(payload);
58
+ if (schemeMatch?.[1]?.toLowerCase() === "basic" && payload && payload.length <= 16 * 1024) {
59
+ try {
60
+ const decoded = Buffer.from(payload, "base64").toString("utf8");
61
+ if (decoded && !decoded.includes("\uFFFD")) {
62
+ addOpaqueSensitiveValue(target, decoded);
63
+ const separator = decoded.indexOf(":");
64
+ if (separator >= 0) {
65
+ addOpaqueSensitiveValue(target, decoded.slice(0, separator));
66
+ addOpaqueSensitiveValue(target, decoded.slice(separator + 1));
67
+ }
68
+ }
69
+ } catch {
70
+ // The raw Basic payload remains protected even if it is malformed.
71
+ }
72
+ }
73
+ }
74
+
75
+ function leadingSensitiveOverlap(text, sensitiveValue) {
76
+ const maxLength = Math.min(text.length, sensitiveValue.length, CODEX_STDERR_TAIL_BYTES);
77
+ if (maxLength < 8) return 0;
78
+ const pattern = text.slice(0, maxLength);
79
+ const failure = new Array(pattern.length).fill(0);
80
+ for (let index = 1, matched = 0; index < pattern.length; index += 1) {
81
+ while (matched > 0 && pattern[index] !== pattern[matched]) matched = failure[matched - 1];
82
+ if (pattern[index] === pattern[matched]) matched += 1;
83
+ failure[index] = matched;
84
+ }
85
+ let matched = 0;
86
+ for (const character of sensitiveValue.slice(-maxLength)) {
87
+ while (matched > 0 && character !== pattern[matched]) matched = failure[matched - 1];
88
+ if (character === pattern[matched]) matched += 1;
89
+ }
90
+ return matched >= 8 ? matched : 0;
91
+ }
92
+
93
+ export function redactCodexDiagnostic(text, sensitiveValues, truncatedStart = false) {
94
+ let redacted = String(text || "");
95
+ for (const value of sensitiveValues) {
96
+ if (truncatedStart) {
97
+ const overlap = leadingSensitiveOverlap(redacted, value);
98
+ if (overlap > 0) redacted = `[REDACTED]${redacted.slice(overlap)}`;
99
+ }
100
+ redacted = redacted.split(value).join("[REDACTED]");
101
+ }
102
+ return redacted
103
+ .replace(SENSITIVE_HEADER_RE, "$1[REDACTED]")
104
+ .replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{12,}/giu, "Bearer [REDACTED]")
105
+ .replace(/\b(?:sk|pk|sess|oauth)[-_][A-Za-z0-9._-]{12,}\b/giu, "[REDACTED]")
106
+ .replace(SENSITIVE_ESCAPED_JSON_LINE_RE, '$1\\"[REDACTED]\\"')
107
+ .replace(SENSITIVE_JSON_LINE_RE, '$1"[REDACTED]"')
108
+ .replace(SENSITIVE_ASSIGNMENT_RE, "$1[REDACTED]");
109
+ }
110
+
111
+ export function utf8Head(text, limit) {
112
+ if (limit <= 0) return "";
113
+ const bytes = Buffer.from(String(text || ""));
114
+ if (bytes.length <= limit) return bytes.toString("utf8");
115
+ let end = limit;
116
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
117
+ return bytes.subarray(0, end).toString("utf8");
118
+ }
119
+
120
+ function boundCodexDiagnostic(text, limit = CODEX_DIAGNOSTIC_BYTES) {
121
+ const value = String(text || "");
122
+ const byteLength = Buffer.byteLength(value);
123
+ if (byteLength <= limit) return value;
124
+ let droppedBytes = byteLength - limit;
125
+ let marker = `\n[truncated ${droppedBytes} later bytes]`;
126
+ let bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
127
+ droppedBytes = byteLength - bodyLimit;
128
+ marker = `\n[truncated ${droppedBytes} later bytes]`;
129
+ bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
130
+ return utf8Head(value, bodyLimit) + marker;
131
+ }
132
+
133
+ function safeDiagnosticString(value) {
134
+ if (typeof value === "string") return value;
135
+ if (value instanceof Error && typeof value.message === "string") return value.message;
136
+ try {
137
+ const serialized = JSON.stringify(value);
138
+ return typeof serialized === "string" ? serialized : String(value ?? "");
139
+ } catch {
140
+ try {
141
+ return String(value);
142
+ } catch {
143
+ return "Codex app-server diagnostic unavailable";
144
+ }
145
+ }
146
+ }
147
+
148
+ export function sanitizeCodexDiagnostic(value, sensitiveValues, limit = CODEX_DIAGNOSTIC_BYTES) {
149
+ return boundCodexDiagnostic(
150
+ redactCodexDiagnostic(safeDiagnosticString(value), sensitiveValues),
151
+ limit,
152
+ );
153
+ }
154
+
155
+ function sanitizeCodexProtocolCode(value, sensitiveValues) {
156
+ return typeof value === "number"
157
+ ? value
158
+ : sanitizeCodexDiagnostic(value, sensitiveValues, 256);
159
+ }
160
+
161
+ function boundedCodexDiagnosticPayload(value, sensitiveValues, limit) {
162
+ const sanitized = redactCodexPayload(value, sensitiveValues);
163
+ try {
164
+ if (Buffer.byteLength(JSON.stringify(sanitized) || "") <= limit) return sanitized;
165
+ } catch {
166
+ // Fall through to a safe string summary for non-serializable values.
167
+ }
168
+ return sanitizeCodexDiagnostic(value, sensitiveValues, limit);
169
+ }
170
+
171
+ export function redactCodexPayload(value, sensitiveValues, seen = new WeakSet(), depth = 0) {
172
+ if (typeof value === "string") return redactCodexDiagnostic(value, sensitiveValues);
173
+ if (value === null || typeof value !== "object") return value;
174
+ if (value instanceof Error) {
175
+ const errorCode = /** @type {any} */ (value).code;
176
+ return {
177
+ name: redactCodexDiagnostic(value.name || "Error", sensitiveValues),
178
+ message: sanitizeCodexDiagnostic(value.message || value, sensitiveValues),
179
+ ...(errorCode !== undefined
180
+ ? { code: sanitizeCodexProtocolCode(errorCode, sensitiveValues) }
181
+ : {}),
182
+ };
183
+ }
184
+ if (depth >= 20) return "[truncated nested Codex payload]";
185
+ if (seen.has(value)) return "[circular Codex payload]";
186
+ seen.add(value);
187
+ if (Array.isArray(value)) {
188
+ const result = value.map((entry) => redactCodexPayload(entry, sensitiveValues, seen, depth + 1));
189
+ seen.delete(value);
190
+ return result;
191
+ }
192
+ const result = {};
193
+ for (const [key, entry] of Object.entries(value)) {
194
+ result[key] = isSensitivePayloadField(key)
195
+ ? "[REDACTED]"
196
+ : redactCodexPayload(entry, sensitiveValues, seen, depth + 1);
197
+ }
198
+ seen.delete(value);
199
+ return result;
200
+ }
201
+
202
+ export function sanitizeCodexResponseError(error, sensitiveValues) {
203
+ const sanitized = redactCodexPayload(error, sensitiveValues);
204
+ let serialized;
205
+ try {
206
+ serialized = JSON.stringify(sanitized);
207
+ } catch {
208
+ serialized = "";
209
+ }
210
+ if (Buffer.byteLength(serialized || "") <= CODEX_DIAGNOSTIC_BYTES) return sanitized;
211
+
212
+ const data = error && typeof error === "object" ? error.data : null;
213
+ const nestedError = data && typeof data === "object" ? data.error : null;
214
+ const info = data?.info ?? nestedError?.info ?? error?.info;
215
+ return {
216
+ ...(error?.code !== undefined
217
+ ? { code: sanitizeCodexProtocolCode(error.code, sensitiveValues) }
218
+ : {}),
219
+ message: sanitizeCodexDiagnostic(codexErrorMessage(error), sensitiveValues, 6 * 1024),
220
+ ...(info !== undefined
221
+ ? { data: { info: boundedCodexDiagnosticPayload(info, sensitiveValues, 1_024) } }
222
+ : {}),
223
+ diagnostic_truncated: true,
224
+ };
225
+ }
226
+
227
+ const CODEX_DIAGNOSTIC_NOTIFICATION_METHODS = new Set([
228
+ "warning",
229
+ "error",
230
+ "configWarning",
231
+ "guardianWarning",
232
+ ]);
233
+
234
+ export function sanitizeCodexNotification(notification, sensitiveValues) {
235
+ const safe = redactCodexPayload(notification, sensitiveValues);
236
+ if (!safe || typeof safe !== "object") return safe;
237
+ if (CODEX_DIAGNOSTIC_NOTIFICATION_METHODS.has(safe.method)) {
238
+ const params = safe.params && typeof safe.params === "object" ? safe.params : {};
239
+ return {
240
+ ...safe,
241
+ params: {
242
+ ...(params.code !== undefined
243
+ ? { code: sanitizeCodexProtocolCode(params.code, sensitiveValues) }
244
+ : {}),
245
+ message: sanitizeCodexDiagnostic(params.message || params.error || params, sensitiveValues),
246
+ },
247
+ };
248
+ }
249
+ if (safe.method === "turn/completed" && safe.params?.turn?.error !== undefined) {
250
+ return {
251
+ ...safe,
252
+ params: {
253
+ ...safe.params,
254
+ turn: {
255
+ ...safe.params.turn,
256
+ error: sanitizeCodexResponseError(safe.params.turn.error, sensitiveValues),
257
+ },
258
+ },
259
+ };
260
+ }
261
+ if ((safe.method === "item/started" || safe.method === "item/completed") && safe.params?.item?.error !== undefined) {
262
+ return {
263
+ ...safe,
264
+ params: {
265
+ ...safe.params,
266
+ item: {
267
+ ...safe.params.item,
268
+ error: sanitizeCodexResponseError(safe.params.item.error, sensitiveValues),
269
+ },
270
+ },
271
+ };
272
+ }
273
+ return safe;
274
+ }
275
+
276
+ function utf8Tail(text, limit) {
277
+ if (limit <= 0) return "";
278
+ const bytes = Buffer.from(String(text || ""));
279
+ if (bytes.length <= limit) return bytes.toString("utf8");
280
+ let start = bytes.length - limit;
281
+ while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start += 1;
282
+ return bytes.subarray(start).toString("utf8");
283
+ }
284
+
285
+ function createCodexStderrTail(sensitiveValues, limit = CODEX_STDERR_TAIL_BYTES) {
286
+ let buffer = Buffer.alloc(0);
287
+ let bytesDropped = 0;
288
+ return {
289
+ push(chunk) {
290
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
291
+ if (incoming.length === 0) return;
292
+ if (incoming.length >= limit) {
293
+ bytesDropped += buffer.length + incoming.length - limit;
294
+ buffer = Buffer.from(incoming.subarray(incoming.length - limit));
295
+ return;
296
+ }
297
+ const overflow = Math.max(0, buffer.length + incoming.length - limit);
298
+ bytesDropped += overflow;
299
+ buffer = Buffer.concat([buffer.subarray(overflow), incoming], Math.min(limit, buffer.length + incoming.length));
300
+ },
301
+ toString() {
302
+ const redacted = redactCodexDiagnostic(
303
+ buffer.toString("utf8").replace(/^\uFFFD/u, ""),
304
+ sensitiveValues,
305
+ bytesDropped > 0,
306
+ ).trim();
307
+ if (bytesDropped === 0) return utf8Tail(redacted, limit);
308
+ const marker = `[truncated ${bytesDropped} earlier bytes]\n`;
309
+ const bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
310
+ return marker + utf8Tail(redacted, bodyLimit);
311
+ },
312
+ };
313
+ }
314
+
315
+ export function codexErrorMessage(error) {
316
+ if (!error) return "Codex app-server error";
317
+ if (typeof error === "string") return error;
318
+ const data = error.data || error.error || {};
319
+ const info = data.info || data.code || error.code;
320
+ if (info && typeof info === "object" && "activeTurnNotSteerable" in info) {
321
+ return "Codex active turn is not steerable";
322
+ }
323
+ return error.message || data.message || safeDiagnosticString(error);
324
+ }
325
+
326
+ export function isCodexRequestTimeout(error, method = null) {
327
+ return error?.code === "CODEX_APP_SERVER_REQUEST_TIMEOUT"
328
+ && (!method || error.method === method);
329
+ }
330
+
331
+ /**
332
+ * @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]
333
+ */
334
+ export function createCodexAppServerClient({
335
+ command = "codex",
336
+ // project_doc_max_bytes=0 keeps codex from injecting its own project docs;
337
+ // the host supplies the full context through developerInstructions.
338
+ args = CODEX_APP_SERVER_ISOLATED_ARGS,
339
+ cwd,
340
+ env = {},
341
+ redactionValues = [],
342
+ onNotification = () => {},
343
+ onServerRequest = (message) => {
344
+ throw new Error(`Unsupported Codex app-server request: ${String(message?.method || "unknown")}`);
345
+ },
346
+ shutdownGraceMs = CODEX_SHUTDOWN_GRACE_MS,
347
+ killGraceMs = CODEX_KILL_GRACE_MS,
348
+ } = {}) {
349
+ const childEnv = { ...process.env, ...env };
350
+ const configuredSensitiveValues = new Set();
351
+ for (const value of redactionValues) {
352
+ addOpaqueSensitiveValue(configuredSensitiveValues, value, { splitCredentials: true });
353
+ }
354
+ const sensitiveValues = [...new Set([
355
+ ...sensitiveEnvironmentValues(childEnv),
356
+ ...configuredSensitiveValues,
357
+ ])].sort((left, right) => right.length - left.length);
358
+ const child = spawn(command, args, {
359
+ cwd,
360
+ env: childEnv,
361
+ stdio: ["pipe", "pipe", "pipe"],
362
+ });
363
+ const pending = new Map();
364
+ const stderrTail = createCodexStderrTail(sensitiveValues);
365
+ const shutdownTimers = new Set();
366
+ let nextId = 1;
367
+ let closed = false;
368
+ let processSettled = false;
369
+ let closing = false;
370
+ /** @type {Promise<void> | null} */
371
+ let closePromise = null;
372
+ let resolveClosed;
373
+ const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
374
+
375
+ function rejectAll(err) {
376
+ for (const { reject, timer } of pending.values()) {
377
+ clearTimeout(timer);
378
+ reject(err);
379
+ }
380
+ pending.clear();
381
+ }
382
+
383
+ function safeTransportError(error) {
384
+ const message = sanitizeCodexDiagnostic(error?.message || error || "codex app-server failed", sensitiveValues);
385
+ const safe = new Error(message || "codex app-server failed");
386
+ return error?.code === undefined ? safe : Object.assign(safe, { code: error.code });
387
+ }
388
+
389
+ function onStderrData(chunk) {
390
+ stderrTail.push(chunk);
391
+ }
392
+
393
+ child.stderr.on("data", onStderrData);
394
+
395
+ function writeProtocolMessage(payload) {
396
+ if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) return;
397
+ child.stdin.write(`${JSON.stringify(payload)}\n`, () => {});
398
+ }
399
+
400
+ function respondToServerRequest(message) {
401
+ // Preserve request visibility for the normal event/fail-fast path, then
402
+ // always settle the JSON-RPC request. Never leave the app-server blocked on
403
+ // an inbound request that this unattended client cannot service.
404
+ const safeMessage = redactCodexPayload(message, sensitiveValues);
405
+ onNotification(safeMessage);
406
+ Promise.resolve()
407
+ .then(() => onServerRequest(safeMessage))
408
+ .then(
409
+ (result) => writeProtocolMessage({ id: message.id, result: result ?? {} }),
410
+ () => writeProtocolMessage({
411
+ id: message.id,
412
+ error: { code: -32601, message: `Unsupported Codex app-server request: ${String(message.method || "unknown")}` },
413
+ }),
414
+ );
415
+ }
416
+
417
+ const rl = createInterface({ input: child.stdout });
418
+ function warnMalformedLine(line) {
419
+ onNotification({
420
+ method: "warning",
421
+ params: {
422
+ message: sanitizeCodexDiagnostic(
423
+ `Malformed Codex app-server output: ${line}`,
424
+ sensitiveValues,
425
+ ),
426
+ },
427
+ });
428
+ }
429
+
430
+ function onLine(line) {
431
+ if (!line.trim()) return;
432
+ let message;
433
+ try {
434
+ message = JSON.parse(line);
435
+ } catch {
436
+ warnMalformedLine(line);
437
+ return;
438
+ }
439
+ // A JSON-RPC frame is always a non-null object. Syntactically valid JSON
440
+ // that is not one (`null`, a scalar, an array) must be rejected here: this
441
+ // listener runs on readline's `line` event, so `hasOwnProperty.call(null,
442
+ // ...)` would throw an uncaught TypeError and take the host process down
443
+ // rather than degrading to a warning.
444
+ if (message === null || typeof message !== "object" || Array.isArray(message)) {
445
+ warnMalformedLine(line);
446
+ return;
447
+ }
448
+ if (Object.prototype.hasOwnProperty.call(message, "id") && (message.result !== undefined || message.error !== undefined)) {
449
+ const entry = pending.get(message.id);
450
+ if (!entry) return;
451
+ pending.delete(message.id);
452
+ clearTimeout(entry.timer);
453
+ if (message.error) {
454
+ const responseError = sanitizeCodexResponseError(message.error, sensitiveValues);
455
+ entry.reject(Object.assign(
456
+ new Error(sanitizeCodexDiagnostic(codexErrorMessage(responseError), sensitiveValues)),
457
+ { responseError },
458
+ ));
459
+ }
460
+ else entry.resolve(message.result);
461
+ return;
462
+ }
463
+ if (Object.prototype.hasOwnProperty.call(message, "id") && message.method) {
464
+ respondToServerRequest(message);
465
+ return;
466
+ }
467
+ if (message.method) onNotification(sanitizeCodexNotification(message, sensitiveValues));
468
+ }
469
+ rl.on("line", onLine);
470
+
471
+ function cleanupTransport() {
472
+ for (const timer of shutdownTimers) clearTimeout(timer);
473
+ shutdownTimers.clear();
474
+ rl.off("line", onLine);
475
+ try { rl.close(); } catch {}
476
+ child.stderr?.off?.("data", onStderrData);
477
+ child.off("error", onChildError);
478
+ child.off("close", onChildClose);
479
+ try { child.stdin?.destroy?.(); } catch {}
480
+ try { child.stdout?.destroy?.(); } catch {}
481
+ try { child.stderr?.destroy?.(); } catch {}
482
+ }
483
+
484
+ function settleClosed(error) {
485
+ if (processSettled) return;
486
+ processSettled = true;
487
+ closed = true;
488
+ rejectAll(error);
489
+ cleanupTransport();
490
+ resolveClosed(error);
491
+ }
492
+
493
+ function onChildError(error) {
494
+ const safe = safeTransportError(error);
495
+ closed = true;
496
+ rejectAll(safe);
497
+ // A spawn failure has no live process and may not emit `close`. By contrast,
498
+ // ChildProcess also emits `error` when signaling a live child fails (EPERM,
499
+ // ESRCH races). Only `close` proves that such a process actually exited.
500
+ if (child.pid === undefined) {
501
+ settleClosed(safe);
502
+ return;
503
+ }
504
+ if (!closing) void close();
505
+ }
506
+
507
+ function onChildClose(code, signal) {
508
+ if (closing) {
509
+ settleClosed(new Error("codex app-server closed"));
510
+ return;
511
+ }
512
+ const summary = signal === null
513
+ ? `codex app-server exited ${code ?? "unknown"}`
514
+ : `codex app-server terminated by ${signal}`;
515
+ const detail = stderrTail.toString();
516
+ settleClosed(new Error(detail ? `${summary}: ${detail}` : summary));
517
+ }
518
+
519
+ child.on("error", onChildError);
520
+ child.once("close", onChildClose);
521
+
522
+ function request(method, params, { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
523
+ if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) {
524
+ return Promise.reject(new Error("codex app-server is not running"));
525
+ }
526
+ const id = nextId++;
527
+ const payload = { id, method, params };
528
+ return new Promise((resolve, reject) => {
529
+ const timer = setTimeout(() => {
530
+ pending.delete(id);
531
+ reject(Object.assign(new Error(`codex app-server request timed out: ${method}`), {
532
+ code: "CODEX_APP_SERVER_REQUEST_TIMEOUT",
533
+ method,
534
+ timeoutMs,
535
+ stderrTail: stderrTail.toString(),
536
+ }));
537
+ }, timeoutMs);
538
+ timer.unref?.();
539
+ pending.set(id, { resolve, reject, timer });
540
+ child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
541
+ if (!err) return;
542
+ pending.delete(id);
543
+ clearTimeout(timer);
544
+ reject(safeTransportError(err));
545
+ });
546
+ });
547
+ }
548
+
549
+ function waitForProcessClose(timeoutMs) {
550
+ if (processSettled) return Promise.resolve(true);
551
+ return new Promise((resolve) => {
552
+ let settled = false;
553
+ let timer;
554
+ const finish = (didClose) => {
555
+ if (settled) return;
556
+ settled = true;
557
+ if (timer !== undefined) {
558
+ clearTimeout(timer);
559
+ shutdownTimers.delete(timer);
560
+ }
561
+ resolve(didClose);
562
+ };
563
+ timer = setTimeout(() => finish(false), timeoutMs);
564
+ timer.unref?.();
565
+ shutdownTimers.add(timer);
566
+ closedPromise.then(() => finish(true));
567
+ });
568
+ }
569
+
570
+ function close() {
571
+ if (closePromise !== null) return closePromise;
572
+ closePromise = (async () => {
573
+ closing = true;
574
+ closed = true;
575
+ rejectAll(new Error("codex app-server closed"));
576
+ if (processSettled) return;
577
+
578
+ try { child.stdin?.end?.(); } catch {}
579
+ try { child.kill("SIGTERM"); } catch {}
580
+ if (await waitForProcessClose(boundedTimeout(shutdownGraceMs, CODEX_SHUTDOWN_GRACE_MS))) return;
581
+
582
+ try { child.kill("SIGKILL"); } catch {}
583
+ if (await waitForProcessClose(boundedTimeout(killGraceMs, CODEX_KILL_GRACE_MS))) return;
584
+
585
+ try { child.unref?.(); } catch {}
586
+ settleClosed(new Error("codex app-server did not exit after SIGKILL"));
587
+ })();
588
+ return closePromise;
589
+ }
590
+
591
+ return { request, close, child, closed: closedPromise };
592
+ }
@@ -114,26 +114,34 @@ function resolveCustomPiModel(resolved, options) {
114
114
  // through the same custom provider once options.customProvider is set for the
115
115
  // run (the router does not re-derive customProvider per chain entry).
116
116
  export function resolvePiRuntimeModel(resolved, options) {
117
- if (options.customProvider) return resolveCustomPiModel(resolved, options);
118
- if (resolved.sdk !== "pi") throw new Error(`unsupported pi sdk: ${resolved.sdk}`);
119
117
  const provider = resolved.provider;
120
- const model = getPiModel(provider, resolved.model);
121
- if (!model) {
118
+ const model = resolved.model;
119
+ if (
120
+ typeof provider !== "string"
121
+ || provider.length === 0
122
+ || typeof model !== "string"
123
+ || model.length === 0
124
+ ) {
125
+ throw new Error("invalid pi model reference: provider and model are required");
126
+ }
127
+ if (options.customProvider) return resolveCustomPiModel(resolved, options);
128
+ const catalogModel = getPiModel(/** @type {*} */ (provider), model);
129
+ if (!catalogModel) {
122
130
  // Phrasing matters: this must match ai/failure.js's NON_RETRYABLE_PROVIDER_RE
123
131
  // `model[_ ]not[_ ]found` alternation so the router classifies a catalog miss
124
132
  // as non-retryable and bails cleanly instead of retrying/misclassifying it as
125
133
  // a transient provider_unavailable failure.
126
- throw new Error(`pi model not found: ${provider}:${resolved.model}`);
134
+ throw new Error(`pi model not found: ${provider}:${model}`);
127
135
  }
128
136
  return {
129
- model,
137
+ model: catalogModel,
130
138
  capabilities: {
131
139
  tool_use: true,
132
- reasoning: !!model.reasoning,
133
- reasoning_mode: model.reasoning ? "effort" : "none",
134
- reasoning_levels: model.reasoning ? reasoningLevelsForPiModel(model) : undefined,
140
+ reasoning: !!catalogModel.reasoning,
141
+ reasoning_mode: catalogModel.reasoning ? "effort" : "none",
142
+ reasoning_levels: catalogModel.reasoning ? reasoningLevelsForPiModel(catalogModel) : undefined,
135
143
  reasoning_disable_supported: true,
136
- vision: Array.isArray(model.input) ? model.input.includes("image") : false,
144
+ vision: Array.isArray(catalogModel.input) ? catalogModel.input.includes("image") : false,
137
145
  json_mode: true,
138
146
  },
139
147
  apiKeys: new Map(),