openmausbot 0.1.78 → 0.1.80
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/assets/index-Cu8BkIfo.js +305 -0
- package/dist/assets/{index-BqDJqf2C.js → index-D6RZKWks.js} +1 -1
- package/dist/assets/index-tfpeJAIG.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +11 -5
- package/dist-server/drivers/agents-proxy.js +21 -6
- package/dist-server/enterprise/server/index.js +0 -1
- package/dist-server/index.js +15942 -6859
- package/dist-server/local-computer.js +0 -1
- package/dist-server/mcp-server.js +8 -1
- package/dist-server/openmausbot.js +9303 -786
- package/dist-server/pair-cli.js +9303 -786
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/box.js +98 -41
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/browser-runtime.js +11 -3
- package/dist-server/server/browser-tool-shape.js +72 -0
- package/dist-server/server/claude-accounts.js +1 -0
- package/dist-server/server/cli-setup.js +1 -1
- package/dist-server/server/composio.js +18 -0
- package/dist-server/server/config.js +16 -4
- package/dist-server/server/container-computer.js +0 -12
- package/dist-server/server/drivers/acp/core.js +4 -19
- package/dist-server/server/drivers/agents-proxy.js +27 -2
- package/dist-server/server/drivers/boxagent.js +48 -4
- package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
- package/dist-server/server/drivers/chat-tool-approval.js +42 -0
- package/dist-server/server/drivers/claude.js +86 -38
- package/dist-server/server/drivers/codex.js +225 -75
- package/dist-server/server/drivers/grok.js +4 -0
- package/dist-server/server/drivers/minimax.js +11 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
- package/dist-server/server/drivers/openai-chat.js +332 -88
- package/dist-server/server/drivers/openai-compat.js +4 -0
- package/dist-server/server/drivers/pi.js +1 -9
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +267 -50
- package/dist-server/server/managed-desktop.js +205 -0
- package/dist-server/server/model-context-window.js +20 -0
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/server/redact.js +1 -0
- package/dist-server/server/room-handoffs.js +57 -9
- package/dist-server/server/store.js +13 -1
- package/dist-server/server/tts/chatterbox.js +83 -0
- package/dist-server/server/tts/index.js +45 -12
- package/dist-server/server/workspace.js +12 -2
- package/dist-server/shared/ask-question.js +28 -0
- package/dist-server/shared/computer-contention.js +17 -0
- package/dist-server/vps-container-mcp.js +11 -5
- package/enterprise/server/index.js +0 -1
- package/package.json +1 -1
- package/dist/assets/index-DjwAroIZ.js +0 -305
- package/dist/assets/index-Pbb6Ao0s.css +0 -1
- package/dist-server/computer-proxy.js +0 -1196
- package/dist-server/server/computer-proxy.js +0 -1070
- package/dist-server/server/remote-computer.js +0 -170
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Per-turn MCP transport for the shared Chat Completions runtime. Approval is
|
|
2
|
+
// owned by the caller; only registered, schema-validated calls reach this file.
|
|
3
|
+
import { Ajv } from "ajv";
|
|
4
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
|
+
import formats from "ajv-formats";
|
|
6
|
+
import { killCliTree, spawnCli } from "../procs.js";
|
|
7
|
+
/** The transport cannot safely continue this turn. A dispatched operation may
|
|
8
|
+
* already have taken effect, so callers must not retry it through a new round. */
|
|
9
|
+
export class ChatToolSessionError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const STARTUP_MS = 8_000;
|
|
12
|
+
const CALL_MS = 10 * 60_000;
|
|
13
|
+
const FRAME_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const OUTPUT_BYTES = 50 * 1024;
|
|
15
|
+
const TOOL_COUNT = 128;
|
|
16
|
+
const MAX_PAGES = 100;
|
|
17
|
+
const SCHEMA_BYTES = 64 * 1024;
|
|
18
|
+
const CATALOG_BYTES = 1024 * 1024;
|
|
19
|
+
const MAX_FRAMES = 10_000;
|
|
20
|
+
function object(value) {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function aborted() { return new Error("MCP operation cancelled"); }
|
|
24
|
+
class ChatMcpClient {
|
|
25
|
+
child;
|
|
26
|
+
buffer = "";
|
|
27
|
+
nextId = 1;
|
|
28
|
+
frames = 0;
|
|
29
|
+
closed = false;
|
|
30
|
+
closing;
|
|
31
|
+
pending = new Map();
|
|
32
|
+
constructor(server) {
|
|
33
|
+
try {
|
|
34
|
+
this.child = spawnCli(server.command, server.args, {
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...server.env },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error("MCP server could not start; check its command and installation");
|
|
40
|
+
}
|
|
41
|
+
this.child.stdout.setEncoding("utf8");
|
|
42
|
+
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
43
|
+
// Stderr may contain credentials; drain without recording or returning it.
|
|
44
|
+
this.child.stderr.on("data", () => { });
|
|
45
|
+
this.child.on("error", () => this.fail(new Error("MCP server could not start; check its command and installation")));
|
|
46
|
+
this.child.on("exit", () => this.fail(new Error("MCP server exited before the session closed")));
|
|
47
|
+
this.child.stdin.on("error", () => this.fail(new Error("MCP server input closed")));
|
|
48
|
+
}
|
|
49
|
+
fail(error) {
|
|
50
|
+
if (this.closed)
|
|
51
|
+
return;
|
|
52
|
+
this.closed = true;
|
|
53
|
+
for (const entry of this.pending.values())
|
|
54
|
+
entry.reject(error);
|
|
55
|
+
this.pending.clear();
|
|
56
|
+
// Capture rejection here; mount/execute finally await close and surface it.
|
|
57
|
+
void this.close().catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
if (this.closing)
|
|
61
|
+
return this.closing;
|
|
62
|
+
this.closed = true;
|
|
63
|
+
for (const entry of this.pending.values())
|
|
64
|
+
entry.reject(new Error("MCP session closed"));
|
|
65
|
+
this.pending.clear();
|
|
66
|
+
this.buffer = "";
|
|
67
|
+
this.closing = killCliTree(this.child, 500).then((stopped) => {
|
|
68
|
+
if (!stopped)
|
|
69
|
+
throw new Error("MCP server shutdown could not be confirmed; execution outcome may be uncertain");
|
|
70
|
+
});
|
|
71
|
+
return this.closing;
|
|
72
|
+
}
|
|
73
|
+
write(frame) {
|
|
74
|
+
if (this.closed || !this.child.stdin.writable || this.child.stdin.destroyed)
|
|
75
|
+
throw new Error("MCP session closed");
|
|
76
|
+
const encoded = JSON.stringify(frame) + "\n";
|
|
77
|
+
if (Buffer.byteLength(encoded) > FRAME_BYTES)
|
|
78
|
+
throw new Error("MCP request exceeds the frame limit");
|
|
79
|
+
this.child.stdin.write(encoded);
|
|
80
|
+
}
|
|
81
|
+
onData(chunk) {
|
|
82
|
+
if (this.closed)
|
|
83
|
+
return;
|
|
84
|
+
this.buffer += chunk;
|
|
85
|
+
let newline;
|
|
86
|
+
while ((newline = this.buffer.indexOf("\n")) !== -1) {
|
|
87
|
+
const line = this.buffer.slice(0, newline);
|
|
88
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
89
|
+
if (++this.frames > MAX_FRAMES)
|
|
90
|
+
return this.fail(new Error("MCP session exceeded the response frame count limit"));
|
|
91
|
+
if (Buffer.byteLength(line) > FRAME_BYTES)
|
|
92
|
+
return this.fail(new Error("MCP response exceeds the frame limit"));
|
|
93
|
+
if (!line.trim())
|
|
94
|
+
continue;
|
|
95
|
+
let message;
|
|
96
|
+
try {
|
|
97
|
+
message = JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return this.fail(new Error("MCP server returned invalid JSON"));
|
|
101
|
+
}
|
|
102
|
+
if (!object(message) || message.jsonrpc !== "2.0")
|
|
103
|
+
return this.fail(new Error("MCP server returned an invalid RPC envelope"));
|
|
104
|
+
if (typeof message.method === "string") {
|
|
105
|
+
if (typeof message.id === "number" || typeof message.id === "string") {
|
|
106
|
+
try {
|
|
107
|
+
this.write({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Client method not supported" } });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return this.fail(new Error("MCP server input closed"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (typeof message.id !== "number")
|
|
116
|
+
return this.fail(new Error("MCP server returned an invalid response ID"));
|
|
117
|
+
const entry = this.pending.get(message.id);
|
|
118
|
+
if (!entry)
|
|
119
|
+
continue;
|
|
120
|
+
this.pending.delete(message.id);
|
|
121
|
+
if ("error" in message)
|
|
122
|
+
entry.reject(new Error("MCP request failed; inspect the configured integration"));
|
|
123
|
+
else if (!("result" in message))
|
|
124
|
+
entry.reject(new Error("MCP response has no result"));
|
|
125
|
+
else
|
|
126
|
+
entry.resolve(message.result);
|
|
127
|
+
}
|
|
128
|
+
if (Buffer.byteLength(this.buffer) > FRAME_BYTES)
|
|
129
|
+
this.fail(new Error("MCP response exceeds the frame limit"));
|
|
130
|
+
}
|
|
131
|
+
async call(method, params, signal, timeout) {
|
|
132
|
+
if (signal.aborted) {
|
|
133
|
+
await this.close();
|
|
134
|
+
throw aborted();
|
|
135
|
+
}
|
|
136
|
+
if (this.closed)
|
|
137
|
+
throw new Error("MCP session closed");
|
|
138
|
+
const id = this.nextId++;
|
|
139
|
+
try {
|
|
140
|
+
return await new Promise((resolve, reject) => {
|
|
141
|
+
const finish = (error, value) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
signal.removeEventListener("abort", cancel);
|
|
144
|
+
this.pending.delete(id);
|
|
145
|
+
if (error)
|
|
146
|
+
reject(error);
|
|
147
|
+
else
|
|
148
|
+
resolve(value);
|
|
149
|
+
};
|
|
150
|
+
const cancel = () => {
|
|
151
|
+
try {
|
|
152
|
+
this.write({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: id, reason: "Cancelled" } });
|
|
153
|
+
}
|
|
154
|
+
catch { /* cleanup owns transport failure */ }
|
|
155
|
+
finish(aborted());
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => finish(new Error("MCP request timed out; execution outcome may be uncertain")), timeout);
|
|
158
|
+
timer.unref();
|
|
159
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
160
|
+
this.pending.set(id, { resolve: (value) => finish(undefined, value), reject: (error) => finish(error) });
|
|
161
|
+
try {
|
|
162
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
finish(new Error("MCP request could not be sent"));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
await this.close();
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async tools(signal) {
|
|
175
|
+
const deadline = Date.now() + STARTUP_MS;
|
|
176
|
+
const remaining = () => {
|
|
177
|
+
if (Date.now() >= deadline)
|
|
178
|
+
throw new Error("MCP startup timed out");
|
|
179
|
+
return deadline - Date.now();
|
|
180
|
+
};
|
|
181
|
+
const initialized = await this.call("initialize", {
|
|
182
|
+
protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "openmausbot-chat", version: "1" },
|
|
183
|
+
}, signal, remaining());
|
|
184
|
+
if (!object(initialized))
|
|
185
|
+
throw new Error("MCP initialization returned an invalid result");
|
|
186
|
+
if (signal.aborted)
|
|
187
|
+
throw aborted();
|
|
188
|
+
this.write({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
189
|
+
const tools = [];
|
|
190
|
+
const cursors = new Set();
|
|
191
|
+
let cursor;
|
|
192
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
193
|
+
const result = await this.call("tools/list", cursor ? { cursor } : {}, signal, remaining());
|
|
194
|
+
if (!object(result) || !Array.isArray(result.tools))
|
|
195
|
+
throw new Error("MCP tools/list returned an invalid result");
|
|
196
|
+
tools.push(...result.tools);
|
|
197
|
+
if (tools.length > TOOL_COUNT)
|
|
198
|
+
throw new Error("MCP tool count exceeds the 128-tool limit");
|
|
199
|
+
if (result.nextCursor === undefined)
|
|
200
|
+
return tools;
|
|
201
|
+
if (typeof result.nextCursor !== "string" || !result.nextCursor || cursors.has(result.nextCursor))
|
|
202
|
+
throw new Error("MCP tools/list returned an invalid pagination cursor");
|
|
203
|
+
cursors.add(result.nextCursor);
|
|
204
|
+
cursor = result.nextCursor;
|
|
205
|
+
}
|
|
206
|
+
throw new Error("MCP tools/list exceeded the pagination limit");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// A schema is a contract, so conversion that drops constraints is not safe.
|
|
210
|
+
// Ajv validates the same schema sent to the provider without coercing values,
|
|
211
|
+
// applying defaults, removing fields, or fetching external references.
|
|
212
|
+
const validatorOptions = {
|
|
213
|
+
strict: true, allErrors: false, coerceTypes: false, useDefaults: false,
|
|
214
|
+
removeAdditional: false, validateFormats: true, ownProperties: true, logger: false,
|
|
215
|
+
// These are style diagnostics, not unsupported validation keywords. Valid
|
|
216
|
+
// schemas may require undeclared names, use untyped composition branches,
|
|
217
|
+
// or describe an open tuple. Ajv still enforces every constraint.
|
|
218
|
+
strictRequired: false, strictTypes: false, strictTuples: false,
|
|
219
|
+
};
|
|
220
|
+
function compileSchema(schema) {
|
|
221
|
+
const dialect = schema.$schema;
|
|
222
|
+
if (dialect !== undefined && dialect !== "http://json-schema.org/draft-07/schema#" && dialect !== "https://json-schema.org/draft/2020-12/schema") {
|
|
223
|
+
throw new Error("MCP tool schema uses an unsupported dialect; use JSON Schema draft-07 or 2020-12");
|
|
224
|
+
}
|
|
225
|
+
// One compiler per schema also prevents external IDs from resolving against
|
|
226
|
+
// unrelated tools or retaining schemas after the turn has closed.
|
|
227
|
+
const compiler = dialect === "https://json-schema.org/draft/2020-12/schema"
|
|
228
|
+
? new Ajv2020(validatorOptions) : new Ajv(validatorOptions);
|
|
229
|
+
// ajv-formats is CommonJS and exports the plugin as both module.exports
|
|
230
|
+
// and .default; the latter also matches its NodeNext declaration.
|
|
231
|
+
formats.default(compiler);
|
|
232
|
+
try {
|
|
233
|
+
return compiler.compile(schema);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw new Error("MCP tool schema could not be validated; check its constraints, formats, and references");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function boundedText(value) {
|
|
240
|
+
const bytes = Buffer.from(value);
|
|
241
|
+
if (bytes.length <= OUTPUT_BYTES)
|
|
242
|
+
return value;
|
|
243
|
+
let end = OUTPUT_BYTES;
|
|
244
|
+
while ((bytes[end] & 0xc0) === 0x80)
|
|
245
|
+
end -= 1;
|
|
246
|
+
return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
|
|
247
|
+
}
|
|
248
|
+
export async function mountChatTools(integrations, signal) {
|
|
249
|
+
const servers = [];
|
|
250
|
+
if (integrations?.agents)
|
|
251
|
+
servers.push(["agents", integrations.agents]);
|
|
252
|
+
if (integrations?.composio)
|
|
253
|
+
servers.push(["composio", integrations.composio]);
|
|
254
|
+
servers.push(...Object.entries(integrations?.custom ?? {}));
|
|
255
|
+
if (servers.length > 32)
|
|
256
|
+
throw new Error("MCP server count exceeds the 32-server limit");
|
|
257
|
+
const clients = [];
|
|
258
|
+
let closed = false;
|
|
259
|
+
let closing;
|
|
260
|
+
const close = () => {
|
|
261
|
+
if (closing)
|
|
262
|
+
return closing;
|
|
263
|
+
closed = true;
|
|
264
|
+
signal.removeEventListener("abort", cancel);
|
|
265
|
+
closing = Promise.allSettled(clients.map((client) => client.close())).then((results) => {
|
|
266
|
+
if (results.some((result) => result.status === "rejected"))
|
|
267
|
+
throw new ChatToolSessionError("MCP server shutdown could not be confirmed; execution outcome may be uncertain");
|
|
268
|
+
});
|
|
269
|
+
return closing;
|
|
270
|
+
};
|
|
271
|
+
const cancel = () => { void close().catch(() => { }); };
|
|
272
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
273
|
+
const definitions = [];
|
|
274
|
+
const registered = new Map();
|
|
275
|
+
try {
|
|
276
|
+
if (signal.aborted)
|
|
277
|
+
throw aborted();
|
|
278
|
+
// Start independent servers concurrently; consume results in config order
|
|
279
|
+
// so names and collision suffixes remain stable across startup timings.
|
|
280
|
+
const mounts = await Promise.allSettled(servers.map(async ([name, descriptor]) => {
|
|
281
|
+
if (signal.aborted || closed)
|
|
282
|
+
throw aborted();
|
|
283
|
+
const client = new ChatMcpClient(descriptor);
|
|
284
|
+
clients.push(client);
|
|
285
|
+
return { name, client, tools: await client.tools(signal) };
|
|
286
|
+
}));
|
|
287
|
+
for (const mount of mounts) {
|
|
288
|
+
if (mount.status === "rejected")
|
|
289
|
+
throw mount.reason;
|
|
290
|
+
const { name: server, client, tools } = mount.value;
|
|
291
|
+
const originalNames = new Set();
|
|
292
|
+
for (const tool of tools) {
|
|
293
|
+
if (!object(tool) || typeof tool.name !== "string" || !tool.name.trim() || originalNames.has(tool.name))
|
|
294
|
+
throw new Error("MCP server advertised an invalid or duplicate tool name");
|
|
295
|
+
originalNames.add(tool.name);
|
|
296
|
+
if (definitions.length >= TOOL_COUNT)
|
|
297
|
+
throw new Error("MCP tool count exceeds the 128-tool limit");
|
|
298
|
+
if (!object(tool.inputSchema) || tool.inputSchema.type !== "object")
|
|
299
|
+
throw new Error("MCP tools require an object input schema");
|
|
300
|
+
if (Buffer.byteLength(JSON.stringify(tool.inputSchema)) > SCHEMA_BYTES)
|
|
301
|
+
throw new Error("MCP tool schema exceeds the 64KB limit");
|
|
302
|
+
const schema = compileSchema(tool.inputSchema);
|
|
303
|
+
const base = `${server}_${tool.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "mcp_tool";
|
|
304
|
+
let name = base;
|
|
305
|
+
for (let index = 2; registered.has(name); index += 1) {
|
|
306
|
+
const suffix = `_${index}`;
|
|
307
|
+
name = base.slice(0, 64 - suffix.length) + suffix;
|
|
308
|
+
}
|
|
309
|
+
registered.set(name, { client, name: tool.name, schema });
|
|
310
|
+
definitions.push({ type: "function", function: { name, description: typeof tool.description === "string" ? tool.description : "Configured MCP tool", parameters: tool.inputSchema } });
|
|
311
|
+
if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
|
|
312
|
+
throw new Error("MCP tool catalog exceeds the 1MB limit");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (signal.aborted || closed)
|
|
316
|
+
throw aborted();
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
await close();
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
const validate = (name, args) => {
|
|
323
|
+
if (closed || signal.aborted)
|
|
324
|
+
throw new ChatToolSessionError("MCP session closed");
|
|
325
|
+
const tool = registered.get(name);
|
|
326
|
+
if (!tool)
|
|
327
|
+
throw new Error("The requested tool was not advertised for this turn");
|
|
328
|
+
if (!object(args) || !tool.schema(args))
|
|
329
|
+
throw new Error("Tool arguments do not match the advertised input schema; use its required fields and types");
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
definitions, validate, close,
|
|
333
|
+
async execute(name, args, callSignal) {
|
|
334
|
+
validate(name, args);
|
|
335
|
+
if (callSignal.aborted) {
|
|
336
|
+
await close();
|
|
337
|
+
throw aborted();
|
|
338
|
+
}
|
|
339
|
+
const tool = registered.get(name);
|
|
340
|
+
try {
|
|
341
|
+
const result = await tool.client.call("tools/call", { name: tool.name, arguments: args }, AbortSignal.any([signal, callSignal]), CALL_MS);
|
|
342
|
+
if (signal.aborted || callSignal.aborted)
|
|
343
|
+
throw aborted();
|
|
344
|
+
if (!object(result) || !Array.isArray(result.content) || (result.isError !== undefined && typeof result.isError !== "boolean"))
|
|
345
|
+
throw new Error("MCP tool returned an invalid result; execution outcome may be uncertain");
|
|
346
|
+
const parts = [];
|
|
347
|
+
let unsupported = 0;
|
|
348
|
+
for (const item of result.content) {
|
|
349
|
+
if (!object(item) || typeof item.type !== "string" || (item.type === "text" && typeof item.text !== "string"))
|
|
350
|
+
throw new Error("MCP tool returned invalid content; execution outcome may be uncertain");
|
|
351
|
+
if (item.type === "text")
|
|
352
|
+
parts.push(item.text);
|
|
353
|
+
else
|
|
354
|
+
unsupported += 1;
|
|
355
|
+
}
|
|
356
|
+
if (result.structuredContent !== undefined)
|
|
357
|
+
parts.push(JSON.stringify(result.structuredContent));
|
|
358
|
+
if (unsupported)
|
|
359
|
+
parts.unshift(`[${unsupported} unsupported MCP content item(s) omitted. The operation may have taken effect, but its full result cannot be represented; inspect its state before retrying.]`);
|
|
360
|
+
return { text: boundedText(parts.join("\n") || "(empty result)"), ok: result.isError !== true && unsupported === 0 };
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
await close();
|
|
364
|
+
throw new ChatToolSessionError(error instanceof Error ? error.message : "MCP transport failed; execution outcome may be uncertain");
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Per-turn permission gate, adapted from the approval lifecycle in #780.
|
|
2
|
+
// Register before publishing: a harness listener may answer synchronously.
|
|
3
|
+
import { newId } from "../contracts.js";
|
|
4
|
+
export function createChatToolApproval(options) {
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
let closed = false;
|
|
7
|
+
return {
|
|
8
|
+
ask(tool, summary) {
|
|
9
|
+
if (closed || options.signal.aborted)
|
|
10
|
+
return Promise.resolve(false);
|
|
11
|
+
const ask = { id: newId(), tool, summary };
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const finish = (allowed, source) => {
|
|
14
|
+
if (!pending.delete(ask.id))
|
|
15
|
+
return;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
options.signal.removeEventListener("abort", abort);
|
|
18
|
+
options.resolved(ask, allowed, source);
|
|
19
|
+
resolve(allowed);
|
|
20
|
+
};
|
|
21
|
+
const abort = () => finish(false, "system");
|
|
22
|
+
const timer = setTimeout(() => finish(false, "timeout"), options.timeoutMs ?? 15 * 60_000);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
pending.set(ask.id, finish);
|
|
25
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
26
|
+
options.open(ask);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
answer(id, behavior) {
|
|
30
|
+
const finish = pending.get(id);
|
|
31
|
+
if (!finish || options.signal.aborted || behavior === "answer")
|
|
32
|
+
return "unavailable";
|
|
33
|
+
finish(behavior === "allow", "user");
|
|
34
|
+
return behavior === "allow" ? "allowed-once" : "rejected";
|
|
35
|
+
},
|
|
36
|
+
close() {
|
|
37
|
+
closed = true;
|
|
38
|
+
for (const finish of pending.values())
|
|
39
|
+
finish(false, "system");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|