@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.
- package/MIGRATION.md +264 -25
- package/README.md +139 -448
- package/package.json +7 -43
- package/src/agent/tools/agent-tool.js +5 -5
- package/src/agent/tools/bash.js +1 -24
- package/src/agent/tools/codex-subscription-search.js +1 -1
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +140 -0
- package/src/agent/tools/pi-bridge.js +92 -7
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +274 -0
- package/src/agent/tools/shared/process-runner.js +23 -5
- package/src/ai/cost.js +13 -68
- package/src/ai/index.js +4 -17
- package/src/ai/pi-interop.js +65 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +49 -21
- package/src/ai/providers/pi-native/harness-adapter.js +338 -0
- package/src/ai/providers/pi-native/result-builder.js +10 -10
- package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
- package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
- package/src/ai/providers/pi-native/turn-runner.js +34 -15
- package/src/ai/providers/pi-native.js +48 -22
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +48 -408
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/types.js +12 -108
- package/src/index.js +0 -6
- package/src/runtime.js +8 -14
- package/types/agent/tools/agent-tool.d.ts +1 -2
- package/types/agent/tools/codex-subscription-search.d.ts +1 -1
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +44 -0
- package/types/agent/tools/pi-bridge.d.ts +3 -2
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +83 -0
- package/types/agent/tools/shared/process-runner.d.ts +11 -2
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +1 -4
- package/types/ai/pi-interop.d.ts +20 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/types.d.ts +28 -250
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- 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 =
|
|
121
|
-
if (
|
|
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}:${
|
|
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: !!
|
|
133
|
-
reasoning_mode:
|
|
134
|
-
reasoning_levels:
|
|
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(
|
|
144
|
+
vision: Array.isArray(catalogModel.input) ? catalogModel.input.includes("image") : false,
|
|
137
145
|
json_mode: true,
|
|
138
146
|
},
|
|
139
147
|
apiKeys: new Map(),
|