@wrongstack/acp 0.292.1 → 0.295.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +6 -189
- package/dist/agent/protocol-handler.d.ts.map +1 -1
- package/dist/agent/server-agent-turn.d.ts +7 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/stdio-transport.d.ts +12 -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 +2 -0
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +172 -22
- package/dist/agent.js.map +4 -4
- package/dist/client/acp-session.d.ts +19 -6
- package/dist/client/acp-session.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/permission.d.ts +7 -3
- package/dist/client/permission.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 +16 -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 +440 -225
- 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 +2433 -2121
- package/dist/index.js.map +4 -4
- 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.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/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 +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/wrongstack-acp-agent.js +107 -16
- package/dist/wrongstack-acp-agent.js.map +4 -4
- package/package.json +10 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/agent/stdio-transport.ts
|
|
2
|
-
import { expectDefined, writeErr } from "@wrongstack/core";
|
|
2
|
+
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
3
3
|
|
|
4
4
|
// src/win32-cmd.ts
|
|
5
5
|
var WIN32_CMD_META = /[&|<>"\r\n\0]/;
|
|
@@ -26,6 +26,11 @@ function quoteWin32CmdArg(arg) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// src/agent/stdio-transport.ts
|
|
29
|
+
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
30
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
31
|
+
function positiveLimit(value, fallback) {
|
|
32
|
+
return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
|
|
33
|
+
}
|
|
29
34
|
var ClientTransport = class {
|
|
30
35
|
child = null;
|
|
31
36
|
buffer = "";
|
|
@@ -34,17 +39,24 @@ var ClientTransport = class {
|
|
|
34
39
|
resolveRead = null;
|
|
35
40
|
messageQueue = [];
|
|
36
41
|
opts;
|
|
42
|
+
maxFrameChars;
|
|
43
|
+
maxQueuedMessages;
|
|
37
44
|
constructor(options) {
|
|
38
45
|
this.opts = {
|
|
39
46
|
handshakeTimeoutMs: 3e4,
|
|
40
47
|
...options
|
|
41
48
|
};
|
|
49
|
+
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
50
|
+
this.maxQueuedMessages = positiveLimit(
|
|
51
|
+
options.maxQueuedMessages,
|
|
52
|
+
DEFAULT_MAX_QUEUED_MESSAGES
|
|
53
|
+
);
|
|
42
54
|
}
|
|
43
55
|
async start() {
|
|
44
56
|
if (this.child) return;
|
|
45
57
|
const [{ spawn: spawn2 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
46
58
|
import("node:child_process"),
|
|
47
|
-
import("@wrongstack/core"),
|
|
59
|
+
import("@wrongstack/core/utils"),
|
|
48
60
|
import("node:os")
|
|
49
61
|
]);
|
|
50
62
|
return new Promise((resolve3, reject) => {
|
|
@@ -139,10 +151,16 @@ var ClientTransport = class {
|
|
|
139
151
|
return () => this.handlers.delete(handler);
|
|
140
152
|
}
|
|
141
153
|
stop() {
|
|
142
|
-
if (!this.child) return;
|
|
143
154
|
this.closed = true;
|
|
155
|
+
this.resolveRead?.(null);
|
|
156
|
+
this.resolveRead = null;
|
|
157
|
+
this.buffer = "";
|
|
158
|
+
this.messageQueue.length = 0;
|
|
159
|
+
this.handlers.clear();
|
|
160
|
+
const child = this.child;
|
|
161
|
+
if (!child) return;
|
|
144
162
|
try {
|
|
145
|
-
|
|
163
|
+
child.kill();
|
|
146
164
|
} catch {
|
|
147
165
|
}
|
|
148
166
|
this.child = null;
|
|
@@ -151,8 +169,20 @@ var ClientTransport = class {
|
|
|
151
169
|
this.buffer += chunk;
|
|
152
170
|
const lines = this.buffer.split("\n");
|
|
153
171
|
this.buffer = lines.pop() ?? "";
|
|
172
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
173
|
+
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
174
|
+
`);
|
|
175
|
+
this.stop();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
154
178
|
for (const raw of lines) {
|
|
155
179
|
if (!raw.trim()) continue;
|
|
180
|
+
if (raw.length > this.maxFrameChars) {
|
|
181
|
+
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
182
|
+
`);
|
|
183
|
+
this.stop();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
156
186
|
try {
|
|
157
187
|
this.dispatch(JSON.parse(raw));
|
|
158
188
|
} catch {
|
|
@@ -166,6 +196,9 @@ var ClientTransport = class {
|
|
|
166
196
|
this.closed = true;
|
|
167
197
|
this.resolveRead?.(null);
|
|
168
198
|
this.resolveRead = null;
|
|
199
|
+
this.buffer = "";
|
|
200
|
+
this.messageQueue.length = 0;
|
|
201
|
+
this.handlers.clear();
|
|
169
202
|
if (code !== 0 && code !== null) {
|
|
170
203
|
writeErr(`[acp-child exited with code ${code}]
|
|
171
204
|
`);
|
|
@@ -176,7 +209,13 @@ var ClientTransport = class {
|
|
|
176
209
|
const resolve3 = this.resolveRead;
|
|
177
210
|
this.resolveRead = null;
|
|
178
211
|
resolve3(msg);
|
|
179
|
-
} else {
|
|
212
|
+
} else if (this.handlers.size === 0) {
|
|
213
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
214
|
+
writeErr(`[acp-child message queue exceeds ${this.maxQueuedMessages} entries]
|
|
215
|
+
`);
|
|
216
|
+
this.stop();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
180
219
|
this.messageQueue.push(msg);
|
|
181
220
|
}
|
|
182
221
|
for (const handler of this.handlers) {
|
|
@@ -188,180 +227,6 @@ var ClientTransport = class {
|
|
|
188
227
|
}
|
|
189
228
|
};
|
|
190
229
|
|
|
191
|
-
// src/client/websocket-transport.ts
|
|
192
|
-
var WebSocketClientTransport = class {
|
|
193
|
-
ws = null;
|
|
194
|
-
handlers = /* @__PURE__ */ new Set();
|
|
195
|
-
closed = false;
|
|
196
|
-
opts;
|
|
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
|
-
};
|
|
364
|
-
|
|
365
230
|
// src/types/acp-v1.ts
|
|
366
231
|
var ACP_PROTOCOL_VERSION = 1;
|
|
367
232
|
|
|
@@ -584,26 +449,42 @@ import { spawn } from "node:child_process";
|
|
|
584
449
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
585
450
|
import * as path2 from "node:path";
|
|
586
451
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
452
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
587
453
|
var TerminalServer = class {
|
|
588
454
|
terminals = /* @__PURE__ */ new Map();
|
|
589
455
|
projectRoot;
|
|
590
456
|
commandTimeoutMs;
|
|
591
457
|
outputByteLimit;
|
|
592
458
|
maxOutputByteLimit;
|
|
459
|
+
maxTerminals;
|
|
460
|
+
abortSignal;
|
|
461
|
+
abortHandler = () => this.releaseAll();
|
|
593
462
|
nextId = 1;
|
|
594
463
|
constructor(opts) {
|
|
595
464
|
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
596
465
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
597
466
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
598
467
|
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
468
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
469
|
+
if (this.maxTerminals < 1) throw new RangeError("maxTerminals must be at least 1");
|
|
470
|
+
this.abortSignal = opts.signal;
|
|
599
471
|
if (opts.signal) {
|
|
600
|
-
opts.signal.addEventListener("abort",
|
|
472
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
601
473
|
}
|
|
602
474
|
}
|
|
603
475
|
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
604
476
|
create(params) {
|
|
477
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
605
482
|
const id = `term_${this.nextId++}`;
|
|
606
483
|
const cwd = this.resolveCwd(params.cwd);
|
|
484
|
+
const perCallByteLimit = Math.min(
|
|
485
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
486
|
+
this.maxOutputByteLimit
|
|
487
|
+
);
|
|
607
488
|
const proc = spawn(params.command, params.args ?? [], {
|
|
608
489
|
cwd,
|
|
609
490
|
env: this.buildEnv(params.env),
|
|
@@ -622,7 +503,8 @@ var TerminalServer = class {
|
|
|
622
503
|
cwd,
|
|
623
504
|
command: params.command,
|
|
624
505
|
args: params.args ?? [],
|
|
625
|
-
|
|
506
|
+
outputChunks: [],
|
|
507
|
+
outputHead: 0,
|
|
626
508
|
retainedBytes: 0,
|
|
627
509
|
truncated: false,
|
|
628
510
|
exitStatus: void 0,
|
|
@@ -647,31 +529,44 @@ var TerminalServer = class {
|
|
|
647
529
|
}
|
|
648
530
|
const exitStatus = { exitCode: 127, signal: null };
|
|
649
531
|
state.exitStatus = exitStatus;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
532
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
533
|
+
`, "utf8");
|
|
534
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
535
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
536
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
537
|
+
errorOutput = errorOutput.subarray(start);
|
|
538
|
+
state.truncated = true;
|
|
539
|
+
}
|
|
540
|
+
state.outputChunks.push(errorOutput);
|
|
541
|
+
state.retainedBytes = errorOutput.length;
|
|
653
542
|
resolve3(exitStatus);
|
|
654
543
|
});
|
|
655
544
|
})
|
|
656
545
|
};
|
|
657
|
-
const perCallByteLimit = Math.min(
|
|
658
|
-
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
659
|
-
this.maxOutputByteLimit
|
|
660
|
-
);
|
|
661
546
|
proc.stdout?.setEncoding("utf8");
|
|
662
547
|
proc.stderr?.setEncoding("utf8");
|
|
663
548
|
const onData = (chunk) => {
|
|
664
|
-
|
|
665
|
-
state.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
549
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
550
|
+
state.outputChunks.push(outputChunk);
|
|
551
|
+
state.retainedBytes += outputChunk.length;
|
|
552
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
553
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
554
|
+
const first = state.outputChunks[state.outputHead];
|
|
555
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
556
|
+
if (first.length <= overflow) {
|
|
557
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
558
|
+
state.outputHead++;
|
|
559
|
+
state.retainedBytes -= first.length;
|
|
560
|
+
continue;
|
|
672
561
|
}
|
|
673
|
-
|
|
674
|
-
|
|
562
|
+
let start = overflow;
|
|
563
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
564
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
565
|
+
state.retainedBytes -= start;
|
|
566
|
+
}
|
|
567
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
568
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
569
|
+
state.outputHead = 0;
|
|
675
570
|
}
|
|
676
571
|
};
|
|
677
572
|
proc.stdout?.on("data", onData);
|
|
@@ -690,7 +585,10 @@ var TerminalServer = class {
|
|
|
690
585
|
const state = this.terminals.get(terminalId);
|
|
691
586
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
692
587
|
return {
|
|
693
|
-
output:
|
|
588
|
+
output: Buffer.concat(
|
|
589
|
+
state.outputChunks.slice(state.outputHead),
|
|
590
|
+
state.retainedBytes
|
|
591
|
+
).toString("utf8"),
|
|
694
592
|
truncated: state.truncated,
|
|
695
593
|
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
696
594
|
};
|
|
@@ -726,6 +624,7 @@ var TerminalServer = class {
|
|
|
726
624
|
}
|
|
727
625
|
/** Kill all active terminals. Used on session close. */
|
|
728
626
|
releaseAll() {
|
|
627
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
729
628
|
for (const id of [...this.terminals.keys()]) {
|
|
730
629
|
this.release(id);
|
|
731
630
|
}
|
|
@@ -789,6 +688,213 @@ var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
789
688
|
"RUBYLIB"
|
|
790
689
|
]);
|
|
791
690
|
|
|
691
|
+
// src/client/trust-boundary-permission.ts
|
|
692
|
+
function pickOption(options, allowed) {
|
|
693
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
694
|
+
for (const kind of kinds) {
|
|
695
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
696
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
697
|
+
}
|
|
698
|
+
return { outcome: "cancelled" };
|
|
699
|
+
}
|
|
700
|
+
function riskFor(kind) {
|
|
701
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
702
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
703
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
704
|
+
return "elevated";
|
|
705
|
+
}
|
|
706
|
+
function capabilityFor(request) {
|
|
707
|
+
const raw = request.toolCall.rawInput;
|
|
708
|
+
if (typeof raw?.path === "string") {
|
|
709
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
710
|
+
}
|
|
711
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
712
|
+
return "process.spawn";
|
|
713
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
714
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
715
|
+
}
|
|
716
|
+
function subjectFor(request) {
|
|
717
|
+
const raw = request.toolCall.rawInput;
|
|
718
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
719
|
+
if (typeof raw?.path === "string") {
|
|
720
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
721
|
+
}
|
|
722
|
+
if (typeof raw?.command === "string") {
|
|
723
|
+
return {
|
|
724
|
+
kind: "command",
|
|
725
|
+
id: raw.command,
|
|
726
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
kind: "resource",
|
|
731
|
+
id: title,
|
|
732
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function isAllowed(decision) {
|
|
736
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
737
|
+
}
|
|
738
|
+
function toTrustBoundaryRequest(request, options) {
|
|
739
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
740
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
741
|
+
return {
|
|
742
|
+
version: 1,
|
|
743
|
+
requestId: String(request.toolCall.toolCallId),
|
|
744
|
+
actor: {
|
|
745
|
+
...options.actor ?? { kind: "agent" },
|
|
746
|
+
...sessionId ? { sessionId } : {}
|
|
747
|
+
},
|
|
748
|
+
surface: "acp",
|
|
749
|
+
capability: capabilityFor(request),
|
|
750
|
+
subject: subjectFor(request),
|
|
751
|
+
risk: riskFor(request.toolCall.kind),
|
|
752
|
+
scope: {
|
|
753
|
+
...options.scope ?? {},
|
|
754
|
+
...sessionId ? { sessionId } : {}
|
|
755
|
+
},
|
|
756
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
757
|
+
metadata: {
|
|
758
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
759
|
+
toolKind: request.toolCall.kind ?? null
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
764
|
+
return async (request) => {
|
|
765
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
766
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
767
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
768
|
+
return pickOption(request.options, isAllowed(decision));
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/client/websocket-transport.ts
|
|
773
|
+
var WebSocketClientTransport = class {
|
|
774
|
+
ws = null;
|
|
775
|
+
handlers = /* @__PURE__ */ new Set();
|
|
776
|
+
closed = false;
|
|
777
|
+
opts;
|
|
778
|
+
maxBufferedBytes;
|
|
779
|
+
maxMessageChars;
|
|
780
|
+
constructor(opts) {
|
|
781
|
+
this.opts = opts;
|
|
782
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
783
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
784
|
+
}
|
|
785
|
+
start() {
|
|
786
|
+
const WS = globalThis.WebSocket;
|
|
787
|
+
if (!WS) {
|
|
788
|
+
return Promise.reject(
|
|
789
|
+
new Error(
|
|
790
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
791
|
+
)
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
795
|
+
return new Promise((resolve3, reject) => {
|
|
796
|
+
let settled = false;
|
|
797
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
798
|
+
this.ws = ws;
|
|
799
|
+
const timer = setTimeout(() => {
|
|
800
|
+
if (settled) return;
|
|
801
|
+
settled = true;
|
|
802
|
+
try {
|
|
803
|
+
ws.close();
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
807
|
+
}, timeoutMs);
|
|
808
|
+
ws.addEventListener("open", () => {
|
|
809
|
+
if (settled) return;
|
|
810
|
+
settled = true;
|
|
811
|
+
clearTimeout(timer);
|
|
812
|
+
resolve3();
|
|
813
|
+
});
|
|
814
|
+
ws.addEventListener("error", (ev) => {
|
|
815
|
+
if (settled) {
|
|
816
|
+
this.closed = true;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
settled = true;
|
|
820
|
+
clearTimeout(timer);
|
|
821
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
822
|
+
reject(new Error(message));
|
|
823
|
+
});
|
|
824
|
+
ws.addEventListener("close", () => {
|
|
825
|
+
this.closed = true;
|
|
826
|
+
});
|
|
827
|
+
ws.addEventListener("message", (ev) => {
|
|
828
|
+
this.onData(ev.data);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
send(msg) {
|
|
833
|
+
if (this.closed || !this.ws) {
|
|
834
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const serialized = JSON.stringify(msg);
|
|
838
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount ?? 0 : 0;
|
|
839
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
840
|
+
this.stop();
|
|
841
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
842
|
+
}
|
|
843
|
+
this.ws.send(serialized);
|
|
844
|
+
return Promise.resolve();
|
|
845
|
+
} catch (err) {
|
|
846
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
onMessage(handler) {
|
|
850
|
+
this.handlers.add(handler);
|
|
851
|
+
return () => this.handlers.delete(handler);
|
|
852
|
+
}
|
|
853
|
+
stop() {
|
|
854
|
+
this.closed = true;
|
|
855
|
+
if (this.ws) {
|
|
856
|
+
try {
|
|
857
|
+
this.ws.close();
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
this.ws = null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
onData(data) {
|
|
864
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
865
|
+
if (text.length > this.maxMessageChars) {
|
|
866
|
+
this.stop();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (!text.trim()) return;
|
|
870
|
+
let msg;
|
|
871
|
+
try {
|
|
872
|
+
msg = JSON.parse(text);
|
|
873
|
+
} catch {
|
|
874
|
+
for (const line of text.split("\n")) {
|
|
875
|
+
if (!line.trim()) continue;
|
|
876
|
+
try {
|
|
877
|
+
this.dispatch(JSON.parse(line));
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.dispatch(msg);
|
|
884
|
+
}
|
|
885
|
+
dispatch(msg) {
|
|
886
|
+
for (const handler of [...this.handlers]) {
|
|
887
|
+
try {
|
|
888
|
+
handler(msg);
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
function finitePositiveLimit(value, fallback) {
|
|
895
|
+
return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
|
|
896
|
+
}
|
|
897
|
+
|
|
792
898
|
// src/client/acp-session.ts
|
|
793
899
|
var ACPSessionError = class extends Error {
|
|
794
900
|
kind;
|
|
@@ -810,6 +916,7 @@ var ACPSession = class _ACPSession {
|
|
|
810
916
|
permissionPolicy;
|
|
811
917
|
timeoutMs;
|
|
812
918
|
opts;
|
|
919
|
+
transportOff = null;
|
|
813
920
|
state = "init";
|
|
814
921
|
sessionId = null;
|
|
815
922
|
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
@@ -841,8 +948,19 @@ var ACPSession = class _ACPSession {
|
|
|
841
948
|
if (opts.terminalOutputByteLimit !== void 0) {
|
|
842
949
|
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
843
950
|
}
|
|
951
|
+
if (opts.terminalMaxCount !== void 0) {
|
|
952
|
+
termOpts.maxTerminals = opts.terminalMaxCount;
|
|
953
|
+
}
|
|
844
954
|
this.terminalServer = new TerminalServer(termOpts);
|
|
845
|
-
|
|
955
|
+
if (opts.permissionPolicy && opts.trustBoundary) {
|
|
956
|
+
throw new TypeError("permissionPolicy and trustBoundary are mutually exclusive");
|
|
957
|
+
}
|
|
958
|
+
this.permissionPolicy = opts.trustBoundary ? makeTrustBoundaryPermissionPolicy({
|
|
959
|
+
boundary: opts.trustBoundary,
|
|
960
|
+
...opts.trustActor ? { actor: opts.trustActor } : {},
|
|
961
|
+
scope: opts.trustScope ?? { cwd: opts.projectRoot },
|
|
962
|
+
...opts.trustAuthContext ? { authContext: opts.trustAuthContext } : {}
|
|
963
|
+
}) : opts.permissionPolicy ?? readOnlyPermissionPolicy;
|
|
846
964
|
}
|
|
847
965
|
// ──────────────────────────────────────────────────────────────────────
|
|
848
966
|
// Public accessors
|
|
@@ -916,10 +1034,12 @@ var ACPSession = class _ACPSession {
|
|
|
916
1034
|
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
917
1035
|
}
|
|
918
1036
|
const session = new _ACPSession(opts, transport);
|
|
919
|
-
transport.onMessage((msg) => session.handleMessage(msg));
|
|
1037
|
+
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
920
1038
|
try {
|
|
921
1039
|
await session.initialize();
|
|
922
1040
|
} catch (err) {
|
|
1041
|
+
session.transportOff?.();
|
|
1042
|
+
session.transportOff = null;
|
|
923
1043
|
try {
|
|
924
1044
|
transport.stop();
|
|
925
1045
|
} catch {
|
|
@@ -1083,7 +1203,11 @@ var ACPSession = class _ACPSession {
|
|
|
1083
1203
|
mcpServers: servers
|
|
1084
1204
|
});
|
|
1085
1205
|
if (isJsonRpcError(result)) {
|
|
1086
|
-
throw new ACPSessionError(
|
|
1206
|
+
throw new ACPSessionError(
|
|
1207
|
+
"prompt_failed",
|
|
1208
|
+
`session/resume failed: ${result.message}`,
|
|
1209
|
+
result
|
|
1210
|
+
);
|
|
1087
1211
|
}
|
|
1088
1212
|
this.sessionId = sessionId;
|
|
1089
1213
|
}
|
|
@@ -1134,7 +1258,11 @@ var ACPSession = class _ACPSession {
|
|
|
1134
1258
|
const id = this.allocId();
|
|
1135
1259
|
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1136
1260
|
if (isJsonRpcError(result)) {
|
|
1137
|
-
throw new ACPSessionError(
|
|
1261
|
+
throw new ACPSessionError(
|
|
1262
|
+
"prompt_failed",
|
|
1263
|
+
`session/delete failed: ${result.message}`,
|
|
1264
|
+
result
|
|
1265
|
+
);
|
|
1138
1266
|
}
|
|
1139
1267
|
if (this.sessionId === sessionId) {
|
|
1140
1268
|
this.sessionId = null;
|
|
@@ -1169,7 +1297,11 @@ var ACPSession = class _ACPSession {
|
|
|
1169
1297
|
const id = this.allocId();
|
|
1170
1298
|
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
1171
1299
|
if (isJsonRpcError(result)) {
|
|
1172
|
-
throw new ACPSessionError(
|
|
1300
|
+
throw new ACPSessionError(
|
|
1301
|
+
"prompt_failed",
|
|
1302
|
+
`session/set_mode failed: ${result.message}`,
|
|
1303
|
+
result
|
|
1304
|
+
);
|
|
1173
1305
|
}
|
|
1174
1306
|
}
|
|
1175
1307
|
/**
|
|
@@ -1184,7 +1316,11 @@ var ACPSession = class _ACPSession {
|
|
|
1184
1316
|
value
|
|
1185
1317
|
});
|
|
1186
1318
|
if (isJsonRpcError(result)) {
|
|
1187
|
-
throw new ACPSessionError(
|
|
1319
|
+
throw new ACPSessionError(
|
|
1320
|
+
"prompt_failed",
|
|
1321
|
+
`session/set_config_option failed: ${result.message}`,
|
|
1322
|
+
result
|
|
1323
|
+
);
|
|
1188
1324
|
}
|
|
1189
1325
|
}
|
|
1190
1326
|
/**
|
|
@@ -1195,7 +1331,11 @@ var ACPSession = class _ACPSession {
|
|
|
1195
1331
|
const id = this.allocId();
|
|
1196
1332
|
const result = await this.sendRequest(id, "providers/list", {});
|
|
1197
1333
|
if (isJsonRpcError(result)) {
|
|
1198
|
-
throw new ACPSessionError(
|
|
1334
|
+
throw new ACPSessionError(
|
|
1335
|
+
"prompt_failed",
|
|
1336
|
+
`providers/list failed: ${result.message}`,
|
|
1337
|
+
result
|
|
1338
|
+
);
|
|
1199
1339
|
}
|
|
1200
1340
|
const r = result;
|
|
1201
1341
|
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
@@ -1231,7 +1371,11 @@ var ACPSession = class _ACPSession {
|
|
|
1231
1371
|
const id = this.allocId();
|
|
1232
1372
|
const result = await this.sendRequest(id, "providers/disable", {});
|
|
1233
1373
|
if (isJsonRpcError(result)) {
|
|
1234
|
-
throw new ACPSessionError(
|
|
1374
|
+
throw new ACPSessionError(
|
|
1375
|
+
"prompt_failed",
|
|
1376
|
+
`providers/disable failed: ${result.message}`,
|
|
1377
|
+
result
|
|
1378
|
+
);
|
|
1235
1379
|
}
|
|
1236
1380
|
}
|
|
1237
1381
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -1338,11 +1482,7 @@ var ACPSession = class _ACPSession {
|
|
|
1338
1482
|
}
|
|
1339
1483
|
const sessionId = result.sessionId;
|
|
1340
1484
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1341
|
-
throw new ACPSessionError(
|
|
1342
|
-
"protocol_error",
|
|
1343
|
-
"session/new returned no sessionId",
|
|
1344
|
-
result
|
|
1345
|
-
);
|
|
1485
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
1346
1486
|
}
|
|
1347
1487
|
this.sessionId = sessionId;
|
|
1348
1488
|
}
|
|
@@ -1385,6 +1525,8 @@ var ACPSession = class _ACPSession {
|
|
|
1385
1525
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
1386
1526
|
}
|
|
1387
1527
|
this.pending.clear();
|
|
1528
|
+
this.transportOff?.();
|
|
1529
|
+
this.transportOff = null;
|
|
1388
1530
|
try {
|
|
1389
1531
|
this.transport.stop();
|
|
1390
1532
|
} catch {
|
|
@@ -1420,10 +1562,7 @@ var ACPSession = class _ACPSession {
|
|
|
1420
1562
|
const handle = setTimeout(() => {
|
|
1421
1563
|
this.pending.delete(id);
|
|
1422
1564
|
reject(
|
|
1423
|
-
new ACPSessionError(
|
|
1424
|
-
"protocol_error",
|
|
1425
|
-
`${method} timed out after ${effectiveTimeout}ms`
|
|
1426
|
-
)
|
|
1565
|
+
new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
|
|
1427
1566
|
);
|
|
1428
1567
|
}, effectiveTimeout);
|
|
1429
1568
|
this.pending.set(id, {
|
|
@@ -1657,11 +1796,12 @@ var ACPSession = class _ACPSession {
|
|
|
1657
1796
|
* closing the gap where the agent simply skips the voluntary permission
|
|
1658
1797
|
* request and sends the privileged callback directly.
|
|
1659
1798
|
*
|
|
1660
|
-
* Uses the session's permission policy. The default
|
|
1661
|
-
* (`
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
* `
|
|
1799
|
+
* Uses the session's permission policy. The default
|
|
1800
|
+
* (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
|
|
1801
|
+
* calls (read/search/fetch/think) and rejects everything else — this is
|
|
1802
|
+
* the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
|
|
1803
|
+
* Director fan-out), inject `defaultPermissionPolicy` to grant
|
|
1804
|
+
* write/execute access.
|
|
1665
1805
|
*
|
|
1666
1806
|
* Returns true if the callback is authorized, false if denied.
|
|
1667
1807
|
*/
|
|
@@ -1673,7 +1813,8 @@ var ACPSession = class _ACPSession {
|
|
|
1673
1813
|
toolCallId: partial.toolCallId,
|
|
1674
1814
|
title: partial.title,
|
|
1675
1815
|
kind: partial.kind,
|
|
1676
|
-
status: "pending"
|
|
1816
|
+
status: "pending",
|
|
1817
|
+
...partial.rawInput ? { rawInput: partial.rawInput } : {}
|
|
1677
1818
|
},
|
|
1678
1819
|
options: [
|
|
1679
1820
|
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
@@ -1698,7 +1839,8 @@ var ACPSession = class _ACPSession {
|
|
|
1698
1839
|
const allowed = await this.authorizeCallback({
|
|
1699
1840
|
toolCallId: `acp-fs-write-${id}`,
|
|
1700
1841
|
title: `Write file: ${params.path}`,
|
|
1701
|
-
kind: "edit"
|
|
1842
|
+
kind: "edit",
|
|
1843
|
+
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
1702
1844
|
});
|
|
1703
1845
|
if (!allowed) {
|
|
1704
1846
|
await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
|
|
@@ -1736,7 +1878,13 @@ var ACPSession = class _ACPSession {
|
|
|
1736
1878
|
const allowed = await this.authorizeCallback({
|
|
1737
1879
|
toolCallId: `acp-terminal-create-${id}`,
|
|
1738
1880
|
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
1739
|
-
kind: "execute"
|
|
1881
|
+
kind: "execute",
|
|
1882
|
+
rawInput: {
|
|
1883
|
+
command: params.command,
|
|
1884
|
+
args: params.args,
|
|
1885
|
+
cwd: params.cwd,
|
|
1886
|
+
sessionId: params.sessionId
|
|
1887
|
+
}
|
|
1740
1888
|
});
|
|
1741
1889
|
if (!allowed) {
|
|
1742
1890
|
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
@@ -1963,6 +2111,71 @@ function isRetryable(kind) {
|
|
|
1963
2111
|
return false;
|
|
1964
2112
|
}
|
|
1965
2113
|
}
|
|
2114
|
+
|
|
2115
|
+
// src/client/tool-translator.ts
|
|
2116
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
2117
|
+
var DEFAULT_OPTIONS = {
|
|
2118
|
+
asyncTools: true,
|
|
2119
|
+
pollIntervalMs: 500,
|
|
2120
|
+
totalTimeoutMs: 12e4
|
|
2121
|
+
};
|
|
2122
|
+
var ToolTranslator = class {
|
|
2123
|
+
opts;
|
|
2124
|
+
pending = /* @__PURE__ */ new Map();
|
|
2125
|
+
constructor(opts = {}) {
|
|
2126
|
+
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Start listening to a transport for tool responses and cancellations.
|
|
2130
|
+
* Call this once after constructing the translator and before sending tasks.
|
|
2131
|
+
*/
|
|
2132
|
+
attachToTransport(transport) {
|
|
2133
|
+
transport.onMessage((msg) => {
|
|
2134
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
2135
|
+
const pending = this.pending.get(msg.id);
|
|
2136
|
+
if (pending) {
|
|
2137
|
+
clearTimeout(pending.timeout);
|
|
2138
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
2139
|
+
pending.resolve(msg);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
2143
|
+
const pending = this.pending.get(msg.id);
|
|
2144
|
+
if (pending) {
|
|
2145
|
+
clearTimeout(pending.timeout);
|
|
2146
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
2147
|
+
pending.reject(new Error("Call cancelled by client"));
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Send a tool call over the transport and wait for a response.
|
|
2154
|
+
* If asyncTools is true, polls for progress and resolves when the final
|
|
2155
|
+
* response arrives.
|
|
2156
|
+
*/
|
|
2157
|
+
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
2158
|
+
await transport.send({
|
|
2159
|
+
jsonrpc: "2.0",
|
|
2160
|
+
method: "tools/call",
|
|
2161
|
+
id: callId,
|
|
2162
|
+
params: { name, arguments: args }
|
|
2163
|
+
});
|
|
2164
|
+
return new Promise((resolve3, reject) => {
|
|
2165
|
+
const timeout = setTimeout(() => {
|
|
2166
|
+
this.pending.delete(callId);
|
|
2167
|
+
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
2168
|
+
}, this.opts.totalTimeoutMs);
|
|
2169
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
cancelAll() {
|
|
2173
|
+
for (const [, p] of this.pending) {
|
|
2174
|
+
clearTimeout(p.timeout);
|
|
2175
|
+
}
|
|
2176
|
+
this.pending.clear();
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
1966
2179
|
export {
|
|
1967
2180
|
ACPSession,
|
|
1968
2181
|
ACPSessionError,
|
|
@@ -1974,7 +2187,9 @@ export {
|
|
|
1974
2187
|
imageContent,
|
|
1975
2188
|
makeACPSubagentRunner,
|
|
1976
2189
|
makePermissionPolicy,
|
|
2190
|
+
makeTrustBoundaryPermissionPolicy,
|
|
1977
2191
|
readOnlyPermissionPolicy,
|
|
1978
|
-
textContent
|
|
2192
|
+
textContent,
|
|
2193
|
+
toTrustBoundaryRequest
|
|
1979
2194
|
};
|
|
1980
2195
|
//# sourceMappingURL=client.js.map
|