@rynx-ai/runtime 0.1.0 → 0.1.10-beta.2
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/dist/claude/executor.d.ts +3 -5
- package/dist/claude/executor.js +3 -5
- package/dist/claude/native-bridge.d.ts +74 -17
- package/dist/claude/native-bridge.js +225 -30
- package/dist/claude/native-hook-main.js +327 -38
- package/dist/claude/native-hooks.d.ts +3 -2
- package/dist/claude/native-hooks.js +15 -6
- package/dist/claude/native-integration.d.ts +123 -16
- package/dist/claude/native-integration.js +624 -81
- package/dist/claude/settings.d.ts +8 -0
- package/dist/claude/settings.js +50 -0
- package/dist/claude/transcript.d.ts +2 -2
- package/dist/claude/transcript.js +14 -3
- package/dist/codex/rollout-synth.d.ts +8 -3
- package/dist/codex/rollout-synth.js +65 -32
- package/dist/codex-app-server/client.d.ts +27 -40
- package/dist/codex-app-server/client.js +1134 -99
- package/dist/codex-app-server/forwarder.d.ts +36 -10
- package/dist/codex-app-server/forwarder.js +146 -28
- package/dist/codex-app-server/mapping.d.ts +1 -1
- package/dist/codex-app-server/mapping.js +64 -5
- package/dist/codex-app-server/protocol.d.ts +269 -4
- package/dist/codex-app-server/transport.d.ts +20 -5
- package/dist/codex-app-server/transport.js +93 -40
- package/dist/codex-app-server/ws-channel.d.ts +3 -3
- package/dist/codex-app-server/ws-channel.js +23 -7
- package/dist/codex-child-env.js +33 -0
- package/dist/codex-home.d.ts +16 -6
- package/dist/codex-home.js +46 -15
- package/dist/codex-session-store.d.ts +2 -1
- package/dist/host.d.ts +38 -38
- package/dist/host.js +626 -121
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +13 -0
- package/dist/input-resources.js +67 -0
- package/dist/interactions.d.ts +61 -0
- package/dist/interactions.js +236 -0
- package/dist/models-catalog.d.ts +5 -13
- package/dist/models-catalog.js +60 -9
- package/dist/runner/child.d.ts +9 -1
- package/dist/runner/child.js +100 -19
- package/dist/runner/manager.d.ts +79 -11
- package/dist/runner/manager.js +423 -43
- package/dist/runner/protocol.d.ts +30 -11
- package/dist/runner-main.js +9 -6
- package/dist/runtime-status.js +1 -1
- package/dist/terminal/claude-tui.d.ts +8 -3
- package/dist/terminal/claude-tui.js +6 -2
- package/dist/terminal/codex-tui.d.ts +3 -3
- package/dist/terminal/codex-tui.js +1 -1
- package/dist/terminal/registry.d.ts +1 -1
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +6 -6
- package/dist/terminal/tmux.js +10 -10
- package/package.json +8 -3
|
@@ -1,38 +1,977 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CodexAppServerTransport, } from "./transport.js";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CodexAppServerTransport, NO_SERVER_RESPONSE, } from "./transport.js";
|
|
3
|
+
import { boundInteractionRequest, INTERACTION_LIMITS, redactInteractionResolution, validateInteractionResolution, } from "../interactions.js";
|
|
3
4
|
const DEFAULT_CLIENT_INFO = {
|
|
4
5
|
name: "lark-agent-bridge",
|
|
5
6
|
title: "lark-agent-bridge",
|
|
6
7
|
version: "0.1.0",
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
class InteractionRequestBoundsError extends Error {
|
|
10
|
+
}
|
|
11
|
+
class UnsupportedInteractionSchemaError extends Error {
|
|
12
|
+
}
|
|
13
|
+
const PERMISSION_SUMMARY_BYTES = INTERACTION_LIMITS.descriptionBytes;
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
function stringValue(value) {
|
|
20
|
+
return typeof value === "string" && value ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function boundedText(value, max = 8_000) {
|
|
23
|
+
const text = stringValue(value);
|
|
24
|
+
if (!text)
|
|
25
|
+
return undefined;
|
|
26
|
+
return text.length <= max ? text : `${text.slice(0, max)}\n…`;
|
|
27
|
+
}
|
|
28
|
+
function commandText(value) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const parts = value.filter((part) => typeof part === "string");
|
|
31
|
+
return boundedText(parts.join(" "));
|
|
32
|
+
}
|
|
33
|
+
return boundedText(value);
|
|
34
|
+
}
|
|
35
|
+
function legacyPatchPreview(fileChanges) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for (const [path, change] of Object.entries(fileChanges)) {
|
|
38
|
+
const detail = change.type === "update" ? change.unified_diff : change.content;
|
|
39
|
+
chunks.push(`${path} (${change.type})\n${detail}`);
|
|
40
|
+
}
|
|
41
|
+
return boundedText(chunks.join("\n\n"));
|
|
42
|
+
}
|
|
43
|
+
function requestIdKey(value) {
|
|
44
|
+
return `${typeof value}:${String(value)}`;
|
|
45
|
+
}
|
|
46
|
+
function codexInteractionId(method, requestId, params) {
|
|
47
|
+
const record = asRecord(params);
|
|
48
|
+
const identity = JSON.stringify([
|
|
49
|
+
method,
|
|
50
|
+
requestIdKey(requestId),
|
|
51
|
+
stringValue(record?.threadId) ?? stringValue(record?.conversationId) ?? "",
|
|
52
|
+
stringValue(record?.turnId) ?? "",
|
|
53
|
+
stringValue(record?.itemId) ??
|
|
54
|
+
stringValue(record?.callId) ??
|
|
55
|
+
stringValue(record?.approvalId) ??
|
|
56
|
+
stringValue(record?.elicitationId) ??
|
|
57
|
+
"",
|
|
58
|
+
]);
|
|
59
|
+
return `codex_${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
|
|
60
|
+
}
|
|
61
|
+
function permissionActions() {
|
|
62
|
+
return [
|
|
63
|
+
{ id: "accept", label: "Allow once", style: "primary", requiresAnswers: false },
|
|
64
|
+
{
|
|
65
|
+
id: "accept_session",
|
|
66
|
+
label: "Allow, and don’t ask again for this session",
|
|
67
|
+
requiresAnswers: false,
|
|
68
|
+
},
|
|
69
|
+
{ id: "decline", label: "Deny", style: "danger", requiresAnswers: false },
|
|
70
|
+
{ id: "cancel", label: "Cancel", requiresAnswers: false },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function exactJsonSummary(label, value) {
|
|
74
|
+
if (value === undefined || value === null)
|
|
75
|
+
return undefined;
|
|
76
|
+
const serialized = JSON.stringify(value);
|
|
77
|
+
if (serialized === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
const result = `${label}: ${serialized}`;
|
|
80
|
+
if (Buffer.byteLength(result, "utf8") > PERMISSION_SUMMARY_BYTES) {
|
|
81
|
+
throw new InteractionRequestBoundsError(`${label} exceeds the presentation limit`);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function joinedSummary(parts) {
|
|
86
|
+
const present = parts.filter((part) => Boolean(part));
|
|
87
|
+
if (present.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
const result = present.join(" · ");
|
|
90
|
+
if (Buffer.byteLength(result, "utf8") > PERMISSION_SUMMARY_BYTES) {
|
|
91
|
+
throw new InteractionRequestBoundsError("permission summary exceeds the presentation limit");
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function commandApprovalSummary(input) {
|
|
96
|
+
return joinedSummary([
|
|
97
|
+
exactJsonSummary("Network target", input.networkApprovalContext),
|
|
98
|
+
exactJsonSummary("Command actions", input.commandActions),
|
|
99
|
+
exactJsonSummary("Additional permissions", input.additionalPermissions),
|
|
100
|
+
exactJsonSummary("Exec policy amendment", input.proposedExecpolicyAmendment),
|
|
101
|
+
exactJsonSummary("Network policy amendments", input.proposedNetworkPolicyAmendments),
|
|
102
|
+
exactJsonSummary("Available decisions", input.availableDecisions),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
function commandDecisionActions(input) {
|
|
106
|
+
const advertised = input.availableDecisions;
|
|
107
|
+
const decisions = advertised == null
|
|
108
|
+
? ["accept", "acceptForSession", "decline", "cancel"]
|
|
109
|
+
: advertised;
|
|
110
|
+
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
111
|
+
throw new UnsupportedInteractionSchemaError("command approval has no available decisions");
|
|
112
|
+
}
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
return decisions.map((rawDecision, index) => {
|
|
115
|
+
let action;
|
|
116
|
+
let decision;
|
|
117
|
+
if (typeof rawDecision === "string") {
|
|
118
|
+
decision = rawDecision;
|
|
119
|
+
action = {
|
|
120
|
+
accept: {
|
|
121
|
+
id: "accept",
|
|
122
|
+
label: "Allow once",
|
|
123
|
+
style: "primary",
|
|
124
|
+
requiresAnswers: false,
|
|
125
|
+
},
|
|
126
|
+
acceptForSession: {
|
|
127
|
+
id: "accept_session",
|
|
128
|
+
label: "Allow, and don’t ask again for this session",
|
|
129
|
+
requiresAnswers: false,
|
|
130
|
+
},
|
|
131
|
+
decline: {
|
|
132
|
+
id: "decline",
|
|
133
|
+
label: "Deny",
|
|
134
|
+
style: "danger",
|
|
135
|
+
requiresAnswers: false,
|
|
136
|
+
},
|
|
137
|
+
cancel: { id: "cancel", label: "Cancel", requiresAnswers: false },
|
|
138
|
+
}[rawDecision];
|
|
139
|
+
if (!action) {
|
|
140
|
+
throw new UnsupportedInteractionSchemaError(`unsupported command approval decision: ${String(rawDecision)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const record = asRecord(rawDecision);
|
|
145
|
+
const execPolicy = asRecord(record?.acceptWithExecpolicyAmendment);
|
|
146
|
+
const execAmendment = execPolicy?.execpolicy_amendment;
|
|
147
|
+
const networkPolicy = asRecord(record?.applyNetworkPolicyAmendment);
|
|
148
|
+
const networkAmendment = asRecord(networkPolicy?.network_policy_amendment);
|
|
149
|
+
if (record && Object.keys(record).length === 1 &&
|
|
150
|
+
execPolicy && Object.keys(execPolicy).length === 1 &&
|
|
151
|
+
Array.isArray(execAmendment) &&
|
|
152
|
+
execAmendment.every((value) => typeof value === "string")) {
|
|
153
|
+
decision = rawDecision;
|
|
154
|
+
action = {
|
|
155
|
+
id: `accept_execpolicy_amendment_${index}`,
|
|
156
|
+
label: "Allow, and don’t ask again for this command rule",
|
|
157
|
+
requiresAnswers: false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else if (record && Object.keys(record).length === 1 &&
|
|
161
|
+
networkPolicy && Object.keys(networkPolicy).length === 1 &&
|
|
162
|
+
networkAmendment && Object.keys(networkAmendment).length === 2 &&
|
|
163
|
+
typeof networkAmendment.host === "string" && networkAmendment.host.length > 0 &&
|
|
164
|
+
(networkAmendment.action === "allow" || networkAmendment.action === "deny")) {
|
|
165
|
+
decision = rawDecision;
|
|
166
|
+
action = {
|
|
167
|
+
id: `apply_network_policy_amendment_${index}`,
|
|
168
|
+
label: networkAmendment.action === "allow"
|
|
169
|
+
? `Allow, and don’t ask again for ${networkAmendment.host}`
|
|
170
|
+
: `Deny ${networkAmendment.host}`,
|
|
171
|
+
requiresAnswers: false,
|
|
172
|
+
...(networkAmendment.action === "allow" ? { style: "primary" } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
throw new UnsupportedInteractionSchemaError(`unsupported command approval decision at index ${index}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (seen.has(action.id)) {
|
|
180
|
+
throw new UnsupportedInteractionSchemaError(`duplicate command approval decision: ${action.id}`);
|
|
181
|
+
}
|
|
182
|
+
seen.add(action.id);
|
|
183
|
+
return { action, decision };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function assertOnlyKeys(value, allowed, scope) {
|
|
187
|
+
const allowedSet = new Set(allowed);
|
|
188
|
+
const unsupported = Object.keys(value).find((key) => !allowedSet.has(key));
|
|
189
|
+
if (unsupported) {
|
|
190
|
+
throw new UnsupportedInteractionSchemaError(`${scope} uses unsupported keyword: ${unsupported}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function optionalFiniteNumber(value, name) {
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
197
|
+
throw new UnsupportedInteractionSchemaError(`${name} must be a finite number`);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
function optionalNonNegativeInteger(value, name) {
|
|
202
|
+
const parsed = optionalFiniteNumber(value, name);
|
|
203
|
+
if (parsed !== undefined && (!Number.isInteger(parsed) || parsed < 0)) {
|
|
204
|
+
throw new UnsupportedInteractionSchemaError(`${name} must be a non-negative integer`);
|
|
205
|
+
}
|
|
206
|
+
return parsed;
|
|
207
|
+
}
|
|
208
|
+
function schemaDescription(description, constraints) {
|
|
209
|
+
const values = [description, ...constraints].filter((value) => Boolean(value));
|
|
210
|
+
return values.length > 0 ? values.join(" ") : undefined;
|
|
211
|
+
}
|
|
212
|
+
function titledOptions(value, scope) {
|
|
213
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
214
|
+
throw new UnsupportedInteractionSchemaError(`${scope} must contain at least one option`);
|
|
215
|
+
}
|
|
216
|
+
if (value.length > INTERACTION_LIMITS.optionsPerField) {
|
|
217
|
+
throw new InteractionRequestBoundsError(`${scope} has too many options`);
|
|
218
|
+
}
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
return value.map((raw, index) => {
|
|
221
|
+
const option = asRecord(raw);
|
|
222
|
+
const optionValue = option?.const;
|
|
223
|
+
const title = option?.title;
|
|
224
|
+
if (!option || Object.keys(option).some((key) => key !== "const" && key !== "title") ||
|
|
225
|
+
typeof optionValue !== "string" || !optionValue || typeof title !== "string" || !title) {
|
|
226
|
+
throw new UnsupportedInteractionSchemaError(`${scope}[${index}] is not a titled string option`);
|
|
227
|
+
}
|
|
228
|
+
if (seen.has(optionValue)) {
|
|
229
|
+
throw new UnsupportedInteractionSchemaError(`${scope} contains duplicate option values`);
|
|
230
|
+
}
|
|
231
|
+
seen.add(optionValue);
|
|
232
|
+
return { value: optionValue, label: title };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function enumOptions(values, labels, scope) {
|
|
236
|
+
if (!Array.isArray(values) || values.length === 0 ||
|
|
237
|
+
!values.every((value) => typeof value === "string" && value.length > 0)) {
|
|
238
|
+
throw new UnsupportedInteractionSchemaError(`${scope} must be a non-empty string enum`);
|
|
239
|
+
}
|
|
240
|
+
if (values.length > INTERACTION_LIMITS.optionsPerField) {
|
|
241
|
+
throw new InteractionRequestBoundsError(`${scope} has too many options`);
|
|
242
|
+
}
|
|
243
|
+
if (labels !== undefined &&
|
|
244
|
+
(!Array.isArray(labels) || labels.length !== values.length ||
|
|
245
|
+
!labels.every((label) => typeof label === "string" && label.length > 0))) {
|
|
246
|
+
throw new UnsupportedInteractionSchemaError(`${scope} enumNames must match enum`);
|
|
247
|
+
}
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
return values.map((value, index) => {
|
|
250
|
+
if (seen.has(value)) {
|
|
251
|
+
throw new UnsupportedInteractionSchemaError(`${scope} contains duplicate option values`);
|
|
252
|
+
}
|
|
253
|
+
seen.add(value);
|
|
254
|
+
return { value, label: Array.isArray(labels) ? labels[index] : value };
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function hasOwn(value, key) {
|
|
258
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
259
|
+
}
|
|
260
|
+
function stringConstraints(property, scope, ErrorType = Error) {
|
|
261
|
+
const minLength = optionalNonNegativeInteger(property.minLength, `${scope}.minLength`);
|
|
262
|
+
const maxLength = optionalNonNegativeInteger(property.maxLength, `${scope}.maxLength`);
|
|
263
|
+
if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
|
|
264
|
+
throw new ErrorType(`${scope} has minLength greater than maxLength`);
|
|
265
|
+
}
|
|
266
|
+
const format = property.format;
|
|
267
|
+
if (format !== undefined && format !== "email" && format !== "uri" &&
|
|
268
|
+
format !== "date" && format !== "date-time") {
|
|
269
|
+
throw new ErrorType(`${scope} uses unsupported string format: ${String(format)}`);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
...(minLength !== undefined ? { minLength } : {}),
|
|
273
|
+
...(maxLength !== undefined ? { maxLength } : {}),
|
|
274
|
+
...(typeof format === "string" ? { format } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function validateStringValue(value, property, scope, ErrorType = Error) {
|
|
278
|
+
const { minLength, maxLength, format } = stringConstraints(property, scope, ErrorType);
|
|
279
|
+
const length = [...value].length;
|
|
280
|
+
if (minLength !== undefined && length < minLength) {
|
|
281
|
+
throw new ErrorType(`${scope} must contain at least ${minLength} characters`);
|
|
282
|
+
}
|
|
283
|
+
if (maxLength !== undefined && length > maxLength) {
|
|
284
|
+
throw new ErrorType(`${scope} must contain at most ${maxLength} characters`);
|
|
285
|
+
}
|
|
286
|
+
if (format === undefined)
|
|
287
|
+
return;
|
|
288
|
+
let valid = false;
|
|
289
|
+
if (format === "email") {
|
|
290
|
+
valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
291
|
+
}
|
|
292
|
+
else if (format === "uri") {
|
|
293
|
+
try {
|
|
294
|
+
valid = Boolean(new URL(value).protocol);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
valid = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (format === "date") {
|
|
301
|
+
const timestamp = Date.parse(`${value}T00:00:00.000Z`);
|
|
302
|
+
valid = /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(timestamp) &&
|
|
303
|
+
new Date(timestamp).toISOString().slice(0, 10) === value;
|
|
304
|
+
}
|
|
305
|
+
else if (format === "date-time") {
|
|
306
|
+
valid = value.includes("T") && Number.isFinite(Date.parse(value));
|
|
307
|
+
}
|
|
308
|
+
if (!valid)
|
|
309
|
+
throw new ErrorType(`${scope} is not a valid ${format}`);
|
|
310
|
+
}
|
|
311
|
+
function mcpFields(schemaValue) {
|
|
312
|
+
const schema = asRecord(schemaValue);
|
|
313
|
+
if (!schema || schema.type !== "object") {
|
|
314
|
+
throw new UnsupportedInteractionSchemaError("MCP form schema must be an object schema");
|
|
315
|
+
}
|
|
316
|
+
assertOnlyKeys(schema, ["$schema", "type", "properties", "required"], "MCP form schema");
|
|
317
|
+
if (schema.$schema !== undefined && typeof schema.$schema !== "string") {
|
|
318
|
+
throw new UnsupportedInteractionSchemaError("MCP form $schema must be a string");
|
|
319
|
+
}
|
|
320
|
+
const properties = asRecord(schema?.properties);
|
|
321
|
+
if (!properties) {
|
|
322
|
+
throw new UnsupportedInteractionSchemaError("MCP form properties must be an object");
|
|
323
|
+
}
|
|
324
|
+
if (Object.keys(properties).length > INTERACTION_LIMITS.fields) {
|
|
325
|
+
throw new InteractionRequestBoundsError("MCP form has too many fields");
|
|
326
|
+
}
|
|
327
|
+
const requiredValues = schema.required ?? [];
|
|
328
|
+
if (!Array.isArray(requiredValues) || !requiredValues.every((value) => typeof value === "string")) {
|
|
329
|
+
throw new UnsupportedInteractionSchemaError("MCP form required must be a string array");
|
|
330
|
+
}
|
|
331
|
+
const required = new Set();
|
|
332
|
+
for (const id of requiredValues) {
|
|
333
|
+
if (!id || required.has(id) || !hasOwn(properties, id)) {
|
|
334
|
+
throw new UnsupportedInteractionSchemaError(`MCP form has invalid required field: ${id}`);
|
|
335
|
+
}
|
|
336
|
+
required.add(id);
|
|
337
|
+
}
|
|
338
|
+
const fields = [];
|
|
339
|
+
for (const [id, raw] of Object.entries(properties)) {
|
|
340
|
+
const property = asRecord(raw);
|
|
341
|
+
if (!property) {
|
|
342
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} must be an object schema`);
|
|
343
|
+
}
|
|
344
|
+
const label = stringValue(property.title) ?? id;
|
|
345
|
+
if (property.title !== undefined && !stringValue(property.title)) {
|
|
346
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid title`);
|
|
347
|
+
}
|
|
348
|
+
const rawDescription = property.description;
|
|
349
|
+
if (rawDescription !== undefined && typeof rawDescription !== "string") {
|
|
350
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid description`);
|
|
351
|
+
}
|
|
352
|
+
const description = rawDescription;
|
|
353
|
+
const requiredWithoutDefault = required.has(id) && !hasOwn(property, "default");
|
|
354
|
+
if (property.type === "string" && hasOwn(property, "oneOf")) {
|
|
355
|
+
assertOnlyKeys(property, ["type", "title", "description", "oneOf", "default"], `MCP field ${id}`);
|
|
356
|
+
const options = titledOptions(property.oneOf, `MCP field ${id}.oneOf`);
|
|
357
|
+
if (hasOwn(property, "default") &&
|
|
358
|
+
(typeof property.default !== "string" || !options.some((option) => option.value === property.default))) {
|
|
359
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
360
|
+
}
|
|
361
|
+
fields.push({
|
|
362
|
+
id,
|
|
363
|
+
type: "select",
|
|
364
|
+
label,
|
|
365
|
+
options,
|
|
366
|
+
...(description ? { description } : {}),
|
|
367
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (property.type === "string" && hasOwn(property, "enum")) {
|
|
372
|
+
assertOnlyKeys(property, ["type", "title", "description", "enum", "enumNames", "default"], `MCP field ${id}`);
|
|
373
|
+
const options = enumOptions(property.enum, property.enumNames, `MCP field ${id}.enum`);
|
|
374
|
+
if (hasOwn(property, "default") &&
|
|
375
|
+
(typeof property.default !== "string" || !options.some((option) => option.value === property.default))) {
|
|
376
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
377
|
+
}
|
|
378
|
+
fields.push({
|
|
379
|
+
id,
|
|
380
|
+
type: "select",
|
|
381
|
+
label,
|
|
382
|
+
options,
|
|
383
|
+
...(description ? { description } : {}),
|
|
384
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (property.type === "array") {
|
|
389
|
+
assertOnlyKeys(property, ["type", "title", "description", "minItems", "maxItems", "items", "default"], `MCP field ${id}`);
|
|
390
|
+
const items = asRecord(property.items);
|
|
391
|
+
if (!items) {
|
|
392
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id}.items must be an enum schema`);
|
|
393
|
+
}
|
|
394
|
+
let options;
|
|
395
|
+
if (hasOwn(items, "anyOf")) {
|
|
396
|
+
assertOnlyKeys(items, ["anyOf"], `MCP field ${id}.items`);
|
|
397
|
+
options = titledOptions(items.anyOf, `MCP field ${id}.items.anyOf`);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
assertOnlyKeys(items, ["type", "enum"], `MCP field ${id}.items`);
|
|
401
|
+
if (items.type !== "string") {
|
|
402
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id}.items must contain strings`);
|
|
403
|
+
}
|
|
404
|
+
options = enumOptions(items.enum, undefined, `MCP field ${id}.items.enum`);
|
|
405
|
+
}
|
|
406
|
+
const minItems = optionalNonNegativeInteger(property.minItems, `MCP field ${id}.minItems`);
|
|
407
|
+
const maxItems = optionalNonNegativeInteger(property.maxItems, `MCP field ${id}.maxItems`);
|
|
408
|
+
if (minItems !== undefined && maxItems !== undefined && minItems > maxItems) {
|
|
409
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has minItems greater than maxItems`);
|
|
410
|
+
}
|
|
411
|
+
if (minItems !== undefined && minItems > options.length) {
|
|
412
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} cannot satisfy minItems`);
|
|
413
|
+
}
|
|
414
|
+
if (hasOwn(property, "default")) {
|
|
415
|
+
if (!Array.isArray(property.default) ||
|
|
416
|
+
!property.default.every((value) => typeof value === "string" && options.some((option) => option.value === value)) ||
|
|
417
|
+
new Set(property.default).size !== property.default.length ||
|
|
418
|
+
(minItems !== undefined && property.default.length < minItems) ||
|
|
419
|
+
(maxItems !== undefined && property.default.length > maxItems)) {
|
|
420
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const constraints = [
|
|
424
|
+
minItems !== undefined ? `Select at least ${minItems}.` : undefined,
|
|
425
|
+
maxItems !== undefined ? `Select at most ${maxItems}.` : undefined,
|
|
426
|
+
hasOwn(property, "default") ? `Default: ${property.default.join(", ")}.` : undefined,
|
|
427
|
+
].filter((value) => Boolean(value));
|
|
428
|
+
fields.push({
|
|
429
|
+
id,
|
|
430
|
+
type: "select",
|
|
431
|
+
label,
|
|
432
|
+
options,
|
|
433
|
+
multiple: true,
|
|
434
|
+
...(schemaDescription(description, constraints)
|
|
435
|
+
? { description: schemaDescription(description, constraints) }
|
|
436
|
+
: {}),
|
|
437
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
438
|
+
});
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (property.type === "boolean") {
|
|
442
|
+
assertOnlyKeys(property, ["type", "title", "description", "default"], `MCP field ${id}`);
|
|
443
|
+
if (hasOwn(property, "default") && typeof property.default !== "boolean") {
|
|
444
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
445
|
+
}
|
|
446
|
+
const constraints = hasOwn(property, "default") ? [`Default: ${String(property.default)}.`] : [];
|
|
447
|
+
fields.push({
|
|
448
|
+
id,
|
|
449
|
+
type: "select",
|
|
450
|
+
label,
|
|
451
|
+
options: [
|
|
452
|
+
{ value: "true", label: "True" },
|
|
453
|
+
{ value: "false", label: "False" },
|
|
454
|
+
],
|
|
455
|
+
...(schemaDescription(description, constraints)
|
|
456
|
+
? { description: schemaDescription(description, constraints) }
|
|
457
|
+
: {}),
|
|
458
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
459
|
+
});
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (property.type === "number" || property.type === "integer") {
|
|
463
|
+
assertOnlyKeys(property, ["type", "title", "description", "minimum", "maximum", "default"], `MCP field ${id}`);
|
|
464
|
+
const minimum = optionalFiniteNumber(property.minimum, `MCP field ${id}.minimum`);
|
|
465
|
+
const maximum = optionalFiniteNumber(property.maximum, `MCP field ${id}.maximum`);
|
|
466
|
+
if (minimum !== undefined && maximum !== undefined && minimum > maximum) {
|
|
467
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has minimum greater than maximum`);
|
|
468
|
+
}
|
|
469
|
+
if (hasOwn(property, "default")) {
|
|
470
|
+
if (typeof property.default !== "number" || !Number.isFinite(property.default) ||
|
|
471
|
+
(property.type === "integer" && !Number.isInteger(property.default)) ||
|
|
472
|
+
(minimum !== undefined && property.default < minimum) ||
|
|
473
|
+
(maximum !== undefined && property.default > maximum)) {
|
|
474
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const constraints = [
|
|
478
|
+
property.type === "integer" ? "Enter an integer." : "Enter a number.",
|
|
479
|
+
minimum !== undefined ? `Minimum: ${minimum}.` : undefined,
|
|
480
|
+
maximum !== undefined ? `Maximum: ${maximum}.` : undefined,
|
|
481
|
+
hasOwn(property, "default") ? `Default: ${String(property.default)}.` : undefined,
|
|
482
|
+
].filter((value) => Boolean(value));
|
|
483
|
+
fields.push({
|
|
484
|
+
id,
|
|
485
|
+
type: "text",
|
|
486
|
+
label,
|
|
487
|
+
...(schemaDescription(description, constraints)
|
|
488
|
+
? { description: schemaDescription(description, constraints) }
|
|
489
|
+
: {}),
|
|
490
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
491
|
+
...(hasOwn(property, "default") ? { placeholder: String(property.default) } : {}),
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (property.type === "string") {
|
|
496
|
+
assertOnlyKeys(property, ["type", "title", "description", "minLength", "maxLength", "format", "default"], `MCP field ${id}`);
|
|
497
|
+
if (hasOwn(property, "default") && typeof property.default !== "string") {
|
|
498
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} has an invalid default`);
|
|
499
|
+
}
|
|
500
|
+
if (hasOwn(property, "default")) {
|
|
501
|
+
validateStringValue(property.default, property, `MCP field ${id}`, UnsupportedInteractionSchemaError);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
stringConstraints(property, `MCP field ${id}`, UnsupportedInteractionSchemaError);
|
|
505
|
+
}
|
|
506
|
+
const minLength = optionalNonNegativeInteger(property.minLength, `MCP field ${id}.minLength`);
|
|
507
|
+
const maxLength = optionalNonNegativeInteger(property.maxLength, `MCP field ${id}.maxLength`);
|
|
508
|
+
const constraints = [
|
|
509
|
+
minLength !== undefined ? `Minimum length: ${minLength}.` : undefined,
|
|
510
|
+
maxLength !== undefined ? `Maximum length: ${maxLength}.` : undefined,
|
|
511
|
+
property.format !== undefined ? `Format: ${String(property.format)}.` : undefined,
|
|
512
|
+
hasOwn(property, "default") ? `Default: ${String(property.default)}.` : undefined,
|
|
513
|
+
].filter((value) => Boolean(value));
|
|
514
|
+
fields.push({
|
|
515
|
+
id,
|
|
516
|
+
type: "text",
|
|
517
|
+
label,
|
|
518
|
+
...(schemaDescription(description, constraints)
|
|
519
|
+
? { description: schemaDescription(description, constraints) }
|
|
520
|
+
: {}),
|
|
521
|
+
...(requiredWithoutDefault ? { required: true } : {}),
|
|
522
|
+
...(hasOwn(property, "default") ? { placeholder: property.default } : {}),
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
throw new UnsupportedInteractionSchemaError(`MCP field ${id} uses unsupported type: ${String(property.type)}`);
|
|
527
|
+
}
|
|
528
|
+
return fields;
|
|
529
|
+
}
|
|
530
|
+
function buildCodexInteraction(method, params, nativeRequestId) {
|
|
531
|
+
const p = asRecord(params) ?? {};
|
|
532
|
+
const interactionId = codexInteractionId(method, nativeRequestId, params);
|
|
533
|
+
const turnId = stringValue(p.turnId);
|
|
534
|
+
const base = {
|
|
535
|
+
interactionId,
|
|
536
|
+
createdAt: Date.now(),
|
|
537
|
+
};
|
|
538
|
+
let request;
|
|
539
|
+
switch (method) {
|
|
540
|
+
case "item/tool/requestUserInput": {
|
|
541
|
+
const input = params;
|
|
542
|
+
const questions = Array.isArray(input.questions) ? input.questions : [];
|
|
543
|
+
const fields = questions.map((question) => {
|
|
544
|
+
const options = Array.isArray(question.options) ? question.options : [];
|
|
545
|
+
if (options.length > 0) {
|
|
546
|
+
return {
|
|
547
|
+
id: question.id,
|
|
548
|
+
type: "select",
|
|
549
|
+
label: question.question,
|
|
550
|
+
description: question.header || undefined,
|
|
551
|
+
required: true,
|
|
552
|
+
...(question.multiSelect ? { multiple: true } : {}),
|
|
553
|
+
allowOther: question.isOther,
|
|
554
|
+
options: options.map((option) => ({
|
|
555
|
+
value: option.label,
|
|
556
|
+
label: option.label,
|
|
557
|
+
...(option.description ? { description: option.description } : {}),
|
|
558
|
+
})),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
id: question.id,
|
|
563
|
+
type: "text",
|
|
564
|
+
label: question.question,
|
|
565
|
+
description: question.header || undefined,
|
|
566
|
+
required: true,
|
|
567
|
+
...(question.isSecret ? { secret: true } : {}),
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
request = {
|
|
571
|
+
...base,
|
|
572
|
+
kind: "question",
|
|
573
|
+
title: questions.length === 1 && questions[0]?.header
|
|
574
|
+
? questions[0].header
|
|
575
|
+
: "Input required",
|
|
576
|
+
fields,
|
|
577
|
+
actions: [{ id: "submit", label: "Submit", style: "primary", requiresAnswers: true }],
|
|
578
|
+
};
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "mcpServer/elicitation/request": {
|
|
582
|
+
const input = params;
|
|
583
|
+
const urlDescription = input.mode === "url" ? `${input.message}\n${input.url}` : input.message;
|
|
584
|
+
request = {
|
|
585
|
+
...base,
|
|
586
|
+
kind: "form",
|
|
587
|
+
title: `${input.serverName || "MCP server"} requests input`,
|
|
588
|
+
description: urlDescription,
|
|
589
|
+
fields: input.mode === "url" ? [] : mcpFields(input.requestedSchema),
|
|
590
|
+
actions: [
|
|
591
|
+
{
|
|
592
|
+
id: "accept",
|
|
593
|
+
label: "Continue",
|
|
594
|
+
style: "primary",
|
|
595
|
+
requiresAnswers: input.mode !== "url",
|
|
596
|
+
},
|
|
597
|
+
{ id: "decline", label: "Decline", style: "danger", requiresAnswers: false },
|
|
598
|
+
{ id: "cancel", label: "Cancel", requiresAnswers: false },
|
|
599
|
+
],
|
|
600
|
+
};
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "item/permissions/requestApproval": {
|
|
604
|
+
const input = params;
|
|
605
|
+
const permissionSummary = exactJsonSummary("Requested permissions", input.permissions);
|
|
606
|
+
request = {
|
|
607
|
+
...base,
|
|
608
|
+
kind: "permission",
|
|
609
|
+
title: "Additional permissions required",
|
|
610
|
+
description: input.reason ?? undefined,
|
|
611
|
+
fields: [],
|
|
612
|
+
actions: [
|
|
613
|
+
{ id: "grant_turn", label: "Allow all for turn", style: "primary", requiresAnswers: false },
|
|
614
|
+
{
|
|
615
|
+
id: "grant_session",
|
|
616
|
+
label: "Allow, and don’t ask again for this session",
|
|
617
|
+
requiresAnswers: false,
|
|
618
|
+
},
|
|
619
|
+
{ id: "decline", label: "Deny", style: "danger", requiresAnswers: false },
|
|
620
|
+
{ id: "cancel", label: "Cancel", requiresAnswers: false },
|
|
621
|
+
],
|
|
622
|
+
context: {
|
|
623
|
+
toolName: "permissions",
|
|
624
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
625
|
+
...(permissionSummary ? { summary: permissionSummary } : {}),
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case "item/fileChange/requestApproval": {
|
|
631
|
+
const input = params;
|
|
632
|
+
request = {
|
|
633
|
+
...base,
|
|
634
|
+
kind: "permission",
|
|
635
|
+
title: input.grantRoot ? `Write access under ${input.grantRoot}` : "Approve file changes",
|
|
636
|
+
description: boundedText(input.reason),
|
|
637
|
+
fields: [],
|
|
638
|
+
actions: permissionActions(),
|
|
639
|
+
context: {
|
|
640
|
+
toolName: "file_change",
|
|
641
|
+
...(input.grantRoot ? { summary: `Write access under ${input.grantRoot}` } : {}),
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case "applyPatchApproval": {
|
|
647
|
+
const input = params;
|
|
648
|
+
const diff = legacyPatchPreview(input.fileChanges ?? {});
|
|
649
|
+
request = {
|
|
650
|
+
...base,
|
|
651
|
+
kind: "permission",
|
|
652
|
+
title: input.grantRoot ? `Write access under ${input.grantRoot}` : "Approve file changes",
|
|
653
|
+
description: boundedText(input.reason),
|
|
654
|
+
fields: [],
|
|
655
|
+
actions: permissionActions(),
|
|
656
|
+
context: {
|
|
657
|
+
toolName: "file_change",
|
|
658
|
+
...(input.grantRoot ? { summary: `Write access under ${input.grantRoot}` } : {}),
|
|
659
|
+
...(diff ? { diff } : {}),
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case "item/commandExecution/requestApproval": {
|
|
665
|
+
const input = params;
|
|
666
|
+
const command = commandText(input.command);
|
|
667
|
+
const reason = boundedText(input.reason);
|
|
668
|
+
const summary = commandApprovalSummary(input);
|
|
669
|
+
request = {
|
|
670
|
+
...base,
|
|
671
|
+
kind: "permission",
|
|
672
|
+
title: input.networkApprovalContext && !command
|
|
673
|
+
? `Approve network access to ${input.networkApprovalContext.host}`
|
|
674
|
+
: "Approve command",
|
|
675
|
+
description: reason,
|
|
676
|
+
fields: [],
|
|
677
|
+
actions: commandDecisionActions(input).map((entry) => entry.action),
|
|
678
|
+
context: {
|
|
679
|
+
toolName: "command_execution",
|
|
680
|
+
...(command ? { command } : {}),
|
|
681
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
682
|
+
...(summary ? { summary } : {}),
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "execCommandApproval": {
|
|
688
|
+
const input = params;
|
|
689
|
+
const command = commandText(input.command);
|
|
690
|
+
const reason = boundedText(input.reason);
|
|
691
|
+
request = {
|
|
692
|
+
...base,
|
|
693
|
+
kind: "permission",
|
|
694
|
+
title: "Approve command",
|
|
695
|
+
description: reason,
|
|
696
|
+
fields: [],
|
|
697
|
+
actions: permissionActions(),
|
|
698
|
+
context: {
|
|
699
|
+
toolName: "command_execution",
|
|
700
|
+
...(command ? { command } : {}),
|
|
701
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
702
|
+
...(reason ? { summary: reason } : {}),
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const bounded = boundInteractionRequest(request);
|
|
709
|
+
if (!bounded.ok)
|
|
710
|
+
throw new InteractionRequestBoundsError(bounded.reason);
|
|
711
|
+
return { request: bounded.request, ...(turnId ? { turnId } : {}) };
|
|
712
|
+
}
|
|
713
|
+
function mcpContent(params, answers) {
|
|
714
|
+
if (params.mode === "url")
|
|
715
|
+
return {};
|
|
716
|
+
// Re-validate the native schema at resolution time. It remains untrusted
|
|
717
|
+
// provider input even though the same object was validated for rendering.
|
|
718
|
+
mcpFields(params.requestedSchema);
|
|
719
|
+
const schema = asRecord(params.requestedSchema);
|
|
720
|
+
const properties = asRecord(schema.properties);
|
|
721
|
+
const content = Object.create(null);
|
|
722
|
+
for (const [id, rawProperty] of Object.entries(properties)) {
|
|
723
|
+
const property = asRecord(rawProperty);
|
|
724
|
+
const answer = answers[id];
|
|
725
|
+
const value = answer === undefined && hasOwn(property, "default")
|
|
726
|
+
? property.default
|
|
727
|
+
: answer;
|
|
728
|
+
if (value === undefined)
|
|
729
|
+
continue;
|
|
730
|
+
if (property.type === "boolean") {
|
|
731
|
+
if (value !== "true" && value !== "false" && typeof value !== "boolean") {
|
|
732
|
+
throw new Error(`MCP field ${id} must be true or false`);
|
|
733
|
+
}
|
|
734
|
+
content[id] = typeof value === "boolean" ? value : value === "true";
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (property.type === "number" || property.type === "integer") {
|
|
738
|
+
const number = typeof value === "number"
|
|
739
|
+
? value
|
|
740
|
+
: typeof value === "string" && value.trim().length > 0
|
|
741
|
+
? Number(value)
|
|
742
|
+
: Number.NaN;
|
|
743
|
+
if (!Number.isFinite(number))
|
|
744
|
+
throw new Error(`MCP field ${id} must be a finite number`);
|
|
745
|
+
if (property.type === "integer" && !Number.isInteger(number)) {
|
|
746
|
+
throw new Error(`MCP field ${id} must be an integer`);
|
|
747
|
+
}
|
|
748
|
+
const minimum = optionalFiniteNumber(property.minimum, `MCP field ${id}.minimum`);
|
|
749
|
+
const maximum = optionalFiniteNumber(property.maximum, `MCP field ${id}.maximum`);
|
|
750
|
+
if (minimum !== undefined && number < minimum) {
|
|
751
|
+
throw new Error(`MCP field ${id} must be at least ${minimum}`);
|
|
752
|
+
}
|
|
753
|
+
if (maximum !== undefined && number > maximum) {
|
|
754
|
+
throw new Error(`MCP field ${id} must be at most ${maximum}`);
|
|
755
|
+
}
|
|
756
|
+
content[id] = number;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (property.type === "array") {
|
|
760
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
761
|
+
throw new Error(`MCP field ${id} must be a string array`);
|
|
762
|
+
}
|
|
763
|
+
if (new Set(value).size !== value.length) {
|
|
764
|
+
throw new Error(`MCP field ${id} must not contain duplicate selections`);
|
|
765
|
+
}
|
|
766
|
+
const items = asRecord(property.items);
|
|
767
|
+
const options = hasOwn(items, "anyOf")
|
|
768
|
+
? titledOptions(items.anyOf, `MCP field ${id}.items.anyOf`)
|
|
769
|
+
: enumOptions(items.enum, undefined, `MCP field ${id}.items.enum`);
|
|
770
|
+
if (value.some((entry) => !options.some((option) => option.value === entry))) {
|
|
771
|
+
throw new Error(`MCP field ${id} contains an unsupported selection`);
|
|
772
|
+
}
|
|
773
|
+
const minItems = optionalNonNegativeInteger(property.minItems, `MCP field ${id}.minItems`);
|
|
774
|
+
const maxItems = optionalNonNegativeInteger(property.maxItems, `MCP field ${id}.maxItems`);
|
|
775
|
+
if (minItems !== undefined && value.length < minItems) {
|
|
776
|
+
throw new Error(`MCP field ${id} requires at least ${minItems} selections`);
|
|
777
|
+
}
|
|
778
|
+
if (maxItems !== undefined && value.length > maxItems) {
|
|
779
|
+
throw new Error(`MCP field ${id} allows at most ${maxItems} selections`);
|
|
780
|
+
}
|
|
781
|
+
content[id] = [...value];
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (property.type === "string") {
|
|
785
|
+
if (typeof value !== "string")
|
|
786
|
+
throw new Error(`MCP field ${id} must be a string`);
|
|
787
|
+
if (hasOwn(property, "oneOf")) {
|
|
788
|
+
const options = titledOptions(property.oneOf, `MCP field ${id}.oneOf`);
|
|
789
|
+
if (!options.some((option) => option.value === value)) {
|
|
790
|
+
throw new Error(`MCP field ${id} contains an unsupported selection`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
else if (hasOwn(property, "enum")) {
|
|
794
|
+
const options = enumOptions(property.enum, property.enumNames, `MCP field ${id}.enum`);
|
|
795
|
+
if (!options.some((option) => option.value === value)) {
|
|
796
|
+
throw new Error(`MCP field ${id} contains an unsupported selection`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
validateStringValue(value, property, `MCP field ${id}`);
|
|
801
|
+
}
|
|
802
|
+
content[id] = value;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
throw new Error(`MCP field ${id} uses an unsupported schema`);
|
|
806
|
+
}
|
|
807
|
+
return content;
|
|
808
|
+
}
|
|
809
|
+
function buildNativeInteractionResponse(pending, resolution) {
|
|
810
|
+
const invalid = validateInteractionResolution(pending.request, resolution);
|
|
811
|
+
if (invalid)
|
|
812
|
+
throw new Error(invalid);
|
|
813
|
+
const answers = resolution.answers ?? {};
|
|
814
|
+
switch (pending.method) {
|
|
815
|
+
case "item/tool/requestUserInput": {
|
|
816
|
+
const response = { answers: {} };
|
|
817
|
+
for (const [id, value] of Object.entries(answers)) {
|
|
818
|
+
response.answers[id] = { answers: Array.isArray(value) ? value : [value] };
|
|
819
|
+
}
|
|
820
|
+
return response;
|
|
821
|
+
}
|
|
822
|
+
case "mcpServer/elicitation/request": {
|
|
823
|
+
const action = resolution.actionId;
|
|
824
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") {
|
|
825
|
+
throw new Error(`unsupported MCP action: ${action}`);
|
|
826
|
+
}
|
|
827
|
+
const response = {
|
|
828
|
+
action,
|
|
829
|
+
content: action === "accept"
|
|
830
|
+
? mcpContent(pending.params, answers)
|
|
831
|
+
: null,
|
|
832
|
+
_meta: null,
|
|
833
|
+
};
|
|
834
|
+
return response;
|
|
835
|
+
}
|
|
836
|
+
case "item/permissions/requestApproval": {
|
|
837
|
+
const input = pending.params;
|
|
838
|
+
const granted = resolution.actionId === "grant_turn" || resolution.actionId === "grant_session";
|
|
839
|
+
const permissions = {};
|
|
840
|
+
if (granted && input.permissions.network)
|
|
841
|
+
permissions.network = input.permissions.network;
|
|
842
|
+
if (granted && input.permissions.fileSystem)
|
|
843
|
+
permissions.fileSystem = input.permissions.fileSystem;
|
|
844
|
+
const response = {
|
|
845
|
+
permissions,
|
|
846
|
+
scope: resolution.actionId === "grant_session" ? "session" : "turn",
|
|
847
|
+
};
|
|
848
|
+
return response;
|
|
849
|
+
}
|
|
850
|
+
case "execCommandApproval":
|
|
851
|
+
case "applyPatchApproval": {
|
|
852
|
+
const response = {
|
|
853
|
+
decision: {
|
|
854
|
+
accept: "approved",
|
|
855
|
+
accept_session: "approved_for_session",
|
|
856
|
+
decline: "denied",
|
|
857
|
+
cancel: "abort",
|
|
858
|
+
}[resolution.actionId],
|
|
859
|
+
};
|
|
860
|
+
return response;
|
|
861
|
+
}
|
|
862
|
+
case "item/commandExecution/requestApproval": {
|
|
863
|
+
const input = pending.params;
|
|
864
|
+
const selected = commandDecisionActions(input).find((entry) => entry.action.id === resolution.actionId);
|
|
865
|
+
if (!selected)
|
|
866
|
+
throw new Error(`unsupported approval action: ${resolution.actionId}`);
|
|
867
|
+
const response = { decision: selected.decision };
|
|
868
|
+
return response;
|
|
869
|
+
}
|
|
870
|
+
case "item/fileChange/requestApproval": {
|
|
871
|
+
const decision = resolution.actionId === "accept_session"
|
|
872
|
+
? "acceptForSession"
|
|
873
|
+
: resolution.actionId;
|
|
874
|
+
if (decision !== "accept" && decision !== "acceptForSession" && decision !== "decline" && decision !== "cancel") {
|
|
875
|
+
throw new Error(`unsupported approval action: ${resolution.actionId}`);
|
|
876
|
+
}
|
|
877
|
+
return { decision };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function automaticCommandDecision(params, desired) {
|
|
882
|
+
const entries = commandDecisionActions(params);
|
|
883
|
+
const simple = new Map(entries
|
|
884
|
+
.filter((entry) => typeof entry.decision === "string")
|
|
885
|
+
.map((entry) => [entry.decision, entry.decision]));
|
|
886
|
+
const preferences = desired === "acceptForSession"
|
|
887
|
+
? ["acceptForSession", "accept", "cancel", "decline"]
|
|
888
|
+
: desired === "accept"
|
|
889
|
+
? ["accept", "acceptForSession", "cancel", "decline"]
|
|
890
|
+
: desired === "decline"
|
|
891
|
+
? ["decline", "cancel"]
|
|
892
|
+
: ["cancel", "decline"];
|
|
893
|
+
for (const preference of preferences) {
|
|
894
|
+
const decision = simple.get(preference);
|
|
895
|
+
if (decision)
|
|
896
|
+
return decision;
|
|
897
|
+
}
|
|
898
|
+
throw new UnsupportedInteractionSchemaError("command approval does not advertise a compatible automatic decision");
|
|
899
|
+
}
|
|
900
|
+
function buildAutomaticResponse(method, params, decision) {
|
|
901
|
+
switch (method) {
|
|
902
|
+
case "item/tool/requestUserInput":
|
|
903
|
+
return { answers: {} };
|
|
904
|
+
case "mcpServer/elicitation/request":
|
|
905
|
+
return {
|
|
906
|
+
action: "cancel",
|
|
907
|
+
content: null,
|
|
908
|
+
_meta: null,
|
|
909
|
+
};
|
|
910
|
+
case "item/permissions/requestApproval": {
|
|
911
|
+
const input = params;
|
|
912
|
+
const permissions = {};
|
|
913
|
+
if (decision === "acceptForSession") {
|
|
914
|
+
if (input.permissions.network)
|
|
915
|
+
permissions.network = input.permissions.network;
|
|
916
|
+
if (input.permissions.fileSystem)
|
|
917
|
+
permissions.fileSystem = input.permissions.fileSystem;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
permissions,
|
|
921
|
+
scope: decision === "acceptForSession" ? "session" : "turn",
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
case "execCommandApproval":
|
|
925
|
+
case "applyPatchApproval": {
|
|
926
|
+
const response = {
|
|
927
|
+
decision: decision === "acceptForSession"
|
|
928
|
+
? "approved_for_session"
|
|
929
|
+
: decision === "accept"
|
|
930
|
+
? "approved"
|
|
931
|
+
: decision === "decline"
|
|
932
|
+
? "denied"
|
|
933
|
+
: "abort",
|
|
934
|
+
};
|
|
935
|
+
return response;
|
|
936
|
+
}
|
|
937
|
+
case "item/commandExecution/requestApproval":
|
|
938
|
+
return {
|
|
939
|
+
decision: automaticCommandDecision(params, decision),
|
|
940
|
+
};
|
|
941
|
+
case "item/fileChange/requestApproval":
|
|
942
|
+
return { decision };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const MAX_SETTLED_INTERACTIONS = 512;
|
|
11
946
|
export class CodexAppServerClient {
|
|
12
947
|
transport;
|
|
13
948
|
logger;
|
|
14
949
|
clientInfo;
|
|
15
950
|
approvalDecisionPolicy;
|
|
16
|
-
interactiveApprovals;
|
|
17
951
|
channel;
|
|
18
952
|
notificationSubscribers = new Set();
|
|
19
|
-
|
|
20
|
-
|
|
953
|
+
interactionListener = null;
|
|
954
|
+
connectionListener = null;
|
|
955
|
+
connectionState = "disconnected";
|
|
956
|
+
pendingInteractions = new Map();
|
|
957
|
+
settledInteractions = new Set();
|
|
21
958
|
initializeResponse = null;
|
|
22
|
-
constructor({ spawner, channel, logger = defaultLogger, clientInfo = DEFAULT_CLIENT_INFO, approvalDecisionPolicy = "auto-approve-session",
|
|
959
|
+
constructor({ spawner, channel, logger = defaultLogger, clientInfo = DEFAULT_CLIENT_INFO, approvalDecisionPolicy = "auto-approve-session", }) {
|
|
23
960
|
this.logger = logger;
|
|
24
961
|
this.clientInfo = clientInfo;
|
|
25
962
|
this.approvalDecisionPolicy = approvalDecisionPolicy;
|
|
26
|
-
this.interactiveApprovals = interactiveApprovals;
|
|
27
963
|
this.channel = channel ?? null;
|
|
28
964
|
this.transport = new CodexAppServerTransport({
|
|
29
965
|
...(channel ? { channel } : { spawner }),
|
|
30
966
|
logger,
|
|
31
967
|
onNotification: (method, params) => this.dispatchNotification(method, params),
|
|
32
|
-
onServerRequest: (method, params) => this.handleServerRequest(method, params),
|
|
968
|
+
onServerRequest: (method, params, requestId) => this.handleServerRequest(method, params, requestId),
|
|
969
|
+
onServerRequestResponseDelivery: (requestId, result) => this.handleServerRequestResponseDelivery(requestId, result),
|
|
33
970
|
});
|
|
34
971
|
this.transport.on("exit", () => {
|
|
35
972
|
this.initializeResponse = null;
|
|
973
|
+
this.cancelPendingInteractions("app_server_disconnected");
|
|
974
|
+
this.setConnectionState("disconnected");
|
|
36
975
|
});
|
|
37
976
|
}
|
|
38
977
|
/**
|
|
@@ -55,6 +994,7 @@ export class CodexAppServerClient {
|
|
|
55
994
|
capabilities: { experimentalApi: true },
|
|
56
995
|
});
|
|
57
996
|
this.initializeResponse = response;
|
|
997
|
+
this.setConnectionState("connected");
|
|
58
998
|
return response;
|
|
59
999
|
}
|
|
60
1000
|
async getAuthStatus(params = {}) {
|
|
@@ -148,6 +1088,20 @@ export class CodexAppServerClient {
|
|
|
148
1088
|
await this.transport.stop();
|
|
149
1089
|
}
|
|
150
1090
|
dispatchNotification(method, params) {
|
|
1091
|
+
if (method === "serverRequest/resolved") {
|
|
1092
|
+
const p = asRecord(params);
|
|
1093
|
+
const nativeRequestId = p?.requestId;
|
|
1094
|
+
if (typeof nativeRequestId === "string" || typeof nativeRequestId === "number") {
|
|
1095
|
+
this.resolveByProvider(nativeRequestId, stringValue(p?.threadId));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
else if (method === "turn/completed") {
|
|
1099
|
+
const p = asRecord(params);
|
|
1100
|
+
const turn = asRecord(p?.turn);
|
|
1101
|
+
const turnId = stringValue(turn?.id) ?? stringValue(p?.turnId);
|
|
1102
|
+
if (turnId)
|
|
1103
|
+
this.cancelPendingInteractions("turn_completed", turnId);
|
|
1104
|
+
}
|
|
151
1105
|
for (const listener of this.notificationSubscribers) {
|
|
152
1106
|
try {
|
|
153
1107
|
listener(method, params);
|
|
@@ -161,19 +1115,16 @@ export class CodexAppServerClient {
|
|
|
161
1115
|
}
|
|
162
1116
|
}
|
|
163
1117
|
}
|
|
164
|
-
async handleServerRequest(method, params) {
|
|
1118
|
+
async handleServerRequest(method, params, requestId) {
|
|
165
1119
|
switch (method) {
|
|
166
1120
|
case "item/commandExecution/requestApproval":
|
|
167
1121
|
case "execCommandApproval":
|
|
168
|
-
return this.handleApproval("exec", params);
|
|
169
1122
|
case "item/fileChange/requestApproval":
|
|
170
1123
|
case "applyPatchApproval":
|
|
171
|
-
return this.handleApproval("patch", params);
|
|
172
1124
|
case "item/tool/requestUserInput":
|
|
173
1125
|
case "mcpServer/elicitation/request":
|
|
174
|
-
return this.respondNotSupported(method);
|
|
175
1126
|
case "item/permissions/requestApproval":
|
|
176
|
-
return
|
|
1127
|
+
return this.handleInteraction(method, params, requestId);
|
|
177
1128
|
case "account/chatgptAuthTokens/refresh":
|
|
178
1129
|
// Codex asks the host to refresh tokens; without an attached UI the
|
|
179
1130
|
// best we can do is decline, surfacing the auth error to the caller
|
|
@@ -188,66 +1139,96 @@ export class CodexAppServerClient {
|
|
|
188
1139
|
}
|
|
189
1140
|
}
|
|
190
1141
|
/**
|
|
191
|
-
* Register the
|
|
192
|
-
* the
|
|
193
|
-
*
|
|
1142
|
+
* Register the provider-neutral interaction listener. A request is inserted
|
|
1143
|
+
* into the pending map before the listener is invoked, so a synchronous
|
|
1144
|
+
* resolver still wins correctly.
|
|
194
1145
|
*/
|
|
195
|
-
|
|
196
|
-
this.
|
|
1146
|
+
setInteractionListener(listener) {
|
|
1147
|
+
this.interactionListener = listener;
|
|
197
1148
|
}
|
|
198
|
-
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1149
|
+
/** Observe the underlying app-server connection independently from any one
|
|
1150
|
+
* interaction. A disconnected client that never received the duplicate
|
|
1151
|
+
* native request still has to count as unavailable during host failover. */
|
|
1152
|
+
setConnectionListener(listener) {
|
|
1153
|
+
this.connectionListener = listener;
|
|
1154
|
+
listener?.(this.connectionState);
|
|
1155
|
+
}
|
|
1156
|
+
setConnectionState(state) {
|
|
1157
|
+
if (this.connectionState === state)
|
|
1158
|
+
return;
|
|
1159
|
+
this.connectionState = state;
|
|
1160
|
+
this.connectionListener?.(state);
|
|
1161
|
+
}
|
|
1162
|
+
resolveInteraction(interactionId, resolution) {
|
|
1163
|
+
const pending = this.pendingInteractions.get(interactionId);
|
|
204
1164
|
if (!pending) {
|
|
205
|
-
return
|
|
1165
|
+
return this.settledInteractions.has(interactionId)
|
|
1166
|
+
? { disposition: "already_resolved" }
|
|
1167
|
+
: { disposition: "not_found" };
|
|
206
1168
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1169
|
+
if (pending.submission)
|
|
1170
|
+
return { disposition: "already_resolved" };
|
|
1171
|
+
let nativeResponse;
|
|
1172
|
+
try {
|
|
1173
|
+
nativeResponse = buildNativeInteractionResponse(pending, resolution);
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
return {
|
|
1177
|
+
disposition: "invalid",
|
|
1178
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
pending.submission = {
|
|
1182
|
+
resolution: redactInteractionResolution(pending.request, resolution),
|
|
1183
|
+
};
|
|
1184
|
+
pending.resolve(nativeResponse);
|
|
1185
|
+
return { disposition: "applied" };
|
|
211
1186
|
}
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const listener = this.approvalListener;
|
|
219
|
-
if (!this.interactiveApprovals || !listener) {
|
|
220
|
-
return kind === "exec"
|
|
221
|
-
? this.respondCommandApproval(params)
|
|
222
|
-
: this.respondFileChangeApproval(params);
|
|
223
|
-
}
|
|
224
|
-
const approvalId = randomUUID();
|
|
225
|
-
const request = { approvalId, kind };
|
|
226
|
-
const p = params;
|
|
227
|
-
if (typeof p.command === "string")
|
|
228
|
-
request.command = p.command;
|
|
229
|
-
if (typeof p.cwd === "string")
|
|
230
|
-
request.cwd = p.cwd;
|
|
231
|
-
if (typeof p.diff === "string")
|
|
232
|
-
request.diff = p.diff;
|
|
1187
|
+
/** Cancel every pending request owned by this connection without replying. */
|
|
1188
|
+
cancelInteractions(reason = "client_stopped") {
|
|
1189
|
+
this.cancelPendingInteractions(reason);
|
|
1190
|
+
}
|
|
1191
|
+
async handleInteraction(method, params, nativeRequestId) {
|
|
1192
|
+
let adapted;
|
|
233
1193
|
try {
|
|
234
|
-
|
|
1194
|
+
adapted = buildCodexInteraction(method, params, nativeRequestId);
|
|
235
1195
|
}
|
|
236
1196
|
catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1197
|
+
if (error instanceof InteractionRequestBoundsError ||
|
|
1198
|
+
error instanceof UnsupportedInteractionSchemaError) {
|
|
1199
|
+
this.logger.log({
|
|
1200
|
+
event: "client.interaction_request_rejected",
|
|
1201
|
+
method,
|
|
1202
|
+
reason: error.message,
|
|
1203
|
+
});
|
|
1204
|
+
return buildAutomaticResponse(method, params, "cancel");
|
|
1205
|
+
}
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
const listener = this.interactionListener;
|
|
1209
|
+
// Provider policy decides whether an approval request exists. Once it does,
|
|
1210
|
+
// an installed generic listener must see it; only standalone/no-listener
|
|
1211
|
+
// clients use the automatic fallback.
|
|
1212
|
+
if (!listener) {
|
|
1213
|
+
return buildAutomaticResponse(method, params, this.autoApprovalDecision());
|
|
241
1214
|
}
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
1215
|
+
return new Promise((resolve) => {
|
|
1216
|
+
const pending = {
|
|
1217
|
+
method,
|
|
1218
|
+
nativeRequestId,
|
|
1219
|
+
params,
|
|
1220
|
+
request: adapted.request,
|
|
1221
|
+
...(adapted.turnId ? { turnId: adapted.turnId } : {}),
|
|
1222
|
+
resolve,
|
|
1223
|
+
};
|
|
1224
|
+
// Register before notifying: the listener is allowed to answer inline.
|
|
1225
|
+
this.pendingInteractions.set(adapted.request.interactionId, pending);
|
|
1226
|
+
this.emitInteraction({
|
|
1227
|
+
type: "requested",
|
|
1228
|
+
request: adapted.request,
|
|
1229
|
+
...(adapted.turnId ? { turnId: adapted.turnId } : {}),
|
|
1230
|
+
});
|
|
249
1231
|
});
|
|
250
|
-
return { decision };
|
|
251
1232
|
}
|
|
252
1233
|
autoApprovalDecision() {
|
|
253
1234
|
switch (this.approvalDecisionPolicy) {
|
|
@@ -260,45 +1241,94 @@ export class CodexAppServerClient {
|
|
|
260
1241
|
return "cancel";
|
|
261
1242
|
}
|
|
262
1243
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1244
|
+
resolveByProvider(nativeRequestId, threadId) {
|
|
1245
|
+
for (const [interactionId, pending] of this.pendingInteractions) {
|
|
1246
|
+
const pendingParams = asRecord(pending.params);
|
|
1247
|
+
const pendingThreadId = stringValue(pendingParams?.threadId) ??
|
|
1248
|
+
stringValue(pendingParams?.conversationId);
|
|
1249
|
+
if (requestIdKey(pending.nativeRequestId) !== requestIdKey(nativeRequestId) ||
|
|
1250
|
+
(threadId && pendingThreadId && threadId !== pendingThreadId)) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
this.pendingInteractions.delete(interactionId);
|
|
1254
|
+
this.rememberSettled(interactionId);
|
|
1255
|
+
if (pending.submission) {
|
|
1256
|
+
this.emitInteraction({
|
|
1257
|
+
type: "resolved",
|
|
1258
|
+
interactionId,
|
|
1259
|
+
resolution: pending.submission.resolution,
|
|
1260
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
// This connection never submitted a response, so another app-server
|
|
1265
|
+
// client resolved the request. Do not reply to the stale native request.
|
|
1266
|
+
pending.resolve(NO_SERVER_RESPONSE);
|
|
1267
|
+
this.emitInteraction({
|
|
1268
|
+
type: "cancelled",
|
|
1269
|
+
interactionId,
|
|
1270
|
+
reason: "resolved_by_another_client",
|
|
1271
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return;
|
|
272
1275
|
}
|
|
273
1276
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1277
|
+
handleServerRequestResponseDelivery(nativeRequestId, result) {
|
|
1278
|
+
for (const [interactionId, pending] of this.pendingInteractions) {
|
|
1279
|
+
if (requestIdKey(pending.nativeRequestId) !== requestIdKey(nativeRequestId))
|
|
1280
|
+
continue;
|
|
1281
|
+
if (!pending.submission)
|
|
1282
|
+
return;
|
|
1283
|
+
if (result.delivered)
|
|
1284
|
+
return;
|
|
1285
|
+
this.pendingInteractions.delete(interactionId);
|
|
1286
|
+
this.rememberSettled(interactionId);
|
|
1287
|
+
this.emitInteraction({
|
|
1288
|
+
type: "cancelled",
|
|
1289
|
+
interactionId,
|
|
1290
|
+
reason: "response_write_failed",
|
|
1291
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
1292
|
+
});
|
|
1293
|
+
return;
|
|
283
1294
|
}
|
|
284
1295
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1296
|
+
cancelPendingInteractions(reason, turnId) {
|
|
1297
|
+
for (const [interactionId, pending] of [...this.pendingInteractions]) {
|
|
1298
|
+
// Some provider requests omit turnId (notably MCP URL/form variants).
|
|
1299
|
+
// Codex has one active Turn per thread, so an unknown correlation must be
|
|
1300
|
+
// cancelled by its completion instead of leaking forever.
|
|
1301
|
+
if (turnId && pending.turnId && pending.turnId !== turnId)
|
|
1302
|
+
continue;
|
|
1303
|
+
this.pendingInteractions.delete(interactionId);
|
|
1304
|
+
this.rememberSettled(interactionId);
|
|
1305
|
+
pending.resolve(NO_SERVER_RESPONSE);
|
|
1306
|
+
this.emitInteraction({
|
|
1307
|
+
type: "cancelled",
|
|
1308
|
+
interactionId,
|
|
1309
|
+
reason,
|
|
1310
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
1311
|
+
});
|
|
294
1312
|
}
|
|
295
1313
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
event
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1314
|
+
emitInteraction(event) {
|
|
1315
|
+
try {
|
|
1316
|
+
this.interactionListener?.(event);
|
|
1317
|
+
}
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
this.logger.log({
|
|
1320
|
+
event: "client.interaction_listener_failed",
|
|
1321
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
rememberSettled(interactionId) {
|
|
1326
|
+
this.settledInteractions.add(interactionId);
|
|
1327
|
+
if (this.settledInteractions.size <= MAX_SETTLED_INTERACTIONS)
|
|
1328
|
+
return;
|
|
1329
|
+
const oldest = this.settledInteractions.values().next().value;
|
|
1330
|
+
if (oldest)
|
|
1331
|
+
this.settledInteractions.delete(oldest);
|
|
302
1332
|
}
|
|
303
1333
|
}
|
|
304
1334
|
/**
|
|
@@ -330,6 +1360,11 @@ export function buildSandboxPolicy(options) {
|
|
|
330
1360
|
export function buildTextUserInput(message) {
|
|
331
1361
|
return [{ type: "text", text: message, text_elements: [] }];
|
|
332
1362
|
}
|
|
1363
|
+
export function buildRuntimeUserInput(input) {
|
|
1364
|
+
return input.content.map((part) => part.type === "text"
|
|
1365
|
+
? { type: "text", text: part.text, text_elements: [] }
|
|
1366
|
+
: { type: "localImage", path: part.path });
|
|
1367
|
+
}
|
|
333
1368
|
const defaultLogger = {
|
|
334
1369
|
log(entry) {
|
|
335
1370
|
console.log(JSON.stringify({
|