@mono-agent/agent-runtime 0.6.1 → 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.
- package/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +15 -42
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/file-change-stats.js +0 -21
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +315 -163
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/streaming/codex-events.js +7 -15
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/file-change-stats.d.ts +0 -24
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/streaming/codex-events.d.ts +15 -6
- package/types/ai/types.d.ts +75 -2
- 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
|
|
112
|
-
if (
|
|
113
|
-
if (permissionMode === "
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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 ||
|
|
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
|
|
650
|
+
codex_request_method: error.method ? sanitizeCodexDiagnostic(error.method, sensitiveValues, 256) : null,
|
|
178
651
|
codex_request_timeout_ms: error.timeoutMs || null,
|
|
179
|
-
...(error.stderrTail
|
|
652
|
+
...(error.stderrTail
|
|
653
|
+
? { stderr_tail: sanitizeCodexDiagnostic(error.stderrTail, sensitiveValues) }
|
|
654
|
+
: {}),
|
|
180
655
|
};
|
|
181
656
|
}
|
|
182
|
-
return 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:
|
|
732
|
+
env: childEnv,
|
|
241
733
|
stdio: ["pipe", "pipe", "pipe"],
|
|
242
734
|
});
|
|
243
735
|
const pending = new Map();
|
|
244
|
-
const
|
|
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
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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)
|
|
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)
|
|
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
|
-
|
|
843
|
+
function settleClosed(error) {
|
|
844
|
+
if (processSettled) return;
|
|
845
|
+
processSettled = true;
|
|
289
846
|
closed = true;
|
|
290
|
-
rejectAll(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
847
|
+
rejectAll(error);
|
|
848
|
+
cleanupTransport();
|
|
849
|
+
resolveClosed(error);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function onChildError(error) {
|
|
853
|
+
const safe = safeTransportError(error);
|
|
294
854
|
closed = true;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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 (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
|
468
|
-
//
|
|
469
|
-
const
|
|
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
|
-
|
|
512
|
-
|
|
1146
|
+
const safeEvent = redactCodexPayload(event, sensitiveValues);
|
|
1147
|
+
events.push(safeEvent);
|
|
1148
|
+
options.onEvent?.(safeEvent);
|
|
513
1149
|
}
|
|
514
1150
|
|
|
515
1151
|
function handleAgentText(text) {
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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(
|
|
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
|
-
...(
|
|
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:
|
|
820
|
-
sandbox:
|
|
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 ||
|
|
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:
|
|
846
|
-
sandboxPolicy: options
|
|
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(
|
|
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 ||
|
|
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
|
|
1783
|
+
if (!sessionRetained) await closeCodexClient(client);
|
|
1039
1784
|
}
|
|
1040
1785
|
}
|
|
1041
1786
|
|