@wrongstack/acp 0.309.1 → 0.310.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/dist/agent/index.d.ts +9 -9
- package/dist/agent/protocol-contract.d.ts +1 -1
- package/dist/agent.js +897 -898
- package/dist/client/acp-session.d.ts +3 -3
- package/dist/client/tool-translator.d.ts +2 -2
- package/dist/client.js +1005 -1023
- package/dist/index.d.ts +2 -1
- package/dist/index.js +997 -1023
- package/dist/integration/acp-subagent-runner.d.ts +1 -1
- package/dist/integration/run-one-acp-task.d.ts +6 -0
- package/dist/registry/acp-registry-fetch.d.ts +1 -1
- package/dist/registry/agents.catalog.d.ts +1 -1
- package/dist/registry/contracts.d.ts +54 -0
- package/dist/registry/ensemble-registry.d.ts +3 -42
- package/dist/v1.js +1 -3
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -254,37 +254,9 @@ function verbatimOptions(invocation) {
|
|
|
254
254
|
// src/types/acp-v1.ts
|
|
255
255
|
var ACP_PROTOCOL_VERSION = 1;
|
|
256
256
|
|
|
257
|
-
// src/client/acp-
|
|
258
|
-
function
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
function imageContent(mimeType, data) {
|
|
262
|
-
return { type: "image", mimeType, data };
|
|
263
|
-
}
|
|
264
|
-
function audioContent(mimeType, data) {
|
|
265
|
-
return { type: "audio", mimeType, data };
|
|
266
|
-
}
|
|
267
|
-
function extractText(block) {
|
|
268
|
-
if (typeof block !== "object" || block === null) return "";
|
|
269
|
-
const b = block;
|
|
270
|
-
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
271
|
-
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
272
|
-
return b.resource.text;
|
|
273
|
-
}
|
|
274
|
-
return "";
|
|
275
|
-
}
|
|
276
|
-
function isRecord(v) {
|
|
277
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
278
|
-
}
|
|
279
|
-
function emptyRunResult(stopReason) {
|
|
280
|
-
return {
|
|
281
|
-
text: "",
|
|
282
|
-
stopReason,
|
|
283
|
-
hasText: false,
|
|
284
|
-
toolCalls: [],
|
|
285
|
-
diffs: [],
|
|
286
|
-
thoughts: ""
|
|
287
|
-
};
|
|
257
|
+
// src/client/acp-message-routing.ts
|
|
258
|
+
function isBestEffortAckMethod(method) {
|
|
259
|
+
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
288
260
|
}
|
|
289
261
|
|
|
290
262
|
// src/client/file-server.ts
|
|
@@ -469,622 +441,497 @@ function safeRealpathSync(p) {
|
|
|
469
441
|
}
|
|
470
442
|
}
|
|
471
443
|
|
|
472
|
-
// src/client/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const chosen = ranked[0];
|
|
484
|
-
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
485
|
-
return { outcome: "cancelled" };
|
|
444
|
+
// src/client/acp-session-callbacks.ts
|
|
445
|
+
var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
|
|
446
|
+
async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
|
|
447
|
+
const id = msg.id;
|
|
448
|
+
if (id === void 0) return;
|
|
449
|
+
const params = msg.params;
|
|
450
|
+
const toolCall = params?.toolCall;
|
|
451
|
+
const permissionOptions = Array.isArray(params?.options) ? params.options : [];
|
|
452
|
+
if (!toolCall) {
|
|
453
|
+
await sender.sendErrorResponse(id, -32602, "toolCall is required");
|
|
454
|
+
return;
|
|
486
455
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
502
|
-
const kind = req.toolCall.kind;
|
|
503
|
-
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
504
|
-
return pickAllow(req.options);
|
|
456
|
+
try {
|
|
457
|
+
const outcome = await runPermissionWithDeadline(
|
|
458
|
+
permissionPolicy,
|
|
459
|
+
{
|
|
460
|
+
toolCall,
|
|
461
|
+
options: permissionOptions
|
|
462
|
+
},
|
|
463
|
+
callbackOptions
|
|
464
|
+
);
|
|
465
|
+
await sender.sendResult(id, { outcome });
|
|
466
|
+
} catch (err) {
|
|
467
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
468
|
+
const code = isAbortLikeError(err) ? -32800 : -32603;
|
|
469
|
+
await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
|
|
505
470
|
}
|
|
506
|
-
return pickReject(req.options);
|
|
507
|
-
};
|
|
508
|
-
function makePermissionPolicy(decide) {
|
|
509
|
-
return async (req) => {
|
|
510
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
511
|
-
const allow = await decide(req);
|
|
512
|
-
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
513
|
-
};
|
|
514
471
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
523
|
-
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
524
|
-
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
525
|
-
var TerminalServer = class {
|
|
526
|
-
terminals = /* @__PURE__ */ new Map();
|
|
527
|
-
/**
|
|
528
|
-
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
529
|
-
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
530
|
-
* be cryptographically unique.
|
|
531
|
-
*/
|
|
532
|
-
instanceId;
|
|
533
|
-
projectRoot;
|
|
534
|
-
commandTimeoutMs;
|
|
535
|
-
outputByteLimit;
|
|
536
|
-
maxOutputByteLimit;
|
|
537
|
-
maxTerminals;
|
|
538
|
-
abortSignal;
|
|
539
|
-
abortHandler = () => this.dispose();
|
|
540
|
-
disposed = false;
|
|
541
|
-
nextId = 1;
|
|
542
|
-
constructor(opts) {
|
|
543
|
-
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
544
|
-
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
545
|
-
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
546
|
-
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
547
|
-
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
548
|
-
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
549
|
-
this.abortSignal = opts.signal;
|
|
550
|
-
if (opts.signal) {
|
|
551
|
-
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
552
|
-
}
|
|
472
|
+
async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
473
|
+
const id = msg.id;
|
|
474
|
+
if (id === void 0) return;
|
|
475
|
+
const params = msg.params;
|
|
476
|
+
if (!params?.path) {
|
|
477
|
+
await sender.sendErrorResponse(id, -32602, "path is required");
|
|
478
|
+
return;
|
|
553
479
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
480
|
+
if (msg.method === "fs/write_text_file") {
|
|
481
|
+
const authorization = await authorizeAcpCallback(
|
|
482
|
+
permissionPolicy,
|
|
483
|
+
{
|
|
484
|
+
toolCallId: `acp-fs-write-${id}`,
|
|
485
|
+
title: `Write file: ${params.path}`,
|
|
486
|
+
kind: "edit",
|
|
487
|
+
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
488
|
+
},
|
|
489
|
+
callbackOptions
|
|
490
|
+
);
|
|
491
|
+
if (authorization !== "allowed") {
|
|
492
|
+
const isCancelled = authorization === "cancelled";
|
|
493
|
+
await sender.sendErrorResponse(
|
|
494
|
+
id,
|
|
495
|
+
isCancelled ? -32800 : -32602,
|
|
496
|
+
isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
|
|
559
497
|
);
|
|
498
|
+
return;
|
|
560
499
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
if (msg.method === "fs/read_text_file") {
|
|
503
|
+
const result = await fileServer.readTextFile({
|
|
504
|
+
sessionId: params.sessionId ?? "",
|
|
505
|
+
path: params.path
|
|
506
|
+
});
|
|
507
|
+
await sender.sendResult(id, result);
|
|
508
|
+
} else {
|
|
509
|
+
await fileServer.writeTextFile({
|
|
510
|
+
sessionId: params.sessionId ?? "",
|
|
511
|
+
path: params.path,
|
|
512
|
+
content: params.content ?? ""
|
|
513
|
+
});
|
|
514
|
+
await sender.sendResult(id, {});
|
|
565
515
|
}
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const exitStatus = {
|
|
603
|
-
exitCode: typeof code === "number" ? code : null,
|
|
604
|
-
signal: typeof signalName === "string" ? signalName : null
|
|
605
|
-
};
|
|
606
|
-
state.exitStatus = exitStatus;
|
|
607
|
-
resolve3(exitStatus);
|
|
608
|
-
});
|
|
609
|
-
proc.on("error", (err) => {
|
|
610
|
-
if (state.timeoutHandle) {
|
|
611
|
-
clearTimeout(state.timeoutHandle);
|
|
612
|
-
state.timeoutHandle = null;
|
|
613
|
-
}
|
|
614
|
-
const exitStatus = { exitCode: 127, signal: null };
|
|
615
|
-
state.exitStatus = exitStatus;
|
|
616
|
-
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
617
|
-
`, "utf8");
|
|
618
|
-
if (errorOutput.length > perCallByteLimit) {
|
|
619
|
-
let start = errorOutput.length - perCallByteLimit;
|
|
620
|
-
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
621
|
-
errorOutput = errorOutput.subarray(start);
|
|
622
|
-
state.truncated = true;
|
|
623
|
-
}
|
|
624
|
-
state.outputChunks.push(errorOutput);
|
|
625
|
-
state.retainedBytes = errorOutput.length;
|
|
626
|
-
resolve3(exitStatus);
|
|
627
|
-
});
|
|
628
|
-
})
|
|
629
|
-
};
|
|
630
|
-
proc.stdout?.setEncoding("utf8");
|
|
631
|
-
proc.stderr?.setEncoding("utf8");
|
|
632
|
-
const onData = (chunk) => {
|
|
633
|
-
const outputChunk = Buffer.from(chunk, "utf8");
|
|
634
|
-
state.outputChunks.push(outputChunk);
|
|
635
|
-
state.retainedBytes += outputChunk.length;
|
|
636
|
-
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
637
|
-
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
638
|
-
const first = state.outputChunks[state.outputHead];
|
|
639
|
-
const overflow = state.retainedBytes - perCallByteLimit;
|
|
640
|
-
if (first.length <= overflow) {
|
|
641
|
-
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
642
|
-
state.outputHead++;
|
|
643
|
-
state.retainedBytes -= first.length;
|
|
644
|
-
continue;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
518
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
519
|
+
await sender.sendErrorResponse(id, code, message);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
523
|
+
const id = msg.id;
|
|
524
|
+
if (id === void 0) return;
|
|
525
|
+
const params = msg.params ?? {};
|
|
526
|
+
try {
|
|
527
|
+
switch (msg.method) {
|
|
528
|
+
case "terminal/create": {
|
|
529
|
+
const authorization = await authorizeAcpCallback(
|
|
530
|
+
permissionPolicy,
|
|
531
|
+
{
|
|
532
|
+
toolCallId: `acp-terminal-create-${id}`,
|
|
533
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
534
|
+
kind: "execute",
|
|
535
|
+
rawInput: {
|
|
536
|
+
command: params.command,
|
|
537
|
+
args: params.args,
|
|
538
|
+
cwd: params.cwd,
|
|
539
|
+
sessionId: params.sessionId
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
callbackOptions
|
|
543
|
+
);
|
|
544
|
+
if (authorization !== "allowed") {
|
|
545
|
+
const isCancelled = authorization === "cancelled";
|
|
546
|
+
await sender.sendErrorResponse(
|
|
547
|
+
id,
|
|
548
|
+
isCancelled ? -32800 : -32602,
|
|
549
|
+
isCancelled ? "terminal create permission request cancelled or timed out" : "terminal create denied by permission policy"
|
|
550
|
+
);
|
|
551
|
+
return;
|
|
645
552
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
553
|
+
const createOpts = {
|
|
554
|
+
sessionId: String(params.sessionId ?? ""),
|
|
555
|
+
command: String(params.command ?? ""),
|
|
556
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
557
|
+
};
|
|
558
|
+
if (Array.isArray(params.env)) {
|
|
559
|
+
createOpts.env = params.env;
|
|
560
|
+
}
|
|
561
|
+
if (typeof params.cwd === "string") {
|
|
562
|
+
createOpts.cwd = params.cwd;
|
|
563
|
+
}
|
|
564
|
+
if (typeof params.outputByteLimit === "number") {
|
|
565
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
566
|
+
}
|
|
567
|
+
const result = terminalServer.create(createOpts);
|
|
568
|
+
await sender.sendResult(id, result);
|
|
569
|
+
return;
|
|
650
570
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
571
|
+
case "terminal/output": {
|
|
572
|
+
const terminalId = String(params.terminalId ?? "");
|
|
573
|
+
const out = terminalServer.output(terminalId);
|
|
574
|
+
await sender.sendResult(id, out);
|
|
575
|
+
return;
|
|
654
576
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
577
|
+
case "terminal/wait_for_exit": {
|
|
578
|
+
const terminalId = String(params.terminalId ?? "");
|
|
579
|
+
const exit = await terminalServer.waitForExit(terminalId);
|
|
580
|
+
await sender.sendResult(id, exit);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
case "terminal/kill": {
|
|
584
|
+
const terminalId = String(params.terminalId ?? "");
|
|
585
|
+
terminalServer.kill(terminalId);
|
|
586
|
+
await sender.sendResult(id, {});
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
case "terminal/release": {
|
|
590
|
+
const terminalId = String(params.terminalId ?? "");
|
|
591
|
+
terminalServer.release(terminalId);
|
|
592
|
+
await sender.sendResult(id, {});
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
default:
|
|
596
|
+
await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
597
|
+
}
|
|
598
|
+
} catch (err) {
|
|
599
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
600
|
+
await sender.sendErrorResponse(id, -32603, message);
|
|
664
601
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
602
|
+
}
|
|
603
|
+
async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
|
|
604
|
+
try {
|
|
605
|
+
const outcome = await runPermissionWithDeadline(
|
|
606
|
+
permissionPolicy,
|
|
607
|
+
{
|
|
608
|
+
toolCall: {
|
|
609
|
+
sessionUpdate: "tool_call_update",
|
|
610
|
+
toolCallId: partial.toolCallId,
|
|
611
|
+
title: partial.title,
|
|
612
|
+
kind: partial.kind,
|
|
613
|
+
status: "pending",
|
|
614
|
+
...partial.rawInput ? { rawInput: partial.rawInput } : {}
|
|
615
|
+
},
|
|
616
|
+
options: [
|
|
617
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
618
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
callbackOptions
|
|
622
|
+
);
|
|
623
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return isAbortLikeError(err) ? "cancelled" : "denied";
|
|
677
626
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
627
|
+
}
|
|
628
|
+
async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
|
|
629
|
+
const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
|
|
630
|
+
const controller = new AbortController();
|
|
631
|
+
const abort = () => controller.abort();
|
|
632
|
+
const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
|
|
633
|
+
let removeAbort;
|
|
634
|
+
if (callbackOptions.signal) {
|
|
635
|
+
if (callbackOptions.signal.aborted) {
|
|
636
|
+
abort();
|
|
637
|
+
} else {
|
|
638
|
+
callbackOptions.signal.addEventListener("abort", abort, { once: true });
|
|
639
|
+
removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
|
|
640
|
+
}
|
|
683
641
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
642
|
+
try {
|
|
643
|
+
return await Promise.race([
|
|
644
|
+
permissionPolicy({
|
|
645
|
+
toolCall: call.toolCall,
|
|
646
|
+
options: call.options,
|
|
647
|
+
signal: controller.signal
|
|
648
|
+
}),
|
|
649
|
+
rejectOnAbort(controller.signal)
|
|
650
|
+
]);
|
|
651
|
+
} finally {
|
|
652
|
+
if (timer !== null) clearTimeout(timer);
|
|
653
|
+
removeAbort?.();
|
|
693
654
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
if (state.onData) {
|
|
703
|
-
state.proc.stdout?.off("data", state.onData);
|
|
704
|
-
state.proc.stderr?.off("data", state.onData);
|
|
705
|
-
state.onData = void 0;
|
|
655
|
+
}
|
|
656
|
+
function rejectOnAbort(signal) {
|
|
657
|
+
return new Promise((_, reject) => {
|
|
658
|
+
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
659
|
+
if (signal.aborted) {
|
|
660
|
+
rejectAbort();
|
|
661
|
+
return;
|
|
706
662
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
663
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function isAbortLikeError(err) {
|
|
667
|
+
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
668
|
+
}
|
|
669
|
+
function resolvePermissionDeadline(value) {
|
|
670
|
+
if (value === Number.POSITIVE_INFINITY) return null;
|
|
671
|
+
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
672
|
+
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/client/acp-session-content.ts
|
|
676
|
+
function textContent(text) {
|
|
677
|
+
return { type: "text", text };
|
|
678
|
+
}
|
|
679
|
+
function imageContent(mimeType, data) {
|
|
680
|
+
return { type: "image", mimeType, data };
|
|
681
|
+
}
|
|
682
|
+
function audioContent(mimeType, data) {
|
|
683
|
+
return { type: "audio", mimeType, data };
|
|
684
|
+
}
|
|
685
|
+
function extractText(block) {
|
|
686
|
+
if (typeof block !== "object" || block === null) return "";
|
|
687
|
+
const b = block;
|
|
688
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
689
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
690
|
+
return b.resource.text;
|
|
714
691
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
741
|
-
})
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
this.disposed = true;
|
|
745
|
-
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
746
|
-
for (const id of [...this.terminals.keys()]) {
|
|
747
|
-
this.release(id);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
751
|
-
[Symbol.dispose]() {
|
|
752
|
-
this.dispose();
|
|
692
|
+
return "";
|
|
693
|
+
}
|
|
694
|
+
function isRecord(v) {
|
|
695
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
696
|
+
}
|
|
697
|
+
function emptyRunResult(stopReason) {
|
|
698
|
+
return {
|
|
699
|
+
text: "",
|
|
700
|
+
stopReason,
|
|
701
|
+
hasText: false,
|
|
702
|
+
toolCalls: [],
|
|
703
|
+
diffs: [],
|
|
704
|
+
thoughts: ""
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/client/acp-session-errors.ts
|
|
709
|
+
var ACPSessionError = class extends Error {
|
|
710
|
+
kind;
|
|
711
|
+
cause;
|
|
712
|
+
constructor(kind, message, cause) {
|
|
713
|
+
super(message);
|
|
714
|
+
this.name = "ACPSessionError";
|
|
715
|
+
this.kind = kind;
|
|
716
|
+
this.cause = cause;
|
|
753
717
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
718
|
+
};
|
|
719
|
+
function isJsonRpcError(v) {
|
|
720
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/client/acp-session-ops.ts
|
|
724
|
+
function filterMcpServers(agentCapabilities, servers) {
|
|
725
|
+
if (!servers || servers.length === 0) return [];
|
|
726
|
+
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
727
|
+
return servers.filter((s) => {
|
|
728
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
729
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
730
|
+
return true;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
734
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
735
|
+
if (!ctx.agentCapabilities.loadSession) {
|
|
736
|
+
throw new ACPSessionError(
|
|
737
|
+
"unsupported_capability",
|
|
738
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
739
|
+
);
|
|
764
740
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const resolved = path2.resolve(cwd);
|
|
768
|
-
const rootWithSep = this.projectRoot.endsWith(path2.sep) ? this.projectRoot : this.projectRoot + path2.sep;
|
|
769
|
-
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
770
|
-
return this.projectRoot;
|
|
771
|
-
}
|
|
772
|
-
try {
|
|
773
|
-
const realRoot = realpathSync2(this.projectRoot);
|
|
774
|
-
const realCwd = realpathSync2(resolved);
|
|
775
|
-
const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
|
|
776
|
-
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
777
|
-
return realRoot;
|
|
778
|
-
}
|
|
779
|
-
return realCwd;
|
|
780
|
-
} catch {
|
|
781
|
-
return this.projectRoot;
|
|
782
|
-
}
|
|
741
|
+
if (ctx.sessionId) {
|
|
742
|
+
await ctx.closeSession();
|
|
783
743
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
744
|
+
ctx.resetScratch();
|
|
745
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
746
|
+
const id = ctx.allocId();
|
|
747
|
+
const result = await ctx.sendRequest(id, "session/load", {
|
|
748
|
+
sessionId,
|
|
749
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
750
|
+
mcpServers: servers
|
|
751
|
+
});
|
|
752
|
+
if (isJsonRpcError(result)) {
|
|
753
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
794
754
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
return Math.trunc(value);
|
|
755
|
+
ctx.setSessionId(sessionId);
|
|
756
|
+
}
|
|
757
|
+
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
758
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
759
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
760
|
+
throw new ACPSessionError(
|
|
761
|
+
"unsupported_capability",
|
|
762
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
763
|
+
);
|
|
806
764
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
"NODE_OPTIONS",
|
|
810
|
-
"LD_PRELOAD",
|
|
811
|
-
"LD_LIBRARY_PATH",
|
|
812
|
-
"DYLD_INSERT_LIBRARIES",
|
|
813
|
-
"DYLD_LIBRARY_PATH",
|
|
814
|
-
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
815
|
-
"PATH",
|
|
816
|
-
"PYTHONPATH",
|
|
817
|
-
"PYTHONSTARTUP",
|
|
818
|
-
"PERL5OPT",
|
|
819
|
-
"PERLLIB",
|
|
820
|
-
"RUBYOPT",
|
|
821
|
-
"RUBYLIB"
|
|
822
|
-
]);
|
|
823
|
-
|
|
824
|
-
// src/client/trust-boundary-permission.ts
|
|
825
|
-
function pickOption(options, allowed) {
|
|
826
|
-
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
827
|
-
for (const kind of kinds) {
|
|
828
|
-
const option = options.find((candidate) => candidate.kind === kind);
|
|
829
|
-
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
765
|
+
if (ctx.sessionId) {
|
|
766
|
+
await ctx.closeSession();
|
|
830
767
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const raw = request.toolCall.rawInput;
|
|
841
|
-
if (typeof raw?.path === "string") {
|
|
842
|
-
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
768
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
769
|
+
const id = ctx.allocId();
|
|
770
|
+
const result = await ctx.sendRequest(id, "session/resume", {
|
|
771
|
+
sessionId,
|
|
772
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
773
|
+
mcpServers: servers
|
|
774
|
+
});
|
|
775
|
+
if (isJsonRpcError(result)) {
|
|
776
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
843
777
|
}
|
|
844
|
-
|
|
845
|
-
return "process.spawn";
|
|
846
|
-
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
847
|
-
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
778
|
+
ctx.setSessionId(sessionId);
|
|
848
779
|
}
|
|
849
|
-
function
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
780
|
+
async function executeListSessions(ctx, cursor, cwd) {
|
|
781
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
782
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
783
|
+
throw new ACPSessionError(
|
|
784
|
+
"unsupported_capability",
|
|
785
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
786
|
+
);
|
|
854
787
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
788
|
+
const id = ctx.allocId();
|
|
789
|
+
const params = {};
|
|
790
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
791
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
792
|
+
const result = await ctx.sendRequest(id, "session/list", params);
|
|
793
|
+
if (isJsonRpcError(result)) {
|
|
794
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
861
795
|
}
|
|
796
|
+
const r = result;
|
|
862
797
|
return {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
798
|
+
sessions: r.sessions ?? [],
|
|
799
|
+
nextCursor: r.nextCursor
|
|
866
800
|
};
|
|
867
801
|
}
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
risk: riskFor(request.toolCall.kind),
|
|
885
|
-
scope: {
|
|
886
|
-
...options.scope ?? {},
|
|
887
|
-
...sessionId ? { sessionId } : {}
|
|
888
|
-
},
|
|
889
|
-
...options.authContext ? { authContext: options.authContext } : {},
|
|
890
|
-
metadata: {
|
|
891
|
-
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
892
|
-
toolKind: request.toolCall.kind ?? null
|
|
893
|
-
}
|
|
894
|
-
};
|
|
802
|
+
async function executeDeleteSession(ctx, sessionId) {
|
|
803
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
804
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
805
|
+
throw new ACPSessionError(
|
|
806
|
+
"unsupported_capability",
|
|
807
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const id = ctx.allocId();
|
|
811
|
+
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
812
|
+
if (isJsonRpcError(result)) {
|
|
813
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
814
|
+
}
|
|
815
|
+
if (ctx.sessionId === sessionId) {
|
|
816
|
+
ctx.setSessionId(null);
|
|
817
|
+
}
|
|
895
818
|
}
|
|
896
|
-
function
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
ws = null;
|
|
908
|
-
handlers = /* @__PURE__ */ new Set();
|
|
909
|
-
closed = false;
|
|
910
|
-
opts;
|
|
911
|
-
maxBufferedBytes;
|
|
912
|
-
maxMessageChars;
|
|
913
|
-
constructor(opts) {
|
|
914
|
-
this.opts = opts;
|
|
915
|
-
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
916
|
-
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
917
|
-
}
|
|
918
|
-
/** Pending start() promise resolve/reject — settled in stop() to avoid leaking. */
|
|
919
|
-
pendingStart = null;
|
|
920
|
-
start() {
|
|
921
|
-
if (this.closed || this.ws !== null || this.pendingStart !== null) {
|
|
922
|
-
return Promise.reject(new Error("WebSocket transport has already been started or stopped"));
|
|
923
|
-
}
|
|
924
|
-
const WS = globalThis.WebSocket;
|
|
925
|
-
if (!WS) {
|
|
926
|
-
return Promise.reject(
|
|
927
|
-
new Error(
|
|
928
|
-
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
929
|
-
)
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
933
|
-
return new Promise((resolve3, reject) => {
|
|
934
|
-
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
935
|
-
this.ws = ws;
|
|
936
|
-
const timer = setTimeout(() => {
|
|
937
|
-
const pending = this.pendingStart;
|
|
938
|
-
if (pending === null) return;
|
|
939
|
-
this.pendingStart = null;
|
|
940
|
-
this.closed = true;
|
|
941
|
-
if (this.ws === ws) this.ws = null;
|
|
942
|
-
this.handlers.clear();
|
|
943
|
-
try {
|
|
944
|
-
ws.close();
|
|
945
|
-
} catch {
|
|
946
|
-
}
|
|
947
|
-
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
948
|
-
}, timeoutMs);
|
|
949
|
-
this.pendingStart = { resolve: resolve3, reject, timer };
|
|
950
|
-
ws.addEventListener("open", () => {
|
|
951
|
-
const pending = this.pendingStart;
|
|
952
|
-
if (pending === null) return;
|
|
953
|
-
this.pendingStart = null;
|
|
954
|
-
clearTimeout(pending.timer);
|
|
955
|
-
pending.resolve();
|
|
956
|
-
});
|
|
957
|
-
ws.addEventListener("error", (ev) => {
|
|
958
|
-
const pending = this.pendingStart;
|
|
959
|
-
if (pending === null) {
|
|
960
|
-
this.stop();
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
this.pendingStart = null;
|
|
964
|
-
this.closed = true;
|
|
965
|
-
if (this.ws === ws) this.ws = null;
|
|
966
|
-
this.handlers.clear();
|
|
967
|
-
clearTimeout(pending.timer);
|
|
968
|
-
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
969
|
-
pending.reject(new Error(message));
|
|
970
|
-
});
|
|
971
|
-
ws.addEventListener("close", () => {
|
|
972
|
-
this.closed = true;
|
|
973
|
-
if (this.ws === ws) this.ws = null;
|
|
974
|
-
this.handlers.clear();
|
|
975
|
-
const pending = this.pendingStart;
|
|
976
|
-
if (pending !== null) {
|
|
977
|
-
this.pendingStart = null;
|
|
978
|
-
clearTimeout(pending.timer);
|
|
979
|
-
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
ws.addEventListener("message", (ev) => {
|
|
983
|
-
this.onData(ev.data);
|
|
984
|
-
});
|
|
985
|
-
});
|
|
819
|
+
async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
|
|
820
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
821
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
822
|
+
const id = ctx.allocId();
|
|
823
|
+
const result = await ctx.sendRequest(id, "session/fork", {
|
|
824
|
+
sessionId: sourceSessionId,
|
|
825
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
826
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
827
|
+
});
|
|
828
|
+
if (isJsonRpcError(result)) {
|
|
829
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
986
830
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
try {
|
|
992
|
-
const serialized = JSON.stringify(msg);
|
|
993
|
-
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
|
|
994
|
-
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
995
|
-
this.stop();
|
|
996
|
-
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
997
|
-
}
|
|
998
|
-
this.ws.send(serialized);
|
|
999
|
-
return Promise.resolve();
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1002
|
-
}
|
|
831
|
+
const newId = result.sessionId;
|
|
832
|
+
if (typeof newId !== "string" || !newId) {
|
|
833
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
1003
834
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
835
|
+
return newId;
|
|
836
|
+
}
|
|
837
|
+
async function executeSetMode(ctx, sessionId, modeId) {
|
|
838
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
839
|
+
const id = ctx.allocId();
|
|
840
|
+
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
841
|
+
if (isJsonRpcError(result)) {
|
|
842
|
+
throw new ACPSessionError(
|
|
843
|
+
"prompt_failed",
|
|
844
|
+
`session/set_mode failed: ${result.message}`,
|
|
845
|
+
result
|
|
846
|
+
);
|
|
1007
847
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
} catch {
|
|
1024
|
-
}
|
|
1025
|
-
this.ws = null;
|
|
1026
|
-
}
|
|
848
|
+
}
|
|
849
|
+
async function executeSetConfigOption(ctx, sessionId, configId, value) {
|
|
850
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
851
|
+
const id = ctx.allocId();
|
|
852
|
+
const result = await ctx.sendRequest(id, "session/set_config_option", {
|
|
853
|
+
sessionId,
|
|
854
|
+
configId,
|
|
855
|
+
value
|
|
856
|
+
});
|
|
857
|
+
if (isJsonRpcError(result)) {
|
|
858
|
+
throw new ACPSessionError(
|
|
859
|
+
"prompt_failed",
|
|
860
|
+
`session/set_config_option failed: ${result.message}`,
|
|
861
|
+
result
|
|
862
|
+
);
|
|
1027
863
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
let msg;
|
|
1036
|
-
try {
|
|
1037
|
-
msg = JSON.parse(text);
|
|
1038
|
-
} catch {
|
|
1039
|
-
for (const line of text.split("\n")) {
|
|
1040
|
-
if (!line.trim()) continue;
|
|
1041
|
-
try {
|
|
1042
|
-
this.dispatch(JSON.parse(line));
|
|
1043
|
-
} catch {
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
this.dispatch(msg);
|
|
864
|
+
}
|
|
865
|
+
async function executeListProviders(ctx) {
|
|
866
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
867
|
+
const id = ctx.allocId();
|
|
868
|
+
const result = await ctx.sendRequest(id, "providers/list", {});
|
|
869
|
+
if (isJsonRpcError(result)) {
|
|
870
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
1049
871
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
872
|
+
const r = result;
|
|
873
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
874
|
+
}
|
|
875
|
+
async function executeSetProvider(ctx, providerId, config) {
|
|
876
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
877
|
+
const id = ctx.allocId();
|
|
878
|
+
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
879
|
+
if (isJsonRpcError(result)) {
|
|
880
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
1057
881
|
}
|
|
1058
|
-
};
|
|
1059
|
-
function finitePositiveLimit(value, fallback) {
|
|
1060
|
-
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1061
882
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
883
|
+
async function executeDisableProvider(ctx) {
|
|
884
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
885
|
+
const id = ctx.allocId();
|
|
886
|
+
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
887
|
+
if (isJsonRpcError(result)) {
|
|
888
|
+
throw new ACPSessionError(
|
|
889
|
+
"prompt_failed",
|
|
890
|
+
`providers/disable failed: ${result.message}`,
|
|
891
|
+
result
|
|
892
|
+
);
|
|
1072
893
|
}
|
|
1073
|
-
};
|
|
1074
|
-
function isJsonRpcError(v) {
|
|
1075
|
-
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
1076
894
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
895
|
+
async function executeMcpMessage(ctx, connectionId, message) {
|
|
896
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
897
|
+
const id = ctx.allocId();
|
|
898
|
+
const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
|
|
899
|
+
if (isJsonRpcError(result)) {
|
|
900
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
1081
903
|
}
|
|
1082
|
-
function
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
904
|
+
async function executeCreateSession(ctx) {
|
|
905
|
+
const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
|
|
906
|
+
const id = ctx.allocId();
|
|
907
|
+
const result = await ctx.sendRequest(id, "session/new", {
|
|
908
|
+
cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
909
|
+
mcpServers: servers
|
|
910
|
+
});
|
|
911
|
+
if (isJsonRpcError(result)) {
|
|
912
|
+
throw new ACPSessionError(
|
|
913
|
+
"session_create_failed",
|
|
914
|
+
`session/new failed: ${result.message}`,
|
|
915
|
+
result
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const sessionId = result.sessionId;
|
|
919
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
920
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
921
|
+
}
|
|
922
|
+
return sessionId;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/client/acp-session-updates.ts
|
|
926
|
+
function createSessionScratch() {
|
|
927
|
+
return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
928
|
+
}
|
|
929
|
+
function handleAcpSessionUpdate(msg, scratch, emitProgress) {
|
|
930
|
+
const update = msg.params?.update;
|
|
931
|
+
if (typeof update !== "object" || update === null) return;
|
|
932
|
+
const u = update;
|
|
933
|
+
emitProgress({ type: "raw", update: u });
|
|
934
|
+
switch (u.sessionUpdate) {
|
|
1088
935
|
case "agent_message_chunk": {
|
|
1089
936
|
const text = extractText(u.content);
|
|
1090
937
|
if (text) {
|
|
@@ -1166,454 +1013,593 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
|
|
|
1166
1013
|
});
|
|
1167
1014
|
}
|
|
1168
1015
|
|
|
1169
|
-
// src/client/
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1016
|
+
// src/client/permission.ts
|
|
1017
|
+
function pickAllow(options) {
|
|
1018
|
+
const ranked = [...options].sort((a, b) => {
|
|
1019
|
+
const score = (k) => {
|
|
1020
|
+
if (k === "allow_once") return 0;
|
|
1021
|
+
if (k === "allow_always") return 1;
|
|
1022
|
+
if (k === "reject_once") return 2;
|
|
1023
|
+
return 3;
|
|
1024
|
+
};
|
|
1025
|
+
return score(a.kind) - score(b.kind);
|
|
1026
|
+
});
|
|
1027
|
+
const chosen = ranked[0];
|
|
1028
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1029
|
+
return { outcome: "cancelled" };
|
|
1180
1030
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1031
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
1032
|
+
}
|
|
1033
|
+
function pickReject(options) {
|
|
1034
|
+
const reject = options.find((o) => o.kind === "reject_once" || o.kind === "reject_always");
|
|
1035
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
1036
|
+
}
|
|
1037
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
1038
|
+
var defaultPermissionPolicy = async (req) => {
|
|
1039
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1040
|
+
return pickAllow(req.options);
|
|
1041
|
+
};
|
|
1042
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
1043
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1044
|
+
const kind = req.toolCall.kind;
|
|
1045
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1046
|
+
return pickAllow(req.options);
|
|
1195
1047
|
}
|
|
1048
|
+
return pickReject(req.options);
|
|
1049
|
+
};
|
|
1050
|
+
function makePermissionPolicy(decide) {
|
|
1051
|
+
return async (req) => {
|
|
1052
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1053
|
+
const allow = await decide(req);
|
|
1054
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1055
|
+
};
|
|
1196
1056
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1057
|
+
|
|
1058
|
+
// src/client/terminal-server.ts
|
|
1059
|
+
import { spawn } from "node:child_process";
|
|
1060
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1061
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1062
|
+
import * as path2 from "node:path";
|
|
1063
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1064
|
+
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1065
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1066
|
+
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
1067
|
+
var TerminalServer = class {
|
|
1068
|
+
terminals = /* @__PURE__ */ new Map();
|
|
1069
|
+
/**
|
|
1070
|
+
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
1071
|
+
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
1072
|
+
* be cryptographically unique.
|
|
1073
|
+
*/
|
|
1074
|
+
instanceId;
|
|
1075
|
+
projectRoot;
|
|
1076
|
+
commandTimeoutMs;
|
|
1077
|
+
outputByteLimit;
|
|
1078
|
+
maxOutputByteLimit;
|
|
1079
|
+
maxTerminals;
|
|
1080
|
+
abortSignal;
|
|
1081
|
+
abortHandler = () => this.dispose();
|
|
1082
|
+
disposed = false;
|
|
1083
|
+
nextId = 1;
|
|
1084
|
+
constructor(opts) {
|
|
1085
|
+
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
1086
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1087
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1088
|
+
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1089
|
+
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
1090
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1091
|
+
this.abortSignal = opts.signal;
|
|
1092
|
+
if (opts.signal) {
|
|
1093
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1094
|
+
}
|
|
1204
1095
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
title: `Write file: ${params.path}`,
|
|
1211
|
-
kind: "edit",
|
|
1212
|
-
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
1213
|
-
},
|
|
1214
|
-
callbackOptions
|
|
1215
|
-
);
|
|
1216
|
-
if (authorization !== "allowed") {
|
|
1217
|
-
const isCancelled = authorization === "cancelled";
|
|
1218
|
-
await sender.sendErrorResponse(
|
|
1219
|
-
id,
|
|
1220
|
-
isCancelled ? -32800 : -32602,
|
|
1221
|
-
isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
|
|
1096
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
1097
|
+
create(params) {
|
|
1098
|
+
if (this.disposed) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
"TerminalServer is disposed \u2014 create a new TerminalServer instead of reusing this one"
|
|
1222
1101
|
);
|
|
1223
|
-
return;
|
|
1224
1102
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
sessionId: params.sessionId ?? "",
|
|
1230
|
-
path: params.path
|
|
1231
|
-
});
|
|
1232
|
-
await sender.sendResult(id, result);
|
|
1233
|
-
} else {
|
|
1234
|
-
await fileServer.writeTextFile({
|
|
1235
|
-
sessionId: params.sessionId ?? "",
|
|
1236
|
-
path: params.path,
|
|
1237
|
-
content: params.content ?? ""
|
|
1238
|
-
});
|
|
1239
|
-
await sender.sendResult(id, {});
|
|
1103
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
1106
|
+
);
|
|
1240
1107
|
}
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1108
|
+
const id = `term_${this.nextId++}`;
|
|
1109
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
1110
|
+
const perCallByteLimit = Math.min(
|
|
1111
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1112
|
+
this.maxOutputByteLimit
|
|
1113
|
+
);
|
|
1114
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
1115
|
+
cwd,
|
|
1116
|
+
env: this.buildEnv(params.env),
|
|
1117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1118
|
+
windowsHide: true
|
|
1119
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
1120
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
1121
|
+
// the command a single shell-parsed string, which breaks
|
|
1122
|
+
// Windows cmd quoting for the common case of running node with
|
|
1123
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
1124
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
1125
|
+
// default.
|
|
1126
|
+
});
|
|
1127
|
+
const state = {
|
|
1128
|
+
proc,
|
|
1129
|
+
cwd,
|
|
1130
|
+
command: params.command,
|
|
1131
|
+
args: params.args ?? [],
|
|
1132
|
+
outputChunks: [],
|
|
1133
|
+
outputHead: 0,
|
|
1134
|
+
retainedBytes: 0,
|
|
1135
|
+
truncated: false,
|
|
1136
|
+
exitStatus: void 0,
|
|
1137
|
+
timeoutHandle: null,
|
|
1138
|
+
exitPromise: new Promise((resolve3) => {
|
|
1139
|
+
proc.on("close", (code, signalName) => {
|
|
1140
|
+
if (state.timeoutHandle) {
|
|
1141
|
+
clearTimeout(state.timeoutHandle);
|
|
1142
|
+
state.timeoutHandle = null;
|
|
1143
|
+
}
|
|
1144
|
+
const exitStatus = {
|
|
1145
|
+
exitCode: typeof code === "number" ? code : null,
|
|
1146
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
1147
|
+
};
|
|
1148
|
+
state.exitStatus = exitStatus;
|
|
1149
|
+
resolve3(exitStatus);
|
|
1150
|
+
});
|
|
1151
|
+
proc.on("error", (err) => {
|
|
1152
|
+
if (state.timeoutHandle) {
|
|
1153
|
+
clearTimeout(state.timeoutHandle);
|
|
1154
|
+
state.timeoutHandle = null;
|
|
1155
|
+
}
|
|
1156
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
1157
|
+
state.exitStatus = exitStatus;
|
|
1158
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
1159
|
+
`, "utf8");
|
|
1160
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
1161
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
1162
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
1163
|
+
errorOutput = errorOutput.subarray(start);
|
|
1164
|
+
state.truncated = true;
|
|
1165
|
+
}
|
|
1166
|
+
state.outputChunks.push(errorOutput);
|
|
1167
|
+
state.retainedBytes = errorOutput.length;
|
|
1168
|
+
resolve3(exitStatus);
|
|
1169
|
+
});
|
|
1170
|
+
})
|
|
1171
|
+
};
|
|
1172
|
+
proc.stdout?.setEncoding("utf8");
|
|
1173
|
+
proc.stderr?.setEncoding("utf8");
|
|
1174
|
+
const onData = (chunk) => {
|
|
1175
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
1176
|
+
state.outputChunks.push(outputChunk);
|
|
1177
|
+
state.retainedBytes += outputChunk.length;
|
|
1178
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
1179
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
1180
|
+
const first = state.outputChunks[state.outputHead];
|
|
1181
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
1182
|
+
if (first.length <= overflow) {
|
|
1183
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
1184
|
+
state.outputHead++;
|
|
1185
|
+
state.retainedBytes -= first.length;
|
|
1186
|
+
continue;
|
|
1291
1187
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
case "terminal/output": {
|
|
1297
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1298
|
-
const out = terminalServer.output(terminalId);
|
|
1299
|
-
await sender.sendResult(id, out);
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
case "terminal/wait_for_exit": {
|
|
1303
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1304
|
-
const exit = await terminalServer.waitForExit(terminalId);
|
|
1305
|
-
await sender.sendResult(id, exit);
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
case "terminal/kill": {
|
|
1309
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1310
|
-
terminalServer.kill(terminalId);
|
|
1311
|
-
await sender.sendResult(id, {});
|
|
1312
|
-
return;
|
|
1188
|
+
let start = overflow;
|
|
1189
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
1190
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
1191
|
+
state.retainedBytes -= start;
|
|
1313
1192
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
await sender.sendResult(id, {});
|
|
1318
|
-
return;
|
|
1193
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
1194
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
1195
|
+
state.outputHead = 0;
|
|
1319
1196
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1197
|
+
};
|
|
1198
|
+
state.onData = onData;
|
|
1199
|
+
proc.stdout?.on("data", onData);
|
|
1200
|
+
proc.stderr?.on("data", onData);
|
|
1201
|
+
state.timeoutHandle = setTimeout(() => {
|
|
1202
|
+
treeKill2(proc);
|
|
1203
|
+
}, this.commandTimeoutMs);
|
|
1204
|
+
this.terminals.set(id, state);
|
|
1205
|
+
return { terminalId: id };
|
|
1206
|
+
}
|
|
1207
|
+
/** Return captured output and (if available) the exit status. */
|
|
1208
|
+
output(terminalId) {
|
|
1209
|
+
const state = this.terminals.get(terminalId);
|
|
1210
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1211
|
+
return {
|
|
1212
|
+
output: Buffer.concat(
|
|
1213
|
+
state.outputChunks.slice(state.outputHead),
|
|
1214
|
+
state.retainedBytes
|
|
1215
|
+
).toString("utf8"),
|
|
1216
|
+
truncated: state.truncated,
|
|
1217
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
1221
|
+
async waitForExit(terminalId) {
|
|
1222
|
+
const state = this.terminals.get(terminalId);
|
|
1223
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1224
|
+
return state.exitPromise;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Kill the process but keep the terminal record (agent can still read output).
|
|
1228
|
+
* On POSIX this signals only the direct child; descendants may survive because
|
|
1229
|
+
* terminal processes are not spawned as process-group leaders.
|
|
1230
|
+
*/
|
|
1231
|
+
kill(terminalId) {
|
|
1232
|
+
const state = this.terminals.get(terminalId);
|
|
1233
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1234
|
+
treeKill2(state.proc);
|
|
1235
|
+
}
|
|
1236
|
+
/** Kill the process if alive and remove the record. */
|
|
1237
|
+
release(terminalId) {
|
|
1238
|
+
const state = this.terminals.get(terminalId);
|
|
1239
|
+
if (!state) return;
|
|
1240
|
+
if (state.timeoutHandle) {
|
|
1241
|
+
clearTimeout(state.timeoutHandle);
|
|
1242
|
+
state.timeoutHandle = null;
|
|
1322
1243
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1244
|
+
if (state.onData) {
|
|
1245
|
+
state.proc.stdout?.off("data", state.onData);
|
|
1246
|
+
state.proc.stderr?.off("data", state.onData);
|
|
1247
|
+
state.onData = void 0;
|
|
1248
|
+
}
|
|
1249
|
+
state.proc.stdout?.destroy?.();
|
|
1250
|
+
state.proc.stderr?.destroy?.();
|
|
1251
|
+
state.outputChunks.length = 0;
|
|
1252
|
+
state.outputHead = 0;
|
|
1253
|
+
state.retainedBytes = 0;
|
|
1254
|
+
treeKill2(state.proc, { force: true });
|
|
1255
|
+
this.terminals.delete(terminalId);
|
|
1326
1256
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1257
|
+
/**
|
|
1258
|
+
* Release all resources held by this server: kill every active terminal
|
|
1259
|
+
* and detach the host `AbortSignal` listener.
|
|
1260
|
+
*
|
|
1261
|
+
* Idempotent — calling it multiple times is safe. Required because the
|
|
1262
|
+
* previously-coded `releaseAll()` was the only path that removed the
|
|
1263
|
+
* abort listener: if the host never called it (unhandled error path,
|
|
1264
|
+
* host crash, GC of the session without explicit close), the listener
|
|
1265
|
+
* pinned `this` (terminals Map, output buffers) for the lifetime of the
|
|
1266
|
+
* signal. With `dispose()` this is no longer leak-prone.
|
|
1267
|
+
*
|
|
1268
|
+
* Also exposed as `[Symbol.dispose]` for `using` blocks in Node ≥ 22.
|
|
1269
|
+
*
|
|
1270
|
+
* RAM-leak audit 2026-08-11, MEDIUM (Finding 2).
|
|
1271
|
+
*/
|
|
1272
|
+
dispose() {
|
|
1273
|
+
if (this.disposed) return;
|
|
1274
|
+
if (DEBUG_DISPOSE) {
|
|
1275
|
+
const activeChildren = this.terminals.size;
|
|
1276
|
+
console.debug(
|
|
1277
|
+
JSON.stringify({
|
|
1278
|
+
event: "terminal_server.disposed",
|
|
1279
|
+
instanceId: this.instanceId,
|
|
1280
|
+
activeChildren,
|
|
1281
|
+
hadSignal: this.abortSignal !== void 0,
|
|
1282
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
this.disposed = true;
|
|
1287
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
1288
|
+
for (const id of [...this.terminals.keys()]) {
|
|
1289
|
+
this.release(id);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
1293
|
+
[Symbol.dispose]() {
|
|
1294
|
+
this.dispose();
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Kill all active terminals. Used on session close.
|
|
1298
|
+
*
|
|
1299
|
+
* @deprecated Prefer `dispose()` (or `using { … }` via `Symbol.dispose`).
|
|
1300
|
+
* `releaseAll` is retained as a delegated wrapper for callers that still
|
|
1301
|
+
* reference it; new code should call `dispose()` directly so the
|
|
1302
|
+
* host-signal listener is removed unconditionally.
|
|
1303
|
+
*/
|
|
1304
|
+
releaseAll() {
|
|
1305
|
+
this.dispose();
|
|
1351
1306
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1307
|
+
resolveCwd(cwd) {
|
|
1308
|
+
if (!cwd) return this.projectRoot;
|
|
1309
|
+
const resolved = path2.resolve(cwd);
|
|
1310
|
+
const rootWithSep = this.projectRoot.endsWith(path2.sep) ? this.projectRoot : this.projectRoot + path2.sep;
|
|
1311
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1312
|
+
return this.projectRoot;
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
const realRoot = realpathSync2(this.projectRoot);
|
|
1316
|
+
const realCwd = realpathSync2(resolved);
|
|
1317
|
+
const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
|
|
1318
|
+
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
1319
|
+
return realRoot;
|
|
1320
|
+
}
|
|
1321
|
+
return realCwd;
|
|
1322
|
+
} catch {
|
|
1323
|
+
return this.projectRoot;
|
|
1365
1324
|
}
|
|
1366
1325
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
]);
|
|
1376
|
-
} finally {
|
|
1377
|
-
if (timer !== null) clearTimeout(timer);
|
|
1378
|
-
removeAbort?.();
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
function rejectOnAbort(signal) {
|
|
1382
|
-
return new Promise((_, reject) => {
|
|
1383
|
-
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
1384
|
-
if (signal.aborted) {
|
|
1385
|
-
rejectAbort();
|
|
1386
|
-
return;
|
|
1326
|
+
buildEnv(agentEnv) {
|
|
1327
|
+
const env = buildChildEnv();
|
|
1328
|
+
if (agentEnv) {
|
|
1329
|
+
for (const { name, value } of agentEnv) {
|
|
1330
|
+
const upper = name.toUpperCase();
|
|
1331
|
+
if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
|
|
1332
|
+
env[name] = value;
|
|
1333
|
+
}
|
|
1387
1334
|
}
|
|
1388
|
-
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
function isAbortLikeError(err) {
|
|
1392
|
-
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
1393
|
-
}
|
|
1394
|
-
function resolvePermissionDeadline(value) {
|
|
1395
|
-
if (value === Number.POSITIVE_INFINITY) return null;
|
|
1396
|
-
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
1397
|
-
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// src/client/acp-message-routing.ts
|
|
1401
|
-
function isBestEffortAckMethod(method) {
|
|
1402
|
-
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// src/client/acp-session-ops.ts
|
|
1406
|
-
function filterMcpServers(agentCapabilities, servers) {
|
|
1407
|
-
if (!servers || servers.length === 0) return [];
|
|
1408
|
-
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
1409
|
-
return servers.filter((s) => {
|
|
1410
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
1411
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
1412
|
-
return true;
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
1416
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1417
|
-
if (!ctx.agentCapabilities.loadSession) {
|
|
1418
|
-
throw new ACPSessionError(
|
|
1419
|
-
"unsupported_capability",
|
|
1420
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
1421
|
-
);
|
|
1335
|
+
return env;
|
|
1422
1336
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Clamp an agent-supplied numeric to a finite positive safe integer, falling
|
|
1339
|
+
* back to `defaultValue` for undefined/NaN/non-finite values. Prevents
|
|
1340
|
+
* negative, NaN, or Infinity values from disabling output caps or causing
|
|
1341
|
+
* unbounded memory growth.
|
|
1342
|
+
*/
|
|
1343
|
+
clampFiniteInt(value, defaultValue) {
|
|
1344
|
+
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
1345
|
+
return defaultValue;
|
|
1346
|
+
}
|
|
1347
|
+
return Math.trunc(value);
|
|
1425
1348
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1349
|
+
};
|
|
1350
|
+
var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1351
|
+
"NODE_OPTIONS",
|
|
1352
|
+
"LD_PRELOAD",
|
|
1353
|
+
"LD_LIBRARY_PATH",
|
|
1354
|
+
"DYLD_INSERT_LIBRARIES",
|
|
1355
|
+
"DYLD_LIBRARY_PATH",
|
|
1356
|
+
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
1357
|
+
"PATH",
|
|
1358
|
+
"PYTHONPATH",
|
|
1359
|
+
"PYTHONSTARTUP",
|
|
1360
|
+
"PERL5OPT",
|
|
1361
|
+
"PERLLIB",
|
|
1362
|
+
"RUBYOPT",
|
|
1363
|
+
"RUBYLIB"
|
|
1364
|
+
]);
|
|
1365
|
+
|
|
1366
|
+
// src/client/trust-boundary-permission.ts
|
|
1367
|
+
function pickOption(options, allowed) {
|
|
1368
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
1369
|
+
for (const kind of kinds) {
|
|
1370
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
1371
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
1436
1372
|
}
|
|
1437
|
-
|
|
1373
|
+
return { outcome: "cancelled" };
|
|
1438
1374
|
}
|
|
1439
|
-
|
|
1440
|
-
if (
|
|
1441
|
-
if (
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (
|
|
1448
|
-
|
|
1449
|
-
}
|
|
1450
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
1451
|
-
const id = ctx.allocId();
|
|
1452
|
-
const result = await ctx.sendRequest(id, "session/resume", {
|
|
1453
|
-
sessionId,
|
|
1454
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
1455
|
-
mcpServers: servers
|
|
1456
|
-
});
|
|
1457
|
-
if (isJsonRpcError(result)) {
|
|
1458
|
-
throw new ACPSessionError(
|
|
1459
|
-
"prompt_failed",
|
|
1460
|
-
`session/resume failed: ${result.message}`,
|
|
1461
|
-
result
|
|
1462
|
-
);
|
|
1375
|
+
function riskFor(kind) {
|
|
1376
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
1377
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
1378
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
1379
|
+
return "elevated";
|
|
1380
|
+
}
|
|
1381
|
+
function capabilityFor(request) {
|
|
1382
|
+
const raw = request.toolCall.rawInput;
|
|
1383
|
+
if (typeof raw?.path === "string") {
|
|
1384
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
1463
1385
|
}
|
|
1464
|
-
|
|
1386
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
1387
|
+
return "process.spawn";
|
|
1388
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
1389
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
1465
1390
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
1472
|
-
);
|
|
1391
|
+
function subjectFor(request) {
|
|
1392
|
+
const raw = request.toolCall.rawInput;
|
|
1393
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
1394
|
+
if (typeof raw?.path === "string") {
|
|
1395
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
1473
1396
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
1397
|
+
if (typeof raw?.command === "string") {
|
|
1398
|
+
return {
|
|
1399
|
+
kind: "command",
|
|
1400
|
+
id: raw.command,
|
|
1401
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1402
|
+
};
|
|
1481
1403
|
}
|
|
1482
|
-
const r = result;
|
|
1483
1404
|
return {
|
|
1484
|
-
|
|
1485
|
-
|
|
1405
|
+
kind: "resource",
|
|
1406
|
+
id: title,
|
|
1407
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1486
1408
|
};
|
|
1487
1409
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
1491
|
-
throw new ACPSessionError(
|
|
1492
|
-
"unsupported_capability",
|
|
1493
|
-
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
1494
|
-
);
|
|
1495
|
-
}
|
|
1496
|
-
const id = ctx.allocId();
|
|
1497
|
-
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
1498
|
-
if (isJsonRpcError(result)) {
|
|
1499
|
-
throw new ACPSessionError(
|
|
1500
|
-
"prompt_failed",
|
|
1501
|
-
`session/delete failed: ${result.message}`,
|
|
1502
|
-
result
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
if (ctx.sessionId === sessionId) {
|
|
1506
|
-
ctx.setSessionId(null);
|
|
1507
|
-
}
|
|
1410
|
+
function isAllowed(decision) {
|
|
1411
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
1508
1412
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1413
|
+
function toTrustBoundaryRequest(request, options) {
|
|
1414
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
1415
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
1416
|
+
return {
|
|
1417
|
+
version: 1,
|
|
1418
|
+
requestId: String(request.toolCall.toolCallId),
|
|
1419
|
+
actor: {
|
|
1420
|
+
...options.actor ?? { kind: "agent" },
|
|
1421
|
+
...sessionId ? { sessionId } : {}
|
|
1422
|
+
},
|
|
1423
|
+
surface: "acp",
|
|
1424
|
+
capability: capabilityFor(request),
|
|
1425
|
+
subject: subjectFor(request),
|
|
1426
|
+
risk: riskFor(request.toolCall.kind),
|
|
1427
|
+
scope: {
|
|
1428
|
+
...options.scope ?? {},
|
|
1429
|
+
...sessionId ? { sessionId } : {}
|
|
1430
|
+
},
|
|
1431
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
1432
|
+
metadata: {
|
|
1433
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
1434
|
+
toolKind: request.toolCall.kind ?? null
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1526
1437
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
`session/set_mode failed: ${result.message}`,
|
|
1535
|
-
result
|
|
1536
|
-
);
|
|
1537
|
-
}
|
|
1438
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
1439
|
+
return async (request) => {
|
|
1440
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1441
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
1442
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1443
|
+
return pickOption(request.options, isAllowed(decision));
|
|
1444
|
+
};
|
|
1538
1445
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
);
|
|
1446
|
+
|
|
1447
|
+
// src/client/websocket-transport.ts
|
|
1448
|
+
var WebSocketClientTransport = class {
|
|
1449
|
+
ws = null;
|
|
1450
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1451
|
+
closed = false;
|
|
1452
|
+
opts;
|
|
1453
|
+
maxBufferedBytes;
|
|
1454
|
+
maxMessageChars;
|
|
1455
|
+
constructor(opts) {
|
|
1456
|
+
this.opts = opts;
|
|
1457
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
1458
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
1553
1459
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1460
|
+
/** Pending start() promise resolve/reject — settled in stop() to avoid leaking. */
|
|
1461
|
+
pendingStart = null;
|
|
1462
|
+
start() {
|
|
1463
|
+
if (this.closed || this.ws !== null || this.pendingStart !== null) {
|
|
1464
|
+
return Promise.reject(new Error("WebSocket transport has already been started or stopped"));
|
|
1465
|
+
}
|
|
1466
|
+
const WS = globalThis.WebSocket;
|
|
1467
|
+
if (!WS) {
|
|
1468
|
+
return Promise.reject(
|
|
1469
|
+
new Error(
|
|
1470
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
1471
|
+
)
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1475
|
+
return new Promise((resolve3, reject) => {
|
|
1476
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1477
|
+
this.ws = ws;
|
|
1478
|
+
const timer = setTimeout(() => {
|
|
1479
|
+
const pending = this.pendingStart;
|
|
1480
|
+
if (pending === null) return;
|
|
1481
|
+
this.pendingStart = null;
|
|
1482
|
+
this.closed = true;
|
|
1483
|
+
if (this.ws === ws) this.ws = null;
|
|
1484
|
+
this.handlers.clear();
|
|
1485
|
+
try {
|
|
1486
|
+
ws.close();
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1490
|
+
}, timeoutMs);
|
|
1491
|
+
this.pendingStart = { resolve: resolve3, reject, timer };
|
|
1492
|
+
ws.addEventListener("open", () => {
|
|
1493
|
+
const pending = this.pendingStart;
|
|
1494
|
+
if (pending === null) return;
|
|
1495
|
+
this.pendingStart = null;
|
|
1496
|
+
clearTimeout(pending.timer);
|
|
1497
|
+
pending.resolve();
|
|
1498
|
+
});
|
|
1499
|
+
ws.addEventListener("error", (ev) => {
|
|
1500
|
+
const pending = this.pendingStart;
|
|
1501
|
+
if (pending === null) {
|
|
1502
|
+
this.stop();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
this.pendingStart = null;
|
|
1506
|
+
this.closed = true;
|
|
1507
|
+
if (this.ws === ws) this.ws = null;
|
|
1508
|
+
this.handlers.clear();
|
|
1509
|
+
clearTimeout(pending.timer);
|
|
1510
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
1511
|
+
pending.reject(new Error(message));
|
|
1512
|
+
});
|
|
1513
|
+
ws.addEventListener("close", () => {
|
|
1514
|
+
this.closed = true;
|
|
1515
|
+
if (this.ws === ws) this.ws = null;
|
|
1516
|
+
this.handlers.clear();
|
|
1517
|
+
const pending = this.pendingStart;
|
|
1518
|
+
if (pending !== null) {
|
|
1519
|
+
this.pendingStart = null;
|
|
1520
|
+
clearTimeout(pending.timer);
|
|
1521
|
+
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
ws.addEventListener("message", (ev) => {
|
|
1525
|
+
this.onData(ev.data);
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1565
1528
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1529
|
+
send(msg) {
|
|
1530
|
+
if (this.closed || !this.ws) {
|
|
1531
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
const serialized = JSON.stringify(msg);
|
|
1535
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
|
|
1536
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
1537
|
+
this.stop();
|
|
1538
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
1539
|
+
}
|
|
1540
|
+
this.ws.send(serialized);
|
|
1541
|
+
return Promise.resolve();
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1544
|
+
}
|
|
1575
1545
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const id = ctx.allocId();
|
|
1580
|
-
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
1581
|
-
if (isJsonRpcError(result)) {
|
|
1582
|
-
throw new ACPSessionError(
|
|
1583
|
-
"prompt_failed",
|
|
1584
|
-
`providers/disable failed: ${result.message}`,
|
|
1585
|
-
result
|
|
1586
|
-
);
|
|
1546
|
+
onMessage(handler) {
|
|
1547
|
+
this.handlers.add(handler);
|
|
1548
|
+
return () => this.handlers.delete(handler);
|
|
1587
1549
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1550
|
+
stop() {
|
|
1551
|
+
this.closed = true;
|
|
1552
|
+
this.handlers.clear();
|
|
1553
|
+
if (this.pendingStart !== null) {
|
|
1554
|
+
const pending = this.pendingStart;
|
|
1555
|
+
this.pendingStart = null;
|
|
1556
|
+
clearTimeout(pending.timer);
|
|
1557
|
+
try {
|
|
1558
|
+
pending.reject(new Error("WebSocket transport stopped while connecting"));
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (this.ws) {
|
|
1563
|
+
try {
|
|
1564
|
+
this.ws.close();
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
this.ws = null;
|
|
1568
|
+
}
|
|
1595
1569
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
"
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1570
|
+
onData(data) {
|
|
1571
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1572
|
+
if (text.length > this.maxMessageChars) {
|
|
1573
|
+
this.stop();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (!text.trim()) return;
|
|
1577
|
+
let msg;
|
|
1578
|
+
try {
|
|
1579
|
+
msg = JSON.parse(text);
|
|
1580
|
+
} catch {
|
|
1581
|
+
for (const line of text.split("\n")) {
|
|
1582
|
+
if (!line.trim()) continue;
|
|
1583
|
+
try {
|
|
1584
|
+
this.dispatch(JSON.parse(line));
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
this.dispatch(msg);
|
|
1611
1591
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1592
|
+
dispatch(msg) {
|
|
1593
|
+
for (const handler of [...this.handlers]) {
|
|
1594
|
+
try {
|
|
1595
|
+
handler(msg);
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1615
1599
|
}
|
|
1616
|
-
|
|
1600
|
+
};
|
|
1601
|
+
function finitePositiveLimit(value, fallback) {
|
|
1602
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1617
1603
|
}
|
|
1618
1604
|
|
|
1619
1605
|
// src/client/acp-session.ts
|
|
@@ -2199,11 +2185,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2199
2185
|
options.onProgress?.(event);
|
|
2200
2186
|
};
|
|
2201
2187
|
try {
|
|
2202
|
-
const result = await session.prompt(
|
|
2203
|
-
[textContent(task.description)],
|
|
2204
|
-
ctx.signal,
|
|
2205
|
-
onProgress
|
|
2206
|
-
);
|
|
2188
|
+
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
2207
2189
|
return {
|
|
2208
2190
|
result: result.text,
|
|
2209
2191
|
iterations: 1,
|