@wrongstack/acp 0.293.0 → 0.295.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/dist/agent/protocol-contract.d.ts +210 -0
- package/dist/agent/protocol-contract.d.ts.map +1 -0
- package/dist/agent/protocol-handler.d.ts +15 -189
- package/dist/agent/protocol-handler.d.ts.map +1 -1
- package/dist/agent/server-agent-turn.d.ts +89 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/session-store.d.ts.map +1 -1
- package/dist/agent/stdio-transport.d.ts +30 -1
- package/dist/agent/stdio-transport.d.ts.map +1 -1
- package/dist/agent/tools-registry.d.ts +1 -1
- package/dist/agent/tools-registry.d.ts.map +1 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +37 -0
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +189 -32
- package/dist/agent.js.map +4 -4
- package/dist/client/acp-session.d.ts +13 -1
- package/dist/client/acp-session.d.ts.map +1 -1
- package/dist/client/file-server.d.ts +19 -0
- package/dist/client/file-server.d.ts.map +1 -1
- package/dist/client/index.d.ts +11 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/terminal-server.d.ts +5 -0
- package/dist/client/terminal-server.d.ts.map +1 -1
- package/dist/client/tool-translator.d.ts +1 -1
- package/dist/client/tool-translator.d.ts.map +1 -1
- package/dist/client/trust-boundary-permission.d.ts +31 -0
- package/dist/client/trust-boundary-permission.d.ts.map +1 -0
- package/dist/client/websocket-transport.d.ts +6 -0
- package/dist/client/websocket-transport.d.ts.map +1 -1
- package/dist/client.js +459 -243
- package/dist/client.js.map +4 -4
- package/dist/index.d.ts +29 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2116 -1802
- package/dist/index.js.map +4 -4
- package/dist/integration/acp-bench.d.ts +32 -0
- package/dist/integration/acp-bench.d.ts.map +1 -1
- package/dist/integration/acp-subagent-runner.d.ts +3 -3
- package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
- package/dist/integration/ensemble-runner.d.ts +22 -1
- package/dist/integration/ensemble-runner.d.ts.map +1 -1
- package/dist/legacy.d.ts +8 -0
- package/dist/legacy.d.ts.map +1 -0
- package/dist/legacy.js +6 -0
- package/dist/legacy.js.map +7 -0
- package/dist/registry/acp-registry-fetch.d.ts +1 -1
- package/dist/registry/acp-registry-fetch.d.ts.map +1 -1
- package/dist/registry/ensemble-registry.d.ts +22 -0
- package/dist/registry/ensemble-registry.d.ts.map +1 -1
- package/dist/sdk.d.ts +10 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +22 -0
- package/dist/sdk.js.map +3 -3
- package/dist/v1.d.ts +3 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +12 -0
- package/dist/v1.js.map +7 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/wrongstack-acp-agent.js +121 -24
- package/dist/wrongstack-acp-agent.js.map +4 -4
- package/package.json +10 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/agent/stdio-transport.ts
|
|
2
|
-
import { expectDefined, writeErr } from "@wrongstack/core";
|
|
2
|
+
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
3
|
+
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
3
4
|
|
|
4
5
|
// src/win32-cmd.ts
|
|
5
6
|
var WIN32_CMD_META = /[&|<>"\r\n\0]/;
|
|
@@ -26,6 +27,11 @@ function quoteWin32CmdArg(arg) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// src/agent/stdio-transport.ts
|
|
30
|
+
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
31
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
32
|
+
function positiveLimit(value, fallback) {
|
|
33
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
34
|
+
}
|
|
29
35
|
var ClientTransport = class {
|
|
30
36
|
child = null;
|
|
31
37
|
buffer = "";
|
|
@@ -34,17 +40,21 @@ var ClientTransport = class {
|
|
|
34
40
|
resolveRead = null;
|
|
35
41
|
messageQueue = [];
|
|
36
42
|
opts;
|
|
43
|
+
maxFrameChars;
|
|
44
|
+
maxQueuedMessages;
|
|
37
45
|
constructor(options) {
|
|
38
46
|
this.opts = {
|
|
39
47
|
handshakeTimeoutMs: 3e4,
|
|
40
48
|
...options
|
|
41
49
|
};
|
|
50
|
+
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
51
|
+
this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
42
52
|
}
|
|
43
53
|
async start() {
|
|
44
54
|
if (this.child) return;
|
|
45
55
|
const [{ spawn: spawn2 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
46
56
|
import("node:child_process"),
|
|
47
|
-
import("@wrongstack/core"),
|
|
57
|
+
import("@wrongstack/core/utils"),
|
|
48
58
|
import("node:os")
|
|
49
59
|
]);
|
|
50
60
|
return new Promise((resolve3, reject) => {
|
|
@@ -57,13 +67,13 @@ var ClientTransport = class {
|
|
|
57
67
|
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
58
68
|
try {
|
|
59
69
|
const childArgs = this.opts.args ?? [];
|
|
60
|
-
const
|
|
61
|
-
this.child = spawn2(
|
|
70
|
+
const invocation = spawnInvocation(this.opts.command, childArgs, process.platform);
|
|
71
|
+
this.child = spawn2(invocation.command, invocation.args, {
|
|
62
72
|
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
63
73
|
cwd: spawnCwd,
|
|
64
74
|
stdio: ["pipe", "pipe", "pipe"],
|
|
65
75
|
windowsHide: true,
|
|
66
|
-
...
|
|
76
|
+
...verbatimOptions(invocation)
|
|
67
77
|
});
|
|
68
78
|
} catch (err) {
|
|
69
79
|
clearTimeout(timeout);
|
|
@@ -128,7 +138,8 @@ var ClientTransport = class {
|
|
|
128
138
|
});
|
|
129
139
|
}
|
|
130
140
|
read() {
|
|
131
|
-
if (this.messageQueue.length > 0)
|
|
141
|
+
if (this.messageQueue.length > 0)
|
|
142
|
+
return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
132
143
|
if (this.closed) return Promise.resolve(null);
|
|
133
144
|
return new Promise((resolve3) => {
|
|
134
145
|
this.resolveRead = resolve3;
|
|
@@ -139,20 +150,35 @@ var ClientTransport = class {
|
|
|
139
150
|
return () => this.handlers.delete(handler);
|
|
140
151
|
}
|
|
141
152
|
stop() {
|
|
142
|
-
if (!this.child) return;
|
|
143
153
|
this.closed = true;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
this.resolveRead?.(null);
|
|
155
|
+
this.resolveRead = null;
|
|
156
|
+
this.buffer = "";
|
|
157
|
+
this.messageQueue.length = 0;
|
|
158
|
+
this.handlers.clear();
|
|
159
|
+
const child = this.child;
|
|
160
|
+
if (!child) return;
|
|
161
|
+
treeKill(child);
|
|
148
162
|
this.child = null;
|
|
149
163
|
}
|
|
150
164
|
onChildData(chunk) {
|
|
151
165
|
this.buffer += chunk;
|
|
152
166
|
const lines = this.buffer.split("\n");
|
|
153
167
|
this.buffer = lines.pop() ?? "";
|
|
168
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
169
|
+
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
170
|
+
`);
|
|
171
|
+
this.stop();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
154
174
|
for (const raw of lines) {
|
|
155
175
|
if (!raw.trim()) continue;
|
|
176
|
+
if (raw.length > this.maxFrameChars) {
|
|
177
|
+
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
178
|
+
`);
|
|
179
|
+
this.stop();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
156
182
|
try {
|
|
157
183
|
this.dispatch(JSON.parse(raw));
|
|
158
184
|
} catch {
|
|
@@ -166,6 +192,9 @@ var ClientTransport = class {
|
|
|
166
192
|
this.closed = true;
|
|
167
193
|
this.resolveRead?.(null);
|
|
168
194
|
this.resolveRead = null;
|
|
195
|
+
this.buffer = "";
|
|
196
|
+
this.messageQueue.length = 0;
|
|
197
|
+
this.handlers.clear();
|
|
169
198
|
if (code !== 0 && code !== null) {
|
|
170
199
|
writeErr(`[acp-child exited with code ${code}]
|
|
171
200
|
`);
|
|
@@ -176,7 +205,13 @@ var ClientTransport = class {
|
|
|
176
205
|
const resolve3 = this.resolveRead;
|
|
177
206
|
this.resolveRead = null;
|
|
178
207
|
resolve3(msg);
|
|
179
|
-
} else {
|
|
208
|
+
} else if (this.handlers.size === 0) {
|
|
209
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
210
|
+
writeErr(`[acp-child message queue exceeds ${this.maxQueuedMessages} entries]
|
|
211
|
+
`);
|
|
212
|
+
this.stop();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
180
215
|
this.messageQueue.push(msg);
|
|
181
216
|
}
|
|
182
217
|
for (const handler of this.handlers) {
|
|
@@ -187,180 +222,13 @@ var ClientTransport = class {
|
|
|
187
222
|
}
|
|
188
223
|
}
|
|
189
224
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
constructor(opts) {
|
|
198
|
-
this.opts = opts;
|
|
199
|
-
}
|
|
200
|
-
start() {
|
|
201
|
-
const WS = globalThis.WebSocket;
|
|
202
|
-
if (!WS) {
|
|
203
|
-
return Promise.reject(
|
|
204
|
-
new Error(
|
|
205
|
-
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
206
|
-
)
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
210
|
-
return new Promise((resolve3, reject) => {
|
|
211
|
-
let settled = false;
|
|
212
|
-
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
213
|
-
this.ws = ws;
|
|
214
|
-
const timer = setTimeout(() => {
|
|
215
|
-
if (settled) return;
|
|
216
|
-
settled = true;
|
|
217
|
-
try {
|
|
218
|
-
ws.close();
|
|
219
|
-
} catch {
|
|
220
|
-
}
|
|
221
|
-
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
222
|
-
}, timeoutMs);
|
|
223
|
-
ws.addEventListener("open", () => {
|
|
224
|
-
if (settled) return;
|
|
225
|
-
settled = true;
|
|
226
|
-
clearTimeout(timer);
|
|
227
|
-
resolve3();
|
|
228
|
-
});
|
|
229
|
-
ws.addEventListener("error", (ev) => {
|
|
230
|
-
if (settled) {
|
|
231
|
-
this.closed = true;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
settled = true;
|
|
235
|
-
clearTimeout(timer);
|
|
236
|
-
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
237
|
-
reject(new Error(message));
|
|
238
|
-
});
|
|
239
|
-
ws.addEventListener("close", () => {
|
|
240
|
-
this.closed = true;
|
|
241
|
-
});
|
|
242
|
-
ws.addEventListener("message", (ev) => {
|
|
243
|
-
this.onData(ev.data);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
send(msg) {
|
|
248
|
-
if (this.closed || !this.ws) {
|
|
249
|
-
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
this.ws.send(JSON.stringify(msg));
|
|
253
|
-
return Promise.resolve();
|
|
254
|
-
} catch (err) {
|
|
255
|
-
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
onMessage(handler) {
|
|
259
|
-
this.handlers.add(handler);
|
|
260
|
-
return () => this.handlers.delete(handler);
|
|
261
|
-
}
|
|
262
|
-
stop() {
|
|
263
|
-
this.closed = true;
|
|
264
|
-
if (this.ws) {
|
|
265
|
-
try {
|
|
266
|
-
this.ws.close();
|
|
267
|
-
} catch {
|
|
268
|
-
}
|
|
269
|
-
this.ws = null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
onData(data) {
|
|
273
|
-
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
274
|
-
if (!text.trim()) return;
|
|
275
|
-
let msg;
|
|
276
|
-
try {
|
|
277
|
-
msg = JSON.parse(text);
|
|
278
|
-
} catch {
|
|
279
|
-
for (const line of text.split("\n")) {
|
|
280
|
-
if (!line.trim()) continue;
|
|
281
|
-
try {
|
|
282
|
-
this.dispatch(JSON.parse(line));
|
|
283
|
-
} catch {
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
this.dispatch(msg);
|
|
289
|
-
}
|
|
290
|
-
dispatch(msg) {
|
|
291
|
-
for (const handler of [...this.handlers]) {
|
|
292
|
-
try {
|
|
293
|
-
handler(msg);
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// src/client/tool-translator.ts
|
|
301
|
-
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
302
|
-
var DEFAULT_OPTIONS = {
|
|
303
|
-
asyncTools: true,
|
|
304
|
-
pollIntervalMs: 500,
|
|
305
|
-
totalTimeoutMs: 12e4
|
|
306
|
-
};
|
|
307
|
-
var ToolTranslator = class {
|
|
308
|
-
opts;
|
|
309
|
-
pending = /* @__PURE__ */ new Map();
|
|
310
|
-
constructor(opts = {}) {
|
|
311
|
-
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Start listening to a transport for tool responses and cancellations.
|
|
315
|
-
* Call this once after constructing the translator and before sending tasks.
|
|
316
|
-
*/
|
|
317
|
-
attachToTransport(transport) {
|
|
318
|
-
transport.onMessage((msg) => {
|
|
319
|
-
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
320
|
-
const pending = this.pending.get(msg.id);
|
|
321
|
-
if (pending) {
|
|
322
|
-
clearTimeout(pending.timeout);
|
|
323
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
324
|
-
pending.resolve(msg);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
328
|
-
const pending = this.pending.get(msg.id);
|
|
329
|
-
if (pending) {
|
|
330
|
-
clearTimeout(pending.timeout);
|
|
331
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
332
|
-
pending.reject(new Error("Call cancelled by client"));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Send a tool call over the transport and wait for a response.
|
|
339
|
-
* If asyncTools is true, polls for progress and resolves when the final
|
|
340
|
-
* response arrives.
|
|
341
|
-
*/
|
|
342
|
-
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
343
|
-
await transport.send({
|
|
344
|
-
jsonrpc: "2.0",
|
|
345
|
-
method: "tools/call",
|
|
346
|
-
id: callId,
|
|
347
|
-
params: { name, arguments: args }
|
|
348
|
-
});
|
|
349
|
-
return new Promise((resolve3, reject) => {
|
|
350
|
-
const timeout = setTimeout(() => {
|
|
351
|
-
this.pending.delete(callId);
|
|
352
|
-
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
353
|
-
}, this.opts.totalTimeoutMs);
|
|
354
|
-
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
cancelAll() {
|
|
358
|
-
for (const [, p] of this.pending) {
|
|
359
|
-
clearTimeout(p.timeout);
|
|
360
|
-
}
|
|
361
|
-
this.pending.clear();
|
|
362
|
-
}
|
|
363
|
-
};
|
|
225
|
+
function spawnInvocation(command, args, platform) {
|
|
226
|
+
if (platform !== "win32") return { command, args };
|
|
227
|
+
return buildWin32CmdShimInvocation(command, args);
|
|
228
|
+
}
|
|
229
|
+
function verbatimOptions(invocation) {
|
|
230
|
+
return invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } : {};
|
|
231
|
+
}
|
|
364
232
|
|
|
365
233
|
// src/types/acp-v1.ts
|
|
366
234
|
var ACP_PROTOCOL_VERSION = 1;
|
|
@@ -370,6 +238,14 @@ import { randomBytes } from "node:crypto";
|
|
|
370
238
|
import { realpathSync } from "node:fs";
|
|
371
239
|
import * as fsp from "node:fs/promises";
|
|
372
240
|
import * as path from "node:path";
|
|
241
|
+
var DEFAULT_FILE_OPERATIONS = {
|
|
242
|
+
stat: fsp.stat,
|
|
243
|
+
readFile: fsp.readFile,
|
|
244
|
+
writeFile: fsp.writeFile,
|
|
245
|
+
realpath: fsp.realpath,
|
|
246
|
+
rename: fsp.rename,
|
|
247
|
+
unlink: fsp.unlink
|
|
248
|
+
};
|
|
373
249
|
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
374
250
|
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
375
251
|
var FsError = class extends Error {
|
|
@@ -388,12 +264,14 @@ var FileServer = class {
|
|
|
388
264
|
timeoutMs;
|
|
389
265
|
maxReadBytes;
|
|
390
266
|
maxWriteBytes;
|
|
267
|
+
operations;
|
|
391
268
|
constructor(opts) {
|
|
392
269
|
this.root = path.resolve(opts.projectRoot);
|
|
393
270
|
this.realRoot = safeRealpathSync(this.root);
|
|
394
271
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
395
272
|
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
396
273
|
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
274
|
+
this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
|
|
397
275
|
}
|
|
398
276
|
/** Read a text file. Returns the content as a string. */
|
|
399
277
|
async readTextFile(params) {
|
|
@@ -401,7 +279,7 @@ var FileServer = class {
|
|
|
401
279
|
const controller = new AbortController();
|
|
402
280
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
403
281
|
try {
|
|
404
|
-
const stat2 = await
|
|
282
|
+
const stat2 = await this.operations.stat(safe).catch((err) => {
|
|
405
283
|
throw mapFsError(err, safe);
|
|
406
284
|
});
|
|
407
285
|
if (stat2.size > this.maxReadBytes) {
|
|
@@ -411,7 +289,7 @@ var FileServer = class {
|
|
|
411
289
|
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
412
290
|
);
|
|
413
291
|
}
|
|
414
|
-
const content = await
|
|
292
|
+
const content = await this.operations.readFile(safe, {
|
|
415
293
|
encoding: "utf8",
|
|
416
294
|
signal: controller.signal
|
|
417
295
|
});
|
|
@@ -441,20 +319,20 @@ var FileServer = class {
|
|
|
441
319
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
442
320
|
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
443
321
|
try {
|
|
444
|
-
await
|
|
322
|
+
await this.operations.writeFile(tmp, params.content, {
|
|
445
323
|
encoding: "utf8",
|
|
446
324
|
signal: controller.signal
|
|
447
325
|
});
|
|
448
326
|
await this.assertRealInside(tmp);
|
|
449
327
|
await this.assertRealInside(path.dirname(safe));
|
|
450
|
-
await
|
|
328
|
+
await this.operations.rename(tmp, safe);
|
|
451
329
|
} catch (err) {
|
|
452
330
|
if (err instanceof FsError) {
|
|
453
|
-
await
|
|
331
|
+
await this.operations.unlink(tmp).catch(() => void 0);
|
|
454
332
|
throw err;
|
|
455
333
|
}
|
|
456
334
|
try {
|
|
457
|
-
await
|
|
335
|
+
await this.operations.unlink(tmp);
|
|
458
336
|
} catch {
|
|
459
337
|
}
|
|
460
338
|
if (controller.signal.aborted) {
|
|
@@ -498,12 +376,14 @@ var FileServer = class {
|
|
|
498
376
|
for (; ; ) {
|
|
499
377
|
let real;
|
|
500
378
|
try {
|
|
501
|
-
real = await
|
|
379
|
+
real = await this.operations.realpath(probe);
|
|
502
380
|
} catch (err) {
|
|
503
381
|
const code = err.code;
|
|
504
382
|
if (code === "ENOENT") {
|
|
505
383
|
const parent = path.dirname(probe);
|
|
506
|
-
if (parent === probe)
|
|
384
|
+
if (parent === probe) {
|
|
385
|
+
throw new FsError("ENOENT", resolvedPath, `no existing ancestor: ${resolvedPath}`);
|
|
386
|
+
}
|
|
507
387
|
probe = parent;
|
|
508
388
|
continue;
|
|
509
389
|
}
|
|
@@ -584,26 +464,41 @@ import { spawn } from "node:child_process";
|
|
|
584
464
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
585
465
|
import * as path2 from "node:path";
|
|
586
466
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
467
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
587
468
|
var TerminalServer = class {
|
|
588
469
|
terminals = /* @__PURE__ */ new Map();
|
|
589
470
|
projectRoot;
|
|
590
471
|
commandTimeoutMs;
|
|
591
472
|
outputByteLimit;
|
|
592
473
|
maxOutputByteLimit;
|
|
474
|
+
maxTerminals;
|
|
475
|
+
abortSignal;
|
|
476
|
+
abortHandler = () => this.releaseAll();
|
|
593
477
|
nextId = 1;
|
|
594
478
|
constructor(opts) {
|
|
595
479
|
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
596
480
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
597
481
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
598
482
|
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
483
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
484
|
+
this.abortSignal = opts.signal;
|
|
599
485
|
if (opts.signal) {
|
|
600
|
-
opts.signal.addEventListener("abort",
|
|
486
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
601
487
|
}
|
|
602
488
|
}
|
|
603
489
|
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
604
490
|
create(params) {
|
|
491
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
605
496
|
const id = `term_${this.nextId++}`;
|
|
606
497
|
const cwd = this.resolveCwd(params.cwd);
|
|
498
|
+
const perCallByteLimit = Math.min(
|
|
499
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
500
|
+
this.maxOutputByteLimit
|
|
501
|
+
);
|
|
607
502
|
const proc = spawn(params.command, params.args ?? [], {
|
|
608
503
|
cwd,
|
|
609
504
|
env: this.buildEnv(params.env),
|
|
@@ -622,7 +517,8 @@ var TerminalServer = class {
|
|
|
622
517
|
cwd,
|
|
623
518
|
command: params.command,
|
|
624
519
|
args: params.args ?? [],
|
|
625
|
-
|
|
520
|
+
outputChunks: [],
|
|
521
|
+
outputHead: 0,
|
|
626
522
|
retainedBytes: 0,
|
|
627
523
|
truncated: false,
|
|
628
524
|
exitStatus: void 0,
|
|
@@ -647,31 +543,44 @@ var TerminalServer = class {
|
|
|
647
543
|
}
|
|
648
544
|
const exitStatus = { exitCode: 127, signal: null };
|
|
649
545
|
state.exitStatus = exitStatus;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
546
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
547
|
+
`, "utf8");
|
|
548
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
549
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
550
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
551
|
+
errorOutput = errorOutput.subarray(start);
|
|
552
|
+
state.truncated = true;
|
|
553
|
+
}
|
|
554
|
+
state.outputChunks.push(errorOutput);
|
|
555
|
+
state.retainedBytes = errorOutput.length;
|
|
653
556
|
resolve3(exitStatus);
|
|
654
557
|
});
|
|
655
558
|
})
|
|
656
559
|
};
|
|
657
|
-
const perCallByteLimit = Math.min(
|
|
658
|
-
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
659
|
-
this.maxOutputByteLimit
|
|
660
|
-
);
|
|
661
560
|
proc.stdout?.setEncoding("utf8");
|
|
662
561
|
proc.stderr?.setEncoding("utf8");
|
|
663
562
|
const onData = (chunk) => {
|
|
664
|
-
|
|
665
|
-
state.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
563
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
564
|
+
state.outputChunks.push(outputChunk);
|
|
565
|
+
state.retainedBytes += outputChunk.length;
|
|
566
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
567
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
568
|
+
const first = state.outputChunks[state.outputHead];
|
|
569
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
570
|
+
if (first.length <= overflow) {
|
|
571
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
572
|
+
state.outputHead++;
|
|
573
|
+
state.retainedBytes -= first.length;
|
|
574
|
+
continue;
|
|
672
575
|
}
|
|
673
|
-
|
|
674
|
-
|
|
576
|
+
let start = overflow;
|
|
577
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
578
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
579
|
+
state.retainedBytes -= start;
|
|
580
|
+
}
|
|
581
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
582
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
583
|
+
state.outputHead = 0;
|
|
675
584
|
}
|
|
676
585
|
};
|
|
677
586
|
proc.stdout?.on("data", onData);
|
|
@@ -690,7 +599,10 @@ var TerminalServer = class {
|
|
|
690
599
|
const state = this.terminals.get(terminalId);
|
|
691
600
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
692
601
|
return {
|
|
693
|
-
output:
|
|
602
|
+
output: Buffer.concat(
|
|
603
|
+
state.outputChunks.slice(state.outputHead),
|
|
604
|
+
state.retainedBytes
|
|
605
|
+
).toString("utf8"),
|
|
694
606
|
truncated: state.truncated,
|
|
695
607
|
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
696
608
|
};
|
|
@@ -726,6 +638,7 @@ var TerminalServer = class {
|
|
|
726
638
|
}
|
|
727
639
|
/** Kill all active terminals. Used on session close. */
|
|
728
640
|
releaseAll() {
|
|
641
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
729
642
|
for (const id of [...this.terminals.keys()]) {
|
|
730
643
|
this.release(id);
|
|
731
644
|
}
|
|
@@ -789,6 +702,212 @@ var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
789
702
|
"RUBYLIB"
|
|
790
703
|
]);
|
|
791
704
|
|
|
705
|
+
// src/client/trust-boundary-permission.ts
|
|
706
|
+
function pickOption(options, allowed) {
|
|
707
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
708
|
+
for (const kind of kinds) {
|
|
709
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
710
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
711
|
+
}
|
|
712
|
+
return { outcome: "cancelled" };
|
|
713
|
+
}
|
|
714
|
+
function riskFor(kind) {
|
|
715
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
716
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
717
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
718
|
+
return "elevated";
|
|
719
|
+
}
|
|
720
|
+
function capabilityFor(request) {
|
|
721
|
+
const raw = request.toolCall.rawInput;
|
|
722
|
+
if (typeof raw?.path === "string") {
|
|
723
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
724
|
+
}
|
|
725
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
726
|
+
return "process.spawn";
|
|
727
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
728
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
729
|
+
}
|
|
730
|
+
function subjectFor(request) {
|
|
731
|
+
const raw = request.toolCall.rawInput;
|
|
732
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
733
|
+
if (typeof raw?.path === "string") {
|
|
734
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
735
|
+
}
|
|
736
|
+
if (typeof raw?.command === "string") {
|
|
737
|
+
return {
|
|
738
|
+
kind: "command",
|
|
739
|
+
id: raw.command,
|
|
740
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
kind: "resource",
|
|
745
|
+
id: title,
|
|
746
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function isAllowed(decision) {
|
|
750
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
751
|
+
}
|
|
752
|
+
function toTrustBoundaryRequest(request, options) {
|
|
753
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
754
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
755
|
+
return {
|
|
756
|
+
version: 1,
|
|
757
|
+
requestId: String(request.toolCall.toolCallId),
|
|
758
|
+
actor: {
|
|
759
|
+
...options.actor ?? { kind: "agent" },
|
|
760
|
+
...sessionId ? { sessionId } : {}
|
|
761
|
+
},
|
|
762
|
+
surface: "acp",
|
|
763
|
+
capability: capabilityFor(request),
|
|
764
|
+
subject: subjectFor(request),
|
|
765
|
+
risk: riskFor(request.toolCall.kind),
|
|
766
|
+
scope: {
|
|
767
|
+
...options.scope ?? {},
|
|
768
|
+
...sessionId ? { sessionId } : {}
|
|
769
|
+
},
|
|
770
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
771
|
+
metadata: {
|
|
772
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
773
|
+
toolKind: request.toolCall.kind ?? null
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
778
|
+
return async (request) => {
|
|
779
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
780
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
781
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
782
|
+
return pickOption(request.options, isAllowed(decision));
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/client/websocket-transport.ts
|
|
787
|
+
var WebSocketClientTransport = class {
|
|
788
|
+
ws = null;
|
|
789
|
+
handlers = /* @__PURE__ */ new Set();
|
|
790
|
+
closed = false;
|
|
791
|
+
opts;
|
|
792
|
+
maxBufferedBytes;
|
|
793
|
+
maxMessageChars;
|
|
794
|
+
constructor(opts) {
|
|
795
|
+
this.opts = opts;
|
|
796
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
797
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
798
|
+
}
|
|
799
|
+
start() {
|
|
800
|
+
const WS = globalThis.WebSocket;
|
|
801
|
+
if (!WS) {
|
|
802
|
+
return Promise.reject(
|
|
803
|
+
new Error(
|
|
804
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
805
|
+
)
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
809
|
+
return new Promise((resolve3, reject) => {
|
|
810
|
+
let settled = false;
|
|
811
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
812
|
+
this.ws = ws;
|
|
813
|
+
const timer = setTimeout(() => {
|
|
814
|
+
settled = true;
|
|
815
|
+
try {
|
|
816
|
+
ws.close();
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
820
|
+
}, timeoutMs);
|
|
821
|
+
ws.addEventListener("open", () => {
|
|
822
|
+
if (settled) return;
|
|
823
|
+
settled = true;
|
|
824
|
+
clearTimeout(timer);
|
|
825
|
+
resolve3();
|
|
826
|
+
});
|
|
827
|
+
ws.addEventListener("error", (ev) => {
|
|
828
|
+
if (settled) {
|
|
829
|
+
this.closed = true;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
settled = true;
|
|
833
|
+
clearTimeout(timer);
|
|
834
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
835
|
+
reject(new Error(message));
|
|
836
|
+
});
|
|
837
|
+
ws.addEventListener("close", () => {
|
|
838
|
+
this.closed = true;
|
|
839
|
+
});
|
|
840
|
+
ws.addEventListener("message", (ev) => {
|
|
841
|
+
this.onData(ev.data);
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
send(msg) {
|
|
846
|
+
if (this.closed || !this.ws) {
|
|
847
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
const serialized = JSON.stringify(msg);
|
|
851
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
|
|
852
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
853
|
+
this.stop();
|
|
854
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
855
|
+
}
|
|
856
|
+
this.ws.send(serialized);
|
|
857
|
+
return Promise.resolve();
|
|
858
|
+
} catch (err) {
|
|
859
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
onMessage(handler) {
|
|
863
|
+
this.handlers.add(handler);
|
|
864
|
+
return () => this.handlers.delete(handler);
|
|
865
|
+
}
|
|
866
|
+
stop() {
|
|
867
|
+
this.closed = true;
|
|
868
|
+
if (this.ws) {
|
|
869
|
+
try {
|
|
870
|
+
this.ws.close();
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
this.ws = null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
onData(data) {
|
|
877
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
878
|
+
if (text.length > this.maxMessageChars) {
|
|
879
|
+
this.stop();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (!text.trim()) return;
|
|
883
|
+
let msg;
|
|
884
|
+
try {
|
|
885
|
+
msg = JSON.parse(text);
|
|
886
|
+
} catch {
|
|
887
|
+
for (const line of text.split("\n")) {
|
|
888
|
+
if (!line.trim()) continue;
|
|
889
|
+
try {
|
|
890
|
+
this.dispatch(JSON.parse(line));
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
this.dispatch(msg);
|
|
897
|
+
}
|
|
898
|
+
dispatch(msg) {
|
|
899
|
+
for (const handler of [...this.handlers]) {
|
|
900
|
+
try {
|
|
901
|
+
handler(msg);
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
function finitePositiveLimit(value, fallback) {
|
|
908
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
909
|
+
}
|
|
910
|
+
|
|
792
911
|
// src/client/acp-session.ts
|
|
793
912
|
var ACPSessionError = class extends Error {
|
|
794
913
|
kind;
|
|
@@ -810,6 +929,7 @@ var ACPSession = class _ACPSession {
|
|
|
810
929
|
permissionPolicy;
|
|
811
930
|
timeoutMs;
|
|
812
931
|
opts;
|
|
932
|
+
transportOff = null;
|
|
813
933
|
state = "init";
|
|
814
934
|
sessionId = null;
|
|
815
935
|
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
@@ -841,8 +961,19 @@ var ACPSession = class _ACPSession {
|
|
|
841
961
|
if (opts.terminalOutputByteLimit !== void 0) {
|
|
842
962
|
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
843
963
|
}
|
|
964
|
+
if (opts.terminalMaxCount !== void 0) {
|
|
965
|
+
termOpts.maxTerminals = opts.terminalMaxCount;
|
|
966
|
+
}
|
|
844
967
|
this.terminalServer = new TerminalServer(termOpts);
|
|
845
|
-
|
|
968
|
+
if (opts.permissionPolicy && opts.trustBoundary) {
|
|
969
|
+
throw new TypeError("permissionPolicy and trustBoundary are mutually exclusive");
|
|
970
|
+
}
|
|
971
|
+
this.permissionPolicy = opts.trustBoundary ? makeTrustBoundaryPermissionPolicy({
|
|
972
|
+
boundary: opts.trustBoundary,
|
|
973
|
+
...opts.trustActor ? { actor: opts.trustActor } : {},
|
|
974
|
+
scope: opts.trustScope ?? { cwd: opts.projectRoot },
|
|
975
|
+
...opts.trustAuthContext ? { authContext: opts.trustAuthContext } : {}
|
|
976
|
+
}) : opts.permissionPolicy ?? readOnlyPermissionPolicy;
|
|
846
977
|
}
|
|
847
978
|
// ──────────────────────────────────────────────────────────────────────
|
|
848
979
|
// Public accessors
|
|
@@ -916,10 +1047,12 @@ var ACPSession = class _ACPSession {
|
|
|
916
1047
|
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
917
1048
|
}
|
|
918
1049
|
const session = new _ACPSession(opts, transport);
|
|
919
|
-
transport.onMessage((msg) => session.handleMessage(msg));
|
|
1050
|
+
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
920
1051
|
try {
|
|
921
1052
|
await session.initialize();
|
|
922
1053
|
} catch (err) {
|
|
1054
|
+
session.transportOff?.();
|
|
1055
|
+
session.transportOff = null;
|
|
923
1056
|
try {
|
|
924
1057
|
transport.stop();
|
|
925
1058
|
} catch {
|
|
@@ -1083,7 +1216,11 @@ var ACPSession = class _ACPSession {
|
|
|
1083
1216
|
mcpServers: servers
|
|
1084
1217
|
});
|
|
1085
1218
|
if (isJsonRpcError(result)) {
|
|
1086
|
-
throw new ACPSessionError(
|
|
1219
|
+
throw new ACPSessionError(
|
|
1220
|
+
"prompt_failed",
|
|
1221
|
+
`session/resume failed: ${result.message}`,
|
|
1222
|
+
result
|
|
1223
|
+
);
|
|
1087
1224
|
}
|
|
1088
1225
|
this.sessionId = sessionId;
|
|
1089
1226
|
}
|
|
@@ -1134,7 +1271,11 @@ var ACPSession = class _ACPSession {
|
|
|
1134
1271
|
const id = this.allocId();
|
|
1135
1272
|
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1136
1273
|
if (isJsonRpcError(result)) {
|
|
1137
|
-
throw new ACPSessionError(
|
|
1274
|
+
throw new ACPSessionError(
|
|
1275
|
+
"prompt_failed",
|
|
1276
|
+
`session/delete failed: ${result.message}`,
|
|
1277
|
+
result
|
|
1278
|
+
);
|
|
1138
1279
|
}
|
|
1139
1280
|
if (this.sessionId === sessionId) {
|
|
1140
1281
|
this.sessionId = null;
|
|
@@ -1169,7 +1310,11 @@ var ACPSession = class _ACPSession {
|
|
|
1169
1310
|
const id = this.allocId();
|
|
1170
1311
|
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
1171
1312
|
if (isJsonRpcError(result)) {
|
|
1172
|
-
throw new ACPSessionError(
|
|
1313
|
+
throw new ACPSessionError(
|
|
1314
|
+
"prompt_failed",
|
|
1315
|
+
`session/set_mode failed: ${result.message}`,
|
|
1316
|
+
result
|
|
1317
|
+
);
|
|
1173
1318
|
}
|
|
1174
1319
|
}
|
|
1175
1320
|
/**
|
|
@@ -1184,7 +1329,11 @@ var ACPSession = class _ACPSession {
|
|
|
1184
1329
|
value
|
|
1185
1330
|
});
|
|
1186
1331
|
if (isJsonRpcError(result)) {
|
|
1187
|
-
throw new ACPSessionError(
|
|
1332
|
+
throw new ACPSessionError(
|
|
1333
|
+
"prompt_failed",
|
|
1334
|
+
`session/set_config_option failed: ${result.message}`,
|
|
1335
|
+
result
|
|
1336
|
+
);
|
|
1188
1337
|
}
|
|
1189
1338
|
}
|
|
1190
1339
|
/**
|
|
@@ -1195,7 +1344,11 @@ var ACPSession = class _ACPSession {
|
|
|
1195
1344
|
const id = this.allocId();
|
|
1196
1345
|
const result = await this.sendRequest(id, "providers/list", {});
|
|
1197
1346
|
if (isJsonRpcError(result)) {
|
|
1198
|
-
throw new ACPSessionError(
|
|
1347
|
+
throw new ACPSessionError(
|
|
1348
|
+
"prompt_failed",
|
|
1349
|
+
`providers/list failed: ${result.message}`,
|
|
1350
|
+
result
|
|
1351
|
+
);
|
|
1199
1352
|
}
|
|
1200
1353
|
const r = result;
|
|
1201
1354
|
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
@@ -1231,7 +1384,11 @@ var ACPSession = class _ACPSession {
|
|
|
1231
1384
|
const id = this.allocId();
|
|
1232
1385
|
const result = await this.sendRequest(id, "providers/disable", {});
|
|
1233
1386
|
if (isJsonRpcError(result)) {
|
|
1234
|
-
throw new ACPSessionError(
|
|
1387
|
+
throw new ACPSessionError(
|
|
1388
|
+
"prompt_failed",
|
|
1389
|
+
`providers/disable failed: ${result.message}`,
|
|
1390
|
+
result
|
|
1391
|
+
);
|
|
1235
1392
|
}
|
|
1236
1393
|
}
|
|
1237
1394
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -1338,11 +1495,7 @@ var ACPSession = class _ACPSession {
|
|
|
1338
1495
|
}
|
|
1339
1496
|
const sessionId = result.sessionId;
|
|
1340
1497
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1341
|
-
throw new ACPSessionError(
|
|
1342
|
-
"protocol_error",
|
|
1343
|
-
"session/new returned no sessionId",
|
|
1344
|
-
result
|
|
1345
|
-
);
|
|
1498
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
1346
1499
|
}
|
|
1347
1500
|
this.sessionId = sessionId;
|
|
1348
1501
|
}
|
|
@@ -1385,6 +1538,8 @@ var ACPSession = class _ACPSession {
|
|
|
1385
1538
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
1386
1539
|
}
|
|
1387
1540
|
this.pending.clear();
|
|
1541
|
+
this.transportOff?.();
|
|
1542
|
+
this.transportOff = null;
|
|
1388
1543
|
try {
|
|
1389
1544
|
this.transport.stop();
|
|
1390
1545
|
} catch {
|
|
@@ -1420,10 +1575,7 @@ var ACPSession = class _ACPSession {
|
|
|
1420
1575
|
const handle = setTimeout(() => {
|
|
1421
1576
|
this.pending.delete(id);
|
|
1422
1577
|
reject(
|
|
1423
|
-
new ACPSessionError(
|
|
1424
|
-
"protocol_error",
|
|
1425
|
-
`${method} timed out after ${effectiveTimeout}ms`
|
|
1426
|
-
)
|
|
1578
|
+
new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
|
|
1427
1579
|
);
|
|
1428
1580
|
}, effectiveTimeout);
|
|
1429
1581
|
this.pending.set(id, {
|
|
@@ -1740,7 +1892,12 @@ var ACPSession = class _ACPSession {
|
|
|
1740
1892
|
toolCallId: `acp-terminal-create-${id}`,
|
|
1741
1893
|
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
1742
1894
|
kind: "execute",
|
|
1743
|
-
rawInput: {
|
|
1895
|
+
rawInput: {
|
|
1896
|
+
command: params.command,
|
|
1897
|
+
args: params.args,
|
|
1898
|
+
cwd: params.cwd,
|
|
1899
|
+
sessionId: params.sessionId
|
|
1900
|
+
}
|
|
1744
1901
|
});
|
|
1745
1902
|
if (!allowed) {
|
|
1746
1903
|
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
@@ -1956,17 +2113,74 @@ function mapACPKind(acpKind) {
|
|
|
1956
2113
|
}
|
|
1957
2114
|
}
|
|
1958
2115
|
function isRetryable(kind) {
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
case "provider_rate_limit":
|
|
1962
|
-
case "provider_timeout":
|
|
1963
|
-
case "tool_threw":
|
|
1964
|
-
case "budget_timeout":
|
|
1965
|
-
return true;
|
|
1966
|
-
default:
|
|
1967
|
-
return false;
|
|
1968
|
-
}
|
|
2116
|
+
void kind;
|
|
2117
|
+
return false;
|
|
1969
2118
|
}
|
|
2119
|
+
|
|
2120
|
+
// src/client/tool-translator.ts
|
|
2121
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
2122
|
+
var DEFAULT_OPTIONS = {
|
|
2123
|
+
asyncTools: true,
|
|
2124
|
+
pollIntervalMs: 500,
|
|
2125
|
+
totalTimeoutMs: 12e4
|
|
2126
|
+
};
|
|
2127
|
+
var ToolTranslator = class {
|
|
2128
|
+
opts;
|
|
2129
|
+
pending = /* @__PURE__ */ new Map();
|
|
2130
|
+
constructor(opts = {}) {
|
|
2131
|
+
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Start listening to a transport for tool responses and cancellations.
|
|
2135
|
+
* Call this once after constructing the translator and before sending tasks.
|
|
2136
|
+
*/
|
|
2137
|
+
attachToTransport(transport) {
|
|
2138
|
+
transport.onMessage((msg) => {
|
|
2139
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
2140
|
+
const pending = this.pending.get(msg.id);
|
|
2141
|
+
if (pending) {
|
|
2142
|
+
clearTimeout(pending.timeout);
|
|
2143
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
2144
|
+
pending.resolve(msg);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
2148
|
+
const pending = this.pending.get(msg.id);
|
|
2149
|
+
if (pending) {
|
|
2150
|
+
clearTimeout(pending.timeout);
|
|
2151
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
2152
|
+
pending.reject(new Error("Call cancelled by client"));
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Send a tool call over the transport and wait for a response.
|
|
2159
|
+
* If asyncTools is true, polls for progress and resolves when the final
|
|
2160
|
+
* response arrives.
|
|
2161
|
+
*/
|
|
2162
|
+
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
2163
|
+
await transport.send({
|
|
2164
|
+
jsonrpc: "2.0",
|
|
2165
|
+
method: "tools/call",
|
|
2166
|
+
id: callId,
|
|
2167
|
+
params: { name, arguments: args }
|
|
2168
|
+
});
|
|
2169
|
+
return new Promise((resolve3, reject) => {
|
|
2170
|
+
const timeout = setTimeout(() => {
|
|
2171
|
+
this.pending.delete(callId);
|
|
2172
|
+
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
2173
|
+
}, this.opts.totalTimeoutMs);
|
|
2174
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
cancelAll() {
|
|
2178
|
+
for (const [, p] of this.pending) {
|
|
2179
|
+
clearTimeout(p.timeout);
|
|
2180
|
+
}
|
|
2181
|
+
this.pending.clear();
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
1970
2184
|
export {
|
|
1971
2185
|
ACPSession,
|
|
1972
2186
|
ACPSessionError,
|
|
@@ -1978,7 +2192,9 @@ export {
|
|
|
1978
2192
|
imageContent,
|
|
1979
2193
|
makeACPSubagentRunner,
|
|
1980
2194
|
makePermissionPolicy,
|
|
2195
|
+
makeTrustBoundaryPermissionPolicy,
|
|
1981
2196
|
readOnlyPermissionPolicy,
|
|
1982
|
-
textContent
|
|
2197
|
+
textContent,
|
|
2198
|
+
toTrustBoundaryRequest
|
|
1983
2199
|
};
|
|
1984
2200
|
//# sourceMappingURL=client.js.map
|