@wrongstack/acp 0.309.0 → 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 +4 -4
package/dist/index.js
CHANGED
|
@@ -4,9 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
// src/types/acp-v1.ts
|
|
5
5
|
var ACP_PROTOCOL_VERSION = 1;
|
|
6
6
|
function assertNeverSessionUpdate(x) {
|
|
7
|
-
throw new Error(
|
|
8
|
-
`Unhandled sessionUpdate: ${JSON.stringify(x)}`
|
|
9
|
-
);
|
|
7
|
+
throw new Error(`Unhandled sessionUpdate: ${JSON.stringify(x)}`);
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
// src/version.ts
|
|
@@ -1217,9 +1215,7 @@ var ACPToolsRegistry = class {
|
|
|
1217
1215
|
/** Build the ACP tools/list payload from registered tools. */
|
|
1218
1216
|
buildToolList() {
|
|
1219
1217
|
return {
|
|
1220
|
-
tools: Array.from(this.tools.values()).map(
|
|
1221
|
-
(t) => toACPToolDefinition(t, this.owner)
|
|
1222
|
-
)
|
|
1218
|
+
tools: Array.from(this.tools.values()).map((t) => toACPToolDefinition(t, this.owner))
|
|
1223
1219
|
};
|
|
1224
1220
|
}
|
|
1225
1221
|
/**
|
|
@@ -1522,37 +1518,9 @@ if (isEntrypoint) {
|
|
|
1522
1518
|
});
|
|
1523
1519
|
}
|
|
1524
1520
|
|
|
1525
|
-
// src/client/acp-
|
|
1526
|
-
function
|
|
1527
|
-
return
|
|
1528
|
-
}
|
|
1529
|
-
function imageContent(mimeType, data) {
|
|
1530
|
-
return { type: "image", mimeType, data };
|
|
1531
|
-
}
|
|
1532
|
-
function audioContent(mimeType, data) {
|
|
1533
|
-
return { type: "audio", mimeType, data };
|
|
1534
|
-
}
|
|
1535
|
-
function extractText(block) {
|
|
1536
|
-
if (typeof block !== "object" || block === null) return "";
|
|
1537
|
-
const b = block;
|
|
1538
|
-
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1539
|
-
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1540
|
-
return b.resource.text;
|
|
1541
|
-
}
|
|
1542
|
-
return "";
|
|
1543
|
-
}
|
|
1544
|
-
function isRecord(v) {
|
|
1545
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1546
|
-
}
|
|
1547
|
-
function emptyRunResult(stopReason) {
|
|
1548
|
-
return {
|
|
1549
|
-
text: "",
|
|
1550
|
-
stopReason,
|
|
1551
|
-
hasText: false,
|
|
1552
|
-
toolCalls: [],
|
|
1553
|
-
diffs: [],
|
|
1554
|
-
thoughts: ""
|
|
1555
|
-
};
|
|
1521
|
+
// src/client/acp-message-routing.ts
|
|
1522
|
+
function isBestEffortAckMethod(method) {
|
|
1523
|
+
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
1556
1524
|
}
|
|
1557
1525
|
|
|
1558
1526
|
// src/client/file-server.ts
|
|
@@ -1737,610 +1705,485 @@ function safeRealpathSync(p) {
|
|
|
1737
1705
|
}
|
|
1738
1706
|
}
|
|
1739
1707
|
|
|
1740
|
-
// src/client/
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
const chosen = ranked[0];
|
|
1752
|
-
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1753
|
-
return { outcome: "cancelled" };
|
|
1708
|
+
// src/client/acp-session-callbacks.ts
|
|
1709
|
+
var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
|
|
1710
|
+
async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
|
|
1711
|
+
const id = msg.id;
|
|
1712
|
+
if (id === void 0) return;
|
|
1713
|
+
const params = msg.params;
|
|
1714
|
+
const toolCall = params?.toolCall;
|
|
1715
|
+
const permissionOptions = Array.isArray(params?.options) ? params.options : [];
|
|
1716
|
+
if (!toolCall) {
|
|
1717
|
+
await sender.sendErrorResponse(id, -32602, "toolCall is required");
|
|
1718
|
+
return;
|
|
1754
1719
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1770
|
-
const kind = req.toolCall.kind;
|
|
1771
|
-
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1772
|
-
return pickAllow(req.options);
|
|
1720
|
+
try {
|
|
1721
|
+
const outcome = await runPermissionWithDeadline(
|
|
1722
|
+
permissionPolicy,
|
|
1723
|
+
{
|
|
1724
|
+
toolCall,
|
|
1725
|
+
options: permissionOptions
|
|
1726
|
+
},
|
|
1727
|
+
callbackOptions
|
|
1728
|
+
);
|
|
1729
|
+
await sender.sendResult(id, { outcome });
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1732
|
+
const code = isAbortLikeError(err) ? -32800 : -32603;
|
|
1733
|
+
await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
|
|
1773
1734
|
}
|
|
1774
|
-
return pickReject(req.options);
|
|
1775
|
-
};
|
|
1776
|
-
function makePermissionPolicy(decide) {
|
|
1777
|
-
return async (req) => {
|
|
1778
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1779
|
-
const allow = await decide(req);
|
|
1780
|
-
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1781
|
-
};
|
|
1782
1735
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1791
|
-
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1792
|
-
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
1793
|
-
var TerminalServer = class {
|
|
1794
|
-
terminals = /* @__PURE__ */ new Map();
|
|
1795
|
-
/**
|
|
1796
|
-
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
1797
|
-
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
1798
|
-
* be cryptographically unique.
|
|
1799
|
-
*/
|
|
1800
|
-
instanceId;
|
|
1801
|
-
projectRoot;
|
|
1802
|
-
commandTimeoutMs;
|
|
1803
|
-
outputByteLimit;
|
|
1804
|
-
maxOutputByteLimit;
|
|
1805
|
-
maxTerminals;
|
|
1806
|
-
abortSignal;
|
|
1807
|
-
abortHandler = () => this.dispose();
|
|
1808
|
-
disposed = false;
|
|
1809
|
-
nextId = 1;
|
|
1810
|
-
constructor(opts) {
|
|
1811
|
-
this.projectRoot = path3.resolve(opts.projectRoot);
|
|
1812
|
-
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1813
|
-
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1814
|
-
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1815
|
-
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
1816
|
-
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1817
|
-
this.abortSignal = opts.signal;
|
|
1818
|
-
if (opts.signal) {
|
|
1819
|
-
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1820
|
-
}
|
|
1736
|
+
async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
1737
|
+
const id = msg.id;
|
|
1738
|
+
if (id === void 0) return;
|
|
1739
|
+
const params = msg.params;
|
|
1740
|
+
if (!params?.path) {
|
|
1741
|
+
await sender.sendErrorResponse(id, -32602, "path is required");
|
|
1742
|
+
return;
|
|
1821
1743
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1744
|
+
if (msg.method === "fs/write_text_file") {
|
|
1745
|
+
const authorization = await authorizeAcpCallback(
|
|
1746
|
+
permissionPolicy,
|
|
1747
|
+
{
|
|
1748
|
+
toolCallId: `acp-fs-write-${id}`,
|
|
1749
|
+
title: `Write file: ${params.path}`,
|
|
1750
|
+
kind: "edit",
|
|
1751
|
+
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
1752
|
+
},
|
|
1753
|
+
callbackOptions
|
|
1754
|
+
);
|
|
1755
|
+
if (authorization !== "allowed") {
|
|
1756
|
+
const isCancelled = authorization === "cancelled";
|
|
1757
|
+
await sender.sendErrorResponse(
|
|
1758
|
+
id,
|
|
1759
|
+
isCancelled ? -32800 : -32602,
|
|
1760
|
+
isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
|
|
1827
1761
|
);
|
|
1762
|
+
return;
|
|
1828
1763
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
if (msg.method === "fs/read_text_file") {
|
|
1767
|
+
const result = await fileServer.readTextFile({
|
|
1768
|
+
sessionId: params.sessionId ?? "",
|
|
1769
|
+
path: params.path
|
|
1770
|
+
});
|
|
1771
|
+
await sender.sendResult(id, result);
|
|
1772
|
+
} else {
|
|
1773
|
+
await fileServer.writeTextFile({
|
|
1774
|
+
sessionId: params.sessionId ?? "",
|
|
1775
|
+
path: params.path,
|
|
1776
|
+
content: params.content ?? ""
|
|
1777
|
+
});
|
|
1778
|
+
await sender.sendResult(id, {});
|
|
1833
1779
|
}
|
|
1834
|
-
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const exitStatus = {
|
|
1871
|
-
exitCode: typeof code === "number" ? code : null,
|
|
1872
|
-
signal: typeof signalName === "string" ? signalName : null
|
|
1873
|
-
};
|
|
1874
|
-
state.exitStatus = exitStatus;
|
|
1875
|
-
resolve4(exitStatus);
|
|
1876
|
-
});
|
|
1877
|
-
proc.on("error", (err) => {
|
|
1878
|
-
if (state.timeoutHandle) {
|
|
1879
|
-
clearTimeout(state.timeoutHandle);
|
|
1880
|
-
state.timeoutHandle = null;
|
|
1881
|
-
}
|
|
1882
|
-
const exitStatus = { exitCode: 127, signal: null };
|
|
1883
|
-
state.exitStatus = exitStatus;
|
|
1884
|
-
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
1885
|
-
`, "utf8");
|
|
1886
|
-
if (errorOutput.length > perCallByteLimit) {
|
|
1887
|
-
let start = errorOutput.length - perCallByteLimit;
|
|
1888
|
-
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
1889
|
-
errorOutput = errorOutput.subarray(start);
|
|
1890
|
-
state.truncated = true;
|
|
1891
|
-
}
|
|
1892
|
-
state.outputChunks.push(errorOutput);
|
|
1893
|
-
state.retainedBytes = errorOutput.length;
|
|
1894
|
-
resolve4(exitStatus);
|
|
1895
|
-
});
|
|
1896
|
-
})
|
|
1897
|
-
};
|
|
1898
|
-
proc.stdout?.setEncoding("utf8");
|
|
1899
|
-
proc.stderr?.setEncoding("utf8");
|
|
1900
|
-
const onData = (chunk) => {
|
|
1901
|
-
const outputChunk = Buffer.from(chunk, "utf8");
|
|
1902
|
-
state.outputChunks.push(outputChunk);
|
|
1903
|
-
state.retainedBytes += outputChunk.length;
|
|
1904
|
-
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
1905
|
-
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
1906
|
-
const first = state.outputChunks[state.outputHead];
|
|
1907
|
-
const overflow = state.retainedBytes - perCallByteLimit;
|
|
1908
|
-
if (first.length <= overflow) {
|
|
1909
|
-
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
1910
|
-
state.outputHead++;
|
|
1911
|
-
state.retainedBytes -= first.length;
|
|
1912
|
-
continue;
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
1782
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1783
|
+
await sender.sendErrorResponse(id, code, message);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
1787
|
+
const id = msg.id;
|
|
1788
|
+
if (id === void 0) return;
|
|
1789
|
+
const params = msg.params ?? {};
|
|
1790
|
+
try {
|
|
1791
|
+
switch (msg.method) {
|
|
1792
|
+
case "terminal/create": {
|
|
1793
|
+
const authorization = await authorizeAcpCallback(
|
|
1794
|
+
permissionPolicy,
|
|
1795
|
+
{
|
|
1796
|
+
toolCallId: `acp-terminal-create-${id}`,
|
|
1797
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
1798
|
+
kind: "execute",
|
|
1799
|
+
rawInput: {
|
|
1800
|
+
command: params.command,
|
|
1801
|
+
args: params.args,
|
|
1802
|
+
cwd: params.cwd,
|
|
1803
|
+
sessionId: params.sessionId
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
callbackOptions
|
|
1807
|
+
);
|
|
1808
|
+
if (authorization !== "allowed") {
|
|
1809
|
+
const isCancelled = authorization === "cancelled";
|
|
1810
|
+
await sender.sendErrorResponse(
|
|
1811
|
+
id,
|
|
1812
|
+
isCancelled ? -32800 : -32602,
|
|
1813
|
+
isCancelled ? "terminal create permission request cancelled or timed out" : "terminal create denied by permission policy"
|
|
1814
|
+
);
|
|
1815
|
+
return;
|
|
1913
1816
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1817
|
+
const createOpts = {
|
|
1818
|
+
sessionId: String(params.sessionId ?? ""),
|
|
1819
|
+
command: String(params.command ?? ""),
|
|
1820
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
1821
|
+
};
|
|
1822
|
+
if (Array.isArray(params.env)) {
|
|
1823
|
+
createOpts.env = params.env;
|
|
1824
|
+
}
|
|
1825
|
+
if (typeof params.cwd === "string") {
|
|
1826
|
+
createOpts.cwd = params.cwd;
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof params.outputByteLimit === "number") {
|
|
1829
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
1830
|
+
}
|
|
1831
|
+
const result = terminalServer.create(createOpts);
|
|
1832
|
+
await sender.sendResult(id, result);
|
|
1833
|
+
return;
|
|
1918
1834
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1835
|
+
case "terminal/output": {
|
|
1836
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1837
|
+
const out = terminalServer.output(terminalId);
|
|
1838
|
+
await sender.sendResult(id, out);
|
|
1839
|
+
return;
|
|
1922
1840
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1841
|
+
case "terminal/wait_for_exit": {
|
|
1842
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1843
|
+
const exit = await terminalServer.waitForExit(terminalId);
|
|
1844
|
+
await sender.sendResult(id, exit);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
case "terminal/kill": {
|
|
1848
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1849
|
+
terminalServer.kill(terminalId);
|
|
1850
|
+
await sender.sendResult(id, {});
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
case "terminal/release": {
|
|
1854
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1855
|
+
terminalServer.release(terminalId);
|
|
1856
|
+
await sender.sendResult(id, {});
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
default:
|
|
1860
|
+
await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
1861
|
+
}
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1864
|
+
await sender.sendErrorResponse(id, -32603, message);
|
|
1932
1865
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1866
|
+
}
|
|
1867
|
+
async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
|
|
1868
|
+
try {
|
|
1869
|
+
const outcome = await runPermissionWithDeadline(
|
|
1870
|
+
permissionPolicy,
|
|
1871
|
+
{
|
|
1872
|
+
toolCall: {
|
|
1873
|
+
sessionUpdate: "tool_call_update",
|
|
1874
|
+
toolCallId: partial.toolCallId,
|
|
1875
|
+
title: partial.title,
|
|
1876
|
+
kind: partial.kind,
|
|
1877
|
+
status: "pending",
|
|
1878
|
+
...partial.rawInput ? { rawInput: partial.rawInput } : {}
|
|
1879
|
+
},
|
|
1880
|
+
options: [
|
|
1881
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
1882
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
1883
|
+
]
|
|
1884
|
+
},
|
|
1885
|
+
callbackOptions
|
|
1886
|
+
);
|
|
1887
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
return isAbortLikeError(err) ? "cancelled" : "denied";
|
|
1945
1890
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1891
|
+
}
|
|
1892
|
+
async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
|
|
1893
|
+
const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
|
|
1894
|
+
const controller = new AbortController();
|
|
1895
|
+
const abort = () => controller.abort();
|
|
1896
|
+
const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
|
|
1897
|
+
let removeAbort;
|
|
1898
|
+
if (callbackOptions.signal) {
|
|
1899
|
+
if (callbackOptions.signal.aborted) {
|
|
1900
|
+
abort();
|
|
1901
|
+
} else {
|
|
1902
|
+
callbackOptions.signal.addEventListener("abort", abort, { once: true });
|
|
1903
|
+
removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
|
|
1904
|
+
}
|
|
1951
1905
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1906
|
+
try {
|
|
1907
|
+
return await Promise.race([
|
|
1908
|
+
permissionPolicy({
|
|
1909
|
+
toolCall: call.toolCall,
|
|
1910
|
+
options: call.options,
|
|
1911
|
+
signal: controller.signal
|
|
1912
|
+
}),
|
|
1913
|
+
rejectOnAbort(controller.signal)
|
|
1914
|
+
]);
|
|
1915
|
+
} finally {
|
|
1916
|
+
if (timer !== null) clearTimeout(timer);
|
|
1917
|
+
removeAbort?.();
|
|
1961
1918
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
if (
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
}
|
|
1970
|
-
if (state.onData) {
|
|
1971
|
-
state.proc.stdout?.off("data", state.onData);
|
|
1972
|
-
state.proc.stderr?.off("data", state.onData);
|
|
1973
|
-
state.onData = void 0;
|
|
1919
|
+
}
|
|
1920
|
+
function rejectOnAbort(signal) {
|
|
1921
|
+
return new Promise((_, reject) => {
|
|
1922
|
+
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
1923
|
+
if (signal.aborted) {
|
|
1924
|
+
rejectAbort();
|
|
1925
|
+
return;
|
|
1974
1926
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1927
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
function isAbortLikeError(err) {
|
|
1931
|
+
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
1932
|
+
}
|
|
1933
|
+
function resolvePermissionDeadline(value) {
|
|
1934
|
+
if (value === Number.POSITIVE_INFINITY) return null;
|
|
1935
|
+
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
1936
|
+
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/client/acp-session-content.ts
|
|
1940
|
+
function textContent(text) {
|
|
1941
|
+
return { type: "text", text };
|
|
1942
|
+
}
|
|
1943
|
+
function imageContent(mimeType, data) {
|
|
1944
|
+
return { type: "image", mimeType, data };
|
|
1945
|
+
}
|
|
1946
|
+
function audioContent(mimeType, data) {
|
|
1947
|
+
return { type: "audio", mimeType, data };
|
|
1948
|
+
}
|
|
1949
|
+
function extractText(block) {
|
|
1950
|
+
if (typeof block !== "object" || block === null) return "";
|
|
1951
|
+
const b = block;
|
|
1952
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1953
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1954
|
+
return b.resource.text;
|
|
1982
1955
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2009
|
-
})
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
this.disposed = true;
|
|
2013
|
-
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
2014
|
-
for (const id of [...this.terminals.keys()]) {
|
|
2015
|
-
this.release(id);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
2019
|
-
[Symbol.dispose]() {
|
|
2020
|
-
this.dispose();
|
|
1956
|
+
return "";
|
|
1957
|
+
}
|
|
1958
|
+
function isRecord(v) {
|
|
1959
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1960
|
+
}
|
|
1961
|
+
function emptyRunResult(stopReason) {
|
|
1962
|
+
return {
|
|
1963
|
+
text: "",
|
|
1964
|
+
stopReason,
|
|
1965
|
+
hasText: false,
|
|
1966
|
+
toolCalls: [],
|
|
1967
|
+
diffs: [],
|
|
1968
|
+
thoughts: ""
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/client/acp-session-errors.ts
|
|
1973
|
+
var ACPSessionError = class extends Error {
|
|
1974
|
+
kind;
|
|
1975
|
+
cause;
|
|
1976
|
+
constructor(kind, message, cause) {
|
|
1977
|
+
super(message);
|
|
1978
|
+
this.name = "ACPSessionError";
|
|
1979
|
+
this.kind = kind;
|
|
1980
|
+
this.cause = cause;
|
|
2021
1981
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
1982
|
+
};
|
|
1983
|
+
function isJsonRpcError(v) {
|
|
1984
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/client/acp-session-ops.ts
|
|
1988
|
+
function filterMcpServers(agentCapabilities, servers) {
|
|
1989
|
+
if (!servers || servers.length === 0) return [];
|
|
1990
|
+
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
1991
|
+
return servers.filter((s) => {
|
|
1992
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
1993
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
1994
|
+
return true;
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
1998
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1999
|
+
if (!ctx.agentCapabilities.loadSession) {
|
|
2000
|
+
throw new ACPSessionError(
|
|
2001
|
+
"unsupported_capability",
|
|
2002
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
2003
|
+
);
|
|
2032
2004
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const resolved = path3.resolve(cwd);
|
|
2036
|
-
const rootWithSep = this.projectRoot.endsWith(path3.sep) ? this.projectRoot : this.projectRoot + path3.sep;
|
|
2037
|
-
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
2038
|
-
return this.projectRoot;
|
|
2039
|
-
}
|
|
2040
|
-
try {
|
|
2041
|
-
const realRoot = realpathSync2(this.projectRoot);
|
|
2042
|
-
const realCwd = realpathSync2(resolved);
|
|
2043
|
-
const realRootWithSep = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
|
|
2044
|
-
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
2045
|
-
return realRoot;
|
|
2046
|
-
}
|
|
2047
|
-
return realCwd;
|
|
2048
|
-
} catch {
|
|
2049
|
-
return this.projectRoot;
|
|
2050
|
-
}
|
|
2005
|
+
if (ctx.sessionId) {
|
|
2006
|
+
await ctx.closeSession();
|
|
2051
2007
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2008
|
+
ctx.resetScratch();
|
|
2009
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2010
|
+
const id = ctx.allocId();
|
|
2011
|
+
const result = await ctx.sendRequest(id, "session/load", {
|
|
2012
|
+
sessionId,
|
|
2013
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2014
|
+
mcpServers: servers
|
|
2015
|
+
});
|
|
2016
|
+
if (isJsonRpcError(result)) {
|
|
2017
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
2062
2018
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
}
|
|
2073
|
-
return Math.trunc(value);
|
|
2019
|
+
ctx.setSessionId(sessionId);
|
|
2020
|
+
}
|
|
2021
|
+
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
2022
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2023
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
2024
|
+
throw new ACPSessionError(
|
|
2025
|
+
"unsupported_capability",
|
|
2026
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
2027
|
+
);
|
|
2074
2028
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
"NODE_OPTIONS",
|
|
2078
|
-
"LD_PRELOAD",
|
|
2079
|
-
"LD_LIBRARY_PATH",
|
|
2080
|
-
"DYLD_INSERT_LIBRARIES",
|
|
2081
|
-
"DYLD_LIBRARY_PATH",
|
|
2082
|
-
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
2083
|
-
"PATH",
|
|
2084
|
-
"PYTHONPATH",
|
|
2085
|
-
"PYTHONSTARTUP",
|
|
2086
|
-
"PERL5OPT",
|
|
2087
|
-
"PERLLIB",
|
|
2088
|
-
"RUBYOPT",
|
|
2089
|
-
"RUBYLIB"
|
|
2090
|
-
]);
|
|
2091
|
-
|
|
2092
|
-
// src/client/trust-boundary-permission.ts
|
|
2093
|
-
function pickOption(options, allowed) {
|
|
2094
|
-
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
2095
|
-
for (const kind of kinds) {
|
|
2096
|
-
const option = options.find((candidate) => candidate.kind === kind);
|
|
2097
|
-
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
2029
|
+
if (ctx.sessionId) {
|
|
2030
|
+
await ctx.closeSession();
|
|
2098
2031
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
const raw = request.toolCall.rawInput;
|
|
2109
|
-
if (typeof raw?.path === "string") {
|
|
2110
|
-
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
2032
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2033
|
+
const id = ctx.allocId();
|
|
2034
|
+
const result = await ctx.sendRequest(id, "session/resume", {
|
|
2035
|
+
sessionId,
|
|
2036
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2037
|
+
mcpServers: servers
|
|
2038
|
+
});
|
|
2039
|
+
if (isJsonRpcError(result)) {
|
|
2040
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
2111
2041
|
}
|
|
2112
|
-
|
|
2113
|
-
return "process.spawn";
|
|
2114
|
-
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
2115
|
-
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
2042
|
+
ctx.setSessionId(sessionId);
|
|
2116
2043
|
}
|
|
2117
|
-
function
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2044
|
+
async function executeListSessions(ctx, cursor, cwd) {
|
|
2045
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2046
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
2047
|
+
throw new ACPSessionError(
|
|
2048
|
+
"unsupported_capability",
|
|
2049
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
2050
|
+
);
|
|
2122
2051
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2052
|
+
const id = ctx.allocId();
|
|
2053
|
+
const params = {};
|
|
2054
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
2055
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
2056
|
+
const result = await ctx.sendRequest(id, "session/list", params);
|
|
2057
|
+
if (isJsonRpcError(result)) {
|
|
2058
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
2129
2059
|
}
|
|
2060
|
+
const r = result;
|
|
2130
2061
|
return {
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
2062
|
+
sessions: r.sessions ?? [],
|
|
2063
|
+
nextCursor: r.nextCursor
|
|
2134
2064
|
};
|
|
2135
2065
|
}
|
|
2136
|
-
function
|
|
2137
|
-
|
|
2066
|
+
async function executeDeleteSession(ctx, sessionId) {
|
|
2067
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2068
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
2069
|
+
throw new ACPSessionError(
|
|
2070
|
+
"unsupported_capability",
|
|
2071
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
const id = ctx.allocId();
|
|
2075
|
+
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
2076
|
+
if (isJsonRpcError(result)) {
|
|
2077
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
2078
|
+
}
|
|
2079
|
+
if (ctx.sessionId === sessionId) {
|
|
2080
|
+
ctx.setSessionId(null);
|
|
2081
|
+
}
|
|
2138
2082
|
}
|
|
2139
|
-
function
|
|
2140
|
-
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
},
|
|
2157
|
-
...options.authContext ? { authContext: options.authContext } : {},
|
|
2158
|
-
metadata: {
|
|
2159
|
-
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
2160
|
-
toolKind: request.toolCall.kind ?? null
|
|
2161
|
-
}
|
|
2162
|
-
};
|
|
2083
|
+
async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
|
|
2084
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2085
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2086
|
+
const id = ctx.allocId();
|
|
2087
|
+
const result = await ctx.sendRequest(id, "session/fork", {
|
|
2088
|
+
sessionId: sourceSessionId,
|
|
2089
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2090
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
2091
|
+
});
|
|
2092
|
+
if (isJsonRpcError(result)) {
|
|
2093
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
2094
|
+
}
|
|
2095
|
+
const newId = result.sessionId;
|
|
2096
|
+
if (typeof newId !== "string" || !newId) {
|
|
2097
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
2098
|
+
}
|
|
2099
|
+
return newId;
|
|
2163
2100
|
}
|
|
2164
|
-
function
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2101
|
+
async function executeSetMode(ctx, sessionId, modeId) {
|
|
2102
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2103
|
+
const id = ctx.allocId();
|
|
2104
|
+
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2105
|
+
if (isJsonRpcError(result)) {
|
|
2106
|
+
throw new ACPSessionError(
|
|
2107
|
+
"prompt_failed",
|
|
2108
|
+
`session/set_mode failed: ${result.message}`,
|
|
2109
|
+
result
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2171
2112
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2113
|
+
async function executeSetConfigOption(ctx, sessionId, configId, value) {
|
|
2114
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2115
|
+
const id = ctx.allocId();
|
|
2116
|
+
const result = await ctx.sendRequest(id, "session/set_config_option", {
|
|
2117
|
+
sessionId,
|
|
2118
|
+
configId,
|
|
2119
|
+
value
|
|
2120
|
+
});
|
|
2121
|
+
if (isJsonRpcError(result)) {
|
|
2122
|
+
throw new ACPSessionError(
|
|
2123
|
+
"prompt_failed",
|
|
2124
|
+
`session/set_config_option failed: ${result.message}`,
|
|
2125
|
+
result
|
|
2126
|
+
);
|
|
2185
2127
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
if (!WS) {
|
|
2194
|
-
return Promise.reject(
|
|
2195
|
-
new Error(
|
|
2196
|
-
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
2197
|
-
)
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
2201
|
-
return new Promise((resolve4, reject) => {
|
|
2202
|
-
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
2203
|
-
this.ws = ws;
|
|
2204
|
-
const timer = setTimeout(() => {
|
|
2205
|
-
const pending = this.pendingStart;
|
|
2206
|
-
if (pending === null) return;
|
|
2207
|
-
this.pendingStart = null;
|
|
2208
|
-
this.closed = true;
|
|
2209
|
-
if (this.ws === ws) this.ws = null;
|
|
2210
|
-
this.handlers.clear();
|
|
2211
|
-
try {
|
|
2212
|
-
ws.close();
|
|
2213
|
-
} catch {
|
|
2214
|
-
}
|
|
2215
|
-
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
2216
|
-
}, timeoutMs);
|
|
2217
|
-
this.pendingStart = { resolve: resolve4, reject, timer };
|
|
2218
|
-
ws.addEventListener("open", () => {
|
|
2219
|
-
const pending = this.pendingStart;
|
|
2220
|
-
if (pending === null) return;
|
|
2221
|
-
this.pendingStart = null;
|
|
2222
|
-
clearTimeout(pending.timer);
|
|
2223
|
-
pending.resolve();
|
|
2224
|
-
});
|
|
2225
|
-
ws.addEventListener("error", (ev) => {
|
|
2226
|
-
const pending = this.pendingStart;
|
|
2227
|
-
if (pending === null) {
|
|
2228
|
-
this.stop();
|
|
2229
|
-
return;
|
|
2230
|
-
}
|
|
2231
|
-
this.pendingStart = null;
|
|
2232
|
-
this.closed = true;
|
|
2233
|
-
if (this.ws === ws) this.ws = null;
|
|
2234
|
-
this.handlers.clear();
|
|
2235
|
-
clearTimeout(pending.timer);
|
|
2236
|
-
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
2237
|
-
pending.reject(new Error(message));
|
|
2238
|
-
});
|
|
2239
|
-
ws.addEventListener("close", () => {
|
|
2240
|
-
this.closed = true;
|
|
2241
|
-
if (this.ws === ws) this.ws = null;
|
|
2242
|
-
this.handlers.clear();
|
|
2243
|
-
const pending = this.pendingStart;
|
|
2244
|
-
if (pending !== null) {
|
|
2245
|
-
this.pendingStart = null;
|
|
2246
|
-
clearTimeout(pending.timer);
|
|
2247
|
-
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
2248
|
-
}
|
|
2249
|
-
});
|
|
2250
|
-
ws.addEventListener("message", (ev) => {
|
|
2251
|
-
this.onData(ev.data);
|
|
2252
|
-
});
|
|
2253
|
-
});
|
|
2128
|
+
}
|
|
2129
|
+
async function executeListProviders(ctx) {
|
|
2130
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2131
|
+
const id = ctx.allocId();
|
|
2132
|
+
const result = await ctx.sendRequest(id, "providers/list", {});
|
|
2133
|
+
if (isJsonRpcError(result)) {
|
|
2134
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
2254
2135
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
2265
|
-
}
|
|
2266
|
-
this.ws.send(serialized);
|
|
2267
|
-
return Promise.resolve();
|
|
2268
|
-
} catch (err) {
|
|
2269
|
-
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2270
|
-
}
|
|
2136
|
+
const r = result;
|
|
2137
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
2138
|
+
}
|
|
2139
|
+
async function executeSetProvider(ctx, providerId, config) {
|
|
2140
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2141
|
+
const id = ctx.allocId();
|
|
2142
|
+
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
2143
|
+
if (isJsonRpcError(result)) {
|
|
2144
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
2271
2145
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2146
|
+
}
|
|
2147
|
+
async function executeDisableProvider(ctx) {
|
|
2148
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2149
|
+
const id = ctx.allocId();
|
|
2150
|
+
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
2151
|
+
if (isJsonRpcError(result)) {
|
|
2152
|
+
throw new ACPSessionError(
|
|
2153
|
+
"prompt_failed",
|
|
2154
|
+
`providers/disable failed: ${result.message}`,
|
|
2155
|
+
result
|
|
2156
|
+
);
|
|
2275
2157
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
try {
|
|
2284
|
-
pending.reject(new Error("WebSocket transport stopped while connecting"));
|
|
2285
|
-
} catch {
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
if (this.ws) {
|
|
2289
|
-
try {
|
|
2290
|
-
this.ws.close();
|
|
2291
|
-
} catch {
|
|
2292
|
-
}
|
|
2293
|
-
this.ws = null;
|
|
2294
|
-
}
|
|
2158
|
+
}
|
|
2159
|
+
async function executeMcpMessage(ctx, connectionId, message) {
|
|
2160
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2161
|
+
const id = ctx.allocId();
|
|
2162
|
+
const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
|
|
2163
|
+
if (isJsonRpcError(result)) {
|
|
2164
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
2295
2165
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
} catch {
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
this.dispatch(msg);
|
|
2166
|
+
return result;
|
|
2167
|
+
}
|
|
2168
|
+
async function executeCreateSession(ctx) {
|
|
2169
|
+
const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
|
|
2170
|
+
const id = ctx.allocId();
|
|
2171
|
+
const result = await ctx.sendRequest(id, "session/new", {
|
|
2172
|
+
cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2173
|
+
mcpServers: servers
|
|
2174
|
+
});
|
|
2175
|
+
if (isJsonRpcError(result)) {
|
|
2176
|
+
throw new ACPSessionError(
|
|
2177
|
+
"session_create_failed",
|
|
2178
|
+
`session/new failed: ${result.message}`,
|
|
2179
|
+
result
|
|
2180
|
+
);
|
|
2317
2181
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
handler(msg);
|
|
2322
|
-
} catch {
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2182
|
+
const sessionId = result.sessionId;
|
|
2183
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2184
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
2325
2185
|
}
|
|
2326
|
-
|
|
2327
|
-
function finitePositiveLimit(value, fallback) {
|
|
2328
|
-
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// src/client/acp-session-errors.ts
|
|
2332
|
-
var ACPSessionError = class extends Error {
|
|
2333
|
-
kind;
|
|
2334
|
-
cause;
|
|
2335
|
-
constructor(kind, message, cause) {
|
|
2336
|
-
super(message);
|
|
2337
|
-
this.name = "ACPSessionError";
|
|
2338
|
-
this.kind = kind;
|
|
2339
|
-
this.cause = cause;
|
|
2340
|
-
}
|
|
2341
|
-
};
|
|
2342
|
-
function isJsonRpcError(v) {
|
|
2343
|
-
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2186
|
+
return sessionId;
|
|
2344
2187
|
}
|
|
2345
2188
|
|
|
2346
2189
|
// src/client/acp-session-updates.ts
|
|
@@ -2434,454 +2277,593 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
|
|
|
2434
2277
|
});
|
|
2435
2278
|
}
|
|
2436
2279
|
|
|
2437
|
-
// src/client/
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2280
|
+
// src/client/permission.ts
|
|
2281
|
+
function pickAllow(options) {
|
|
2282
|
+
const ranked = [...options].sort((a, b) => {
|
|
2283
|
+
const score = (k) => {
|
|
2284
|
+
if (k === "allow_once") return 0;
|
|
2285
|
+
if (k === "allow_always") return 1;
|
|
2286
|
+
if (k === "reject_once") return 2;
|
|
2287
|
+
return 3;
|
|
2288
|
+
};
|
|
2289
|
+
return score(a.kind) - score(b.kind);
|
|
2290
|
+
});
|
|
2291
|
+
const chosen = ranked[0];
|
|
2292
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
2293
|
+
return { outcome: "cancelled" };
|
|
2448
2294
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2295
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
2296
|
+
}
|
|
2297
|
+
function pickReject(options) {
|
|
2298
|
+
const reject = options.find((o) => o.kind === "reject_once" || o.kind === "reject_always");
|
|
2299
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
2300
|
+
}
|
|
2301
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
2302
|
+
var defaultPermissionPolicy = async (req) => {
|
|
2303
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
2304
|
+
return pickAllow(req.options);
|
|
2305
|
+
};
|
|
2306
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
2307
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
2308
|
+
const kind = req.toolCall.kind;
|
|
2309
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
2310
|
+
return pickAllow(req.options);
|
|
2463
2311
|
}
|
|
2312
|
+
return pickReject(req.options);
|
|
2313
|
+
};
|
|
2314
|
+
function makePermissionPolicy(decide) {
|
|
2315
|
+
return async (req) => {
|
|
2316
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
2317
|
+
const allow = await decide(req);
|
|
2318
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
2319
|
+
};
|
|
2464
2320
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2321
|
+
|
|
2322
|
+
// src/client/terminal-server.ts
|
|
2323
|
+
import { spawn } from "node:child_process";
|
|
2324
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2325
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
2326
|
+
import * as path3 from "node:path";
|
|
2327
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
2328
|
+
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
2329
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
2330
|
+
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
2331
|
+
var TerminalServer = class {
|
|
2332
|
+
terminals = /* @__PURE__ */ new Map();
|
|
2333
|
+
/**
|
|
2334
|
+
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
2335
|
+
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
2336
|
+
* be cryptographically unique.
|
|
2337
|
+
*/
|
|
2338
|
+
instanceId;
|
|
2339
|
+
projectRoot;
|
|
2340
|
+
commandTimeoutMs;
|
|
2341
|
+
outputByteLimit;
|
|
2342
|
+
maxOutputByteLimit;
|
|
2343
|
+
maxTerminals;
|
|
2344
|
+
abortSignal;
|
|
2345
|
+
abortHandler = () => this.dispose();
|
|
2346
|
+
disposed = false;
|
|
2347
|
+
nextId = 1;
|
|
2348
|
+
constructor(opts) {
|
|
2349
|
+
this.projectRoot = path3.resolve(opts.projectRoot);
|
|
2350
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
2351
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
2352
|
+
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
2353
|
+
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
2354
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
2355
|
+
this.abortSignal = opts.signal;
|
|
2356
|
+
if (opts.signal) {
|
|
2357
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
2358
|
+
}
|
|
2472
2359
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
title: `Write file: ${params.path}`,
|
|
2479
|
-
kind: "edit",
|
|
2480
|
-
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
2481
|
-
},
|
|
2482
|
-
callbackOptions
|
|
2483
|
-
);
|
|
2484
|
-
if (authorization !== "allowed") {
|
|
2485
|
-
const isCancelled = authorization === "cancelled";
|
|
2486
|
-
await sender.sendErrorResponse(
|
|
2487
|
-
id,
|
|
2488
|
-
isCancelled ? -32800 : -32602,
|
|
2489
|
-
isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
|
|
2360
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
2361
|
+
create(params) {
|
|
2362
|
+
if (this.disposed) {
|
|
2363
|
+
throw new Error(
|
|
2364
|
+
"TerminalServer is disposed \u2014 create a new TerminalServer instead of reusing this one"
|
|
2490
2365
|
);
|
|
2491
|
-
return;
|
|
2492
2366
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
sessionId: params.sessionId ?? "",
|
|
2498
|
-
path: params.path
|
|
2499
|
-
});
|
|
2500
|
-
await sender.sendResult(id, result);
|
|
2501
|
-
} else {
|
|
2502
|
-
await fileServer.writeTextFile({
|
|
2503
|
-
sessionId: params.sessionId ?? "",
|
|
2504
|
-
path: params.path,
|
|
2505
|
-
content: params.content ?? ""
|
|
2506
|
-
});
|
|
2507
|
-
await sender.sendResult(id, {});
|
|
2367
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
2368
|
+
throw new Error(
|
|
2369
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
2370
|
+
);
|
|
2508
2371
|
}
|
|
2509
|
-
|
|
2510
|
-
const
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2372
|
+
const id = `term_${this.nextId++}`;
|
|
2373
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
2374
|
+
const perCallByteLimit = Math.min(
|
|
2375
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
2376
|
+
this.maxOutputByteLimit
|
|
2377
|
+
);
|
|
2378
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
2379
|
+
cwd,
|
|
2380
|
+
env: this.buildEnv(params.env),
|
|
2381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2382
|
+
windowsHide: true
|
|
2383
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
2384
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
2385
|
+
// the command a single shell-parsed string, which breaks
|
|
2386
|
+
// Windows cmd quoting for the common case of running node with
|
|
2387
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
2388
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
2389
|
+
// default.
|
|
2390
|
+
});
|
|
2391
|
+
const state = {
|
|
2392
|
+
proc,
|
|
2393
|
+
cwd,
|
|
2394
|
+
command: params.command,
|
|
2395
|
+
args: params.args ?? [],
|
|
2396
|
+
outputChunks: [],
|
|
2397
|
+
outputHead: 0,
|
|
2398
|
+
retainedBytes: 0,
|
|
2399
|
+
truncated: false,
|
|
2400
|
+
exitStatus: void 0,
|
|
2401
|
+
timeoutHandle: null,
|
|
2402
|
+
exitPromise: new Promise((resolve4) => {
|
|
2403
|
+
proc.on("close", (code, signalName) => {
|
|
2404
|
+
if (state.timeoutHandle) {
|
|
2405
|
+
clearTimeout(state.timeoutHandle);
|
|
2406
|
+
state.timeoutHandle = null;
|
|
2407
|
+
}
|
|
2408
|
+
const exitStatus = {
|
|
2409
|
+
exitCode: typeof code === "number" ? code : null,
|
|
2410
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
2411
|
+
};
|
|
2412
|
+
state.exitStatus = exitStatus;
|
|
2413
|
+
resolve4(exitStatus);
|
|
2414
|
+
});
|
|
2415
|
+
proc.on("error", (err) => {
|
|
2416
|
+
if (state.timeoutHandle) {
|
|
2417
|
+
clearTimeout(state.timeoutHandle);
|
|
2418
|
+
state.timeoutHandle = null;
|
|
2419
|
+
}
|
|
2420
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
2421
|
+
state.exitStatus = exitStatus;
|
|
2422
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
2423
|
+
`, "utf8");
|
|
2424
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
2425
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
2426
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
2427
|
+
errorOutput = errorOutput.subarray(start);
|
|
2428
|
+
state.truncated = true;
|
|
2429
|
+
}
|
|
2430
|
+
state.outputChunks.push(errorOutput);
|
|
2431
|
+
state.retainedBytes = errorOutput.length;
|
|
2432
|
+
resolve4(exitStatus);
|
|
2433
|
+
});
|
|
2434
|
+
})
|
|
2435
|
+
};
|
|
2436
|
+
proc.stdout?.setEncoding("utf8");
|
|
2437
|
+
proc.stderr?.setEncoding("utf8");
|
|
2438
|
+
const onData = (chunk) => {
|
|
2439
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
2440
|
+
state.outputChunks.push(outputChunk);
|
|
2441
|
+
state.retainedBytes += outputChunk.length;
|
|
2442
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
2443
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
2444
|
+
const first = state.outputChunks[state.outputHead];
|
|
2445
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
2446
|
+
if (first.length <= overflow) {
|
|
2447
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
2448
|
+
state.outputHead++;
|
|
2449
|
+
state.retainedBytes -= first.length;
|
|
2450
|
+
continue;
|
|
2559
2451
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
case "terminal/output": {
|
|
2565
|
-
const terminalId = String(params.terminalId ?? "");
|
|
2566
|
-
const out = terminalServer.output(terminalId);
|
|
2567
|
-
await sender.sendResult(id, out);
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
case "terminal/wait_for_exit": {
|
|
2571
|
-
const terminalId = String(params.terminalId ?? "");
|
|
2572
|
-
const exit = await terminalServer.waitForExit(terminalId);
|
|
2573
|
-
await sender.sendResult(id, exit);
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
case "terminal/kill": {
|
|
2577
|
-
const terminalId = String(params.terminalId ?? "");
|
|
2578
|
-
terminalServer.kill(terminalId);
|
|
2579
|
-
await sender.sendResult(id, {});
|
|
2580
|
-
return;
|
|
2452
|
+
let start = overflow;
|
|
2453
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
2454
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
2455
|
+
state.retainedBytes -= start;
|
|
2581
2456
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
await sender.sendResult(id, {});
|
|
2586
|
-
return;
|
|
2457
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
2458
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
2459
|
+
state.outputHead = 0;
|
|
2587
2460
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2461
|
+
};
|
|
2462
|
+
state.onData = onData;
|
|
2463
|
+
proc.stdout?.on("data", onData);
|
|
2464
|
+
proc.stderr?.on("data", onData);
|
|
2465
|
+
state.timeoutHandle = setTimeout(() => {
|
|
2466
|
+
treeKill2(proc);
|
|
2467
|
+
}, this.commandTimeoutMs);
|
|
2468
|
+
this.terminals.set(id, state);
|
|
2469
|
+
return { terminalId: id };
|
|
2470
|
+
}
|
|
2471
|
+
/** Return captured output and (if available) the exit status. */
|
|
2472
|
+
output(terminalId) {
|
|
2473
|
+
const state = this.terminals.get(terminalId);
|
|
2474
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
2475
|
+
return {
|
|
2476
|
+
output: Buffer.concat(
|
|
2477
|
+
state.outputChunks.slice(state.outputHead),
|
|
2478
|
+
state.retainedBytes
|
|
2479
|
+
).toString("utf8"),
|
|
2480
|
+
truncated: state.truncated,
|
|
2481
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
2485
|
+
async waitForExit(terminalId) {
|
|
2486
|
+
const state = this.terminals.get(terminalId);
|
|
2487
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
2488
|
+
return state.exitPromise;
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Kill the process but keep the terminal record (agent can still read output).
|
|
2492
|
+
* On POSIX this signals only the direct child; descendants may survive because
|
|
2493
|
+
* terminal processes are not spawned as process-group leaders.
|
|
2494
|
+
*/
|
|
2495
|
+
kill(terminalId) {
|
|
2496
|
+
const state = this.terminals.get(terminalId);
|
|
2497
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
2498
|
+
treeKill2(state.proc);
|
|
2499
|
+
}
|
|
2500
|
+
/** Kill the process if alive and remove the record. */
|
|
2501
|
+
release(terminalId) {
|
|
2502
|
+
const state = this.terminals.get(terminalId);
|
|
2503
|
+
if (!state) return;
|
|
2504
|
+
if (state.timeoutHandle) {
|
|
2505
|
+
clearTimeout(state.timeoutHandle);
|
|
2506
|
+
state.timeoutHandle = null;
|
|
2590
2507
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2508
|
+
if (state.onData) {
|
|
2509
|
+
state.proc.stdout?.off("data", state.onData);
|
|
2510
|
+
state.proc.stderr?.off("data", state.onData);
|
|
2511
|
+
state.onData = void 0;
|
|
2512
|
+
}
|
|
2513
|
+
state.proc.stdout?.destroy?.();
|
|
2514
|
+
state.proc.stderr?.destroy?.();
|
|
2515
|
+
state.outputChunks.length = 0;
|
|
2516
|
+
state.outputHead = 0;
|
|
2517
|
+
state.retainedBytes = 0;
|
|
2518
|
+
treeKill2(state.proc, { force: true });
|
|
2519
|
+
this.terminals.delete(terminalId);
|
|
2594
2520
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2521
|
+
/**
|
|
2522
|
+
* Release all resources held by this server: kill every active terminal
|
|
2523
|
+
* and detach the host `AbortSignal` listener.
|
|
2524
|
+
*
|
|
2525
|
+
* Idempotent — calling it multiple times is safe. Required because the
|
|
2526
|
+
* previously-coded `releaseAll()` was the only path that removed the
|
|
2527
|
+
* abort listener: if the host never called it (unhandled error path,
|
|
2528
|
+
* host crash, GC of the session without explicit close), the listener
|
|
2529
|
+
* pinned `this` (terminals Map, output buffers) for the lifetime of the
|
|
2530
|
+
* signal. With `dispose()` this is no longer leak-prone.
|
|
2531
|
+
*
|
|
2532
|
+
* Also exposed as `[Symbol.dispose]` for `using` blocks in Node ≥ 22.
|
|
2533
|
+
*
|
|
2534
|
+
* RAM-leak audit 2026-08-11, MEDIUM (Finding 2).
|
|
2535
|
+
*/
|
|
2536
|
+
dispose() {
|
|
2537
|
+
if (this.disposed) return;
|
|
2538
|
+
if (DEBUG_DISPOSE) {
|
|
2539
|
+
const activeChildren = this.terminals.size;
|
|
2540
|
+
console.debug(
|
|
2541
|
+
JSON.stringify({
|
|
2542
|
+
event: "terminal_server.disposed",
|
|
2543
|
+
instanceId: this.instanceId,
|
|
2544
|
+
activeChildren,
|
|
2545
|
+
hadSignal: this.abortSignal !== void 0,
|
|
2546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2547
|
+
})
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
this.disposed = true;
|
|
2551
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
2552
|
+
for (const id of [...this.terminals.keys()]) {
|
|
2553
|
+
this.release(id);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
2557
|
+
[Symbol.dispose]() {
|
|
2558
|
+
this.dispose();
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Kill all active terminals. Used on session close.
|
|
2562
|
+
*
|
|
2563
|
+
* @deprecated Prefer `dispose()` (or `using { … }` via `Symbol.dispose`).
|
|
2564
|
+
* `releaseAll` is retained as a delegated wrapper for callers that still
|
|
2565
|
+
* reference it; new code should call `dispose()` directly so the
|
|
2566
|
+
* host-signal listener is removed unconditionally.
|
|
2567
|
+
*/
|
|
2568
|
+
releaseAll() {
|
|
2569
|
+
this.dispose();
|
|
2619
2570
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2571
|
+
resolveCwd(cwd) {
|
|
2572
|
+
if (!cwd) return this.projectRoot;
|
|
2573
|
+
const resolved = path3.resolve(cwd);
|
|
2574
|
+
const rootWithSep = this.projectRoot.endsWith(path3.sep) ? this.projectRoot : this.projectRoot + path3.sep;
|
|
2575
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
2576
|
+
return this.projectRoot;
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
const realRoot = realpathSync2(this.projectRoot);
|
|
2580
|
+
const realCwd = realpathSync2(resolved);
|
|
2581
|
+
const realRootWithSep = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
|
|
2582
|
+
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
2583
|
+
return realRoot;
|
|
2584
|
+
}
|
|
2585
|
+
return realCwd;
|
|
2586
|
+
} catch {
|
|
2587
|
+
return this.projectRoot;
|
|
2633
2588
|
}
|
|
2634
2589
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
]);
|
|
2644
|
-
} finally {
|
|
2645
|
-
if (timer !== null) clearTimeout(timer);
|
|
2646
|
-
removeAbort?.();
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
function rejectOnAbort(signal) {
|
|
2650
|
-
return new Promise((_, reject) => {
|
|
2651
|
-
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
2652
|
-
if (signal.aborted) {
|
|
2653
|
-
rejectAbort();
|
|
2654
|
-
return;
|
|
2590
|
+
buildEnv(agentEnv) {
|
|
2591
|
+
const env = buildChildEnv();
|
|
2592
|
+
if (agentEnv) {
|
|
2593
|
+
for (const { name, value } of agentEnv) {
|
|
2594
|
+
const upper = name.toUpperCase();
|
|
2595
|
+
if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
|
|
2596
|
+
env[name] = value;
|
|
2597
|
+
}
|
|
2655
2598
|
}
|
|
2656
|
-
|
|
2657
|
-
});
|
|
2658
|
-
}
|
|
2659
|
-
function isAbortLikeError(err) {
|
|
2660
|
-
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
2661
|
-
}
|
|
2662
|
-
function resolvePermissionDeadline(value) {
|
|
2663
|
-
if (value === Number.POSITIVE_INFINITY) return null;
|
|
2664
|
-
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
2665
|
-
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
// src/client/acp-message-routing.ts
|
|
2669
|
-
function isBestEffortAckMethod(method) {
|
|
2670
|
-
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
// src/client/acp-session-ops.ts
|
|
2674
|
-
function filterMcpServers(agentCapabilities, servers) {
|
|
2675
|
-
if (!servers || servers.length === 0) return [];
|
|
2676
|
-
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
2677
|
-
return servers.filter((s) => {
|
|
2678
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
2679
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
2680
|
-
return true;
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2683
|
-
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
2684
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2685
|
-
if (!ctx.agentCapabilities.loadSession) {
|
|
2686
|
-
throw new ACPSessionError(
|
|
2687
|
-
"unsupported_capability",
|
|
2688
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
2689
|
-
);
|
|
2599
|
+
return env;
|
|
2690
2600
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2601
|
+
/**
|
|
2602
|
+
* Clamp an agent-supplied numeric to a finite positive safe integer, falling
|
|
2603
|
+
* back to `defaultValue` for undefined/NaN/non-finite values. Prevents
|
|
2604
|
+
* negative, NaN, or Infinity values from disabling output caps or causing
|
|
2605
|
+
* unbounded memory growth.
|
|
2606
|
+
*/
|
|
2607
|
+
clampFiniteInt(value, defaultValue) {
|
|
2608
|
+
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
2609
|
+
return defaultValue;
|
|
2610
|
+
}
|
|
2611
|
+
return Math.trunc(value);
|
|
2693
2612
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2613
|
+
};
|
|
2614
|
+
var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
2615
|
+
"NODE_OPTIONS",
|
|
2616
|
+
"LD_PRELOAD",
|
|
2617
|
+
"LD_LIBRARY_PATH",
|
|
2618
|
+
"DYLD_INSERT_LIBRARIES",
|
|
2619
|
+
"DYLD_LIBRARY_PATH",
|
|
2620
|
+
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
2621
|
+
"PATH",
|
|
2622
|
+
"PYTHONPATH",
|
|
2623
|
+
"PYTHONSTARTUP",
|
|
2624
|
+
"PERL5OPT",
|
|
2625
|
+
"PERLLIB",
|
|
2626
|
+
"RUBYOPT",
|
|
2627
|
+
"RUBYLIB"
|
|
2628
|
+
]);
|
|
2629
|
+
|
|
2630
|
+
// src/client/trust-boundary-permission.ts
|
|
2631
|
+
function pickOption(options, allowed) {
|
|
2632
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
2633
|
+
for (const kind of kinds) {
|
|
2634
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
2635
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
2704
2636
|
}
|
|
2705
|
-
|
|
2637
|
+
return { outcome: "cancelled" };
|
|
2706
2638
|
}
|
|
2707
|
-
|
|
2708
|
-
if (
|
|
2709
|
-
if (
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
if (
|
|
2716
|
-
|
|
2717
|
-
}
|
|
2718
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2719
|
-
const id = ctx.allocId();
|
|
2720
|
-
const result = await ctx.sendRequest(id, "session/resume", {
|
|
2721
|
-
sessionId,
|
|
2722
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2723
|
-
mcpServers: servers
|
|
2724
|
-
});
|
|
2725
|
-
if (isJsonRpcError(result)) {
|
|
2726
|
-
throw new ACPSessionError(
|
|
2727
|
-
"prompt_failed",
|
|
2728
|
-
`session/resume failed: ${result.message}`,
|
|
2729
|
-
result
|
|
2730
|
-
);
|
|
2639
|
+
function riskFor(kind) {
|
|
2640
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
2641
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
2642
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
2643
|
+
return "elevated";
|
|
2644
|
+
}
|
|
2645
|
+
function capabilityFor(request) {
|
|
2646
|
+
const raw = request.toolCall.rawInput;
|
|
2647
|
+
if (typeof raw?.path === "string") {
|
|
2648
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
2731
2649
|
}
|
|
2732
|
-
|
|
2650
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
2651
|
+
return "process.spawn";
|
|
2652
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
2653
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
2733
2654
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
2740
|
-
);
|
|
2655
|
+
function subjectFor(request) {
|
|
2656
|
+
const raw = request.toolCall.rawInput;
|
|
2657
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
2658
|
+
if (typeof raw?.path === "string") {
|
|
2659
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
2741
2660
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
2661
|
+
if (typeof raw?.command === "string") {
|
|
2662
|
+
return {
|
|
2663
|
+
kind: "command",
|
|
2664
|
+
id: raw.command,
|
|
2665
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
2666
|
+
};
|
|
2749
2667
|
}
|
|
2750
|
-
const r = result;
|
|
2751
2668
|
return {
|
|
2752
|
-
|
|
2753
|
-
|
|
2669
|
+
kind: "resource",
|
|
2670
|
+
id: title,
|
|
2671
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
2754
2672
|
};
|
|
2755
2673
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
2759
|
-
throw new ACPSessionError(
|
|
2760
|
-
"unsupported_capability",
|
|
2761
|
-
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
2762
|
-
);
|
|
2763
|
-
}
|
|
2764
|
-
const id = ctx.allocId();
|
|
2765
|
-
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
2766
|
-
if (isJsonRpcError(result)) {
|
|
2767
|
-
throw new ACPSessionError(
|
|
2768
|
-
"prompt_failed",
|
|
2769
|
-
`session/delete failed: ${result.message}`,
|
|
2770
|
-
result
|
|
2771
|
-
);
|
|
2772
|
-
}
|
|
2773
|
-
if (ctx.sessionId === sessionId) {
|
|
2774
|
-
ctx.setSessionId(null);
|
|
2775
|
-
}
|
|
2674
|
+
function isAllowed(decision) {
|
|
2675
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
2776
2676
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2677
|
+
function toTrustBoundaryRequest(request, options) {
|
|
2678
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
2679
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
2680
|
+
return {
|
|
2681
|
+
version: 1,
|
|
2682
|
+
requestId: String(request.toolCall.toolCallId),
|
|
2683
|
+
actor: {
|
|
2684
|
+
...options.actor ?? { kind: "agent" },
|
|
2685
|
+
...sessionId ? { sessionId } : {}
|
|
2686
|
+
},
|
|
2687
|
+
surface: "acp",
|
|
2688
|
+
capability: capabilityFor(request),
|
|
2689
|
+
subject: subjectFor(request),
|
|
2690
|
+
risk: riskFor(request.toolCall.kind),
|
|
2691
|
+
scope: {
|
|
2692
|
+
...options.scope ?? {},
|
|
2693
|
+
...sessionId ? { sessionId } : {}
|
|
2694
|
+
},
|
|
2695
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
2696
|
+
metadata: {
|
|
2697
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
2698
|
+
toolKind: request.toolCall.kind ?? null
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2794
2701
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
`session/set_mode failed: ${result.message}`,
|
|
2803
|
-
result
|
|
2804
|
-
);
|
|
2805
|
-
}
|
|
2702
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
2703
|
+
return async (request) => {
|
|
2704
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
2705
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
2706
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
2707
|
+
return pickOption(request.options, isAllowed(decision));
|
|
2708
|
+
};
|
|
2806
2709
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
);
|
|
2710
|
+
|
|
2711
|
+
// src/client/websocket-transport.ts
|
|
2712
|
+
var WebSocketClientTransport = class {
|
|
2713
|
+
ws = null;
|
|
2714
|
+
handlers = /* @__PURE__ */ new Set();
|
|
2715
|
+
closed = false;
|
|
2716
|
+
opts;
|
|
2717
|
+
maxBufferedBytes;
|
|
2718
|
+
maxMessageChars;
|
|
2719
|
+
constructor(opts) {
|
|
2720
|
+
this.opts = opts;
|
|
2721
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
2722
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
2821
2723
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2724
|
+
/** Pending start() promise resolve/reject — settled in stop() to avoid leaking. */
|
|
2725
|
+
pendingStart = null;
|
|
2726
|
+
start() {
|
|
2727
|
+
if (this.closed || this.ws !== null || this.pendingStart !== null) {
|
|
2728
|
+
return Promise.reject(new Error("WebSocket transport has already been started or stopped"));
|
|
2729
|
+
}
|
|
2730
|
+
const WS = globalThis.WebSocket;
|
|
2731
|
+
if (!WS) {
|
|
2732
|
+
return Promise.reject(
|
|
2733
|
+
new Error(
|
|
2734
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
2735
|
+
)
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
2739
|
+
return new Promise((resolve4, reject) => {
|
|
2740
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
2741
|
+
this.ws = ws;
|
|
2742
|
+
const timer = setTimeout(() => {
|
|
2743
|
+
const pending = this.pendingStart;
|
|
2744
|
+
if (pending === null) return;
|
|
2745
|
+
this.pendingStart = null;
|
|
2746
|
+
this.closed = true;
|
|
2747
|
+
if (this.ws === ws) this.ws = null;
|
|
2748
|
+
this.handlers.clear();
|
|
2749
|
+
try {
|
|
2750
|
+
ws.close();
|
|
2751
|
+
} catch {
|
|
2752
|
+
}
|
|
2753
|
+
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
2754
|
+
}, timeoutMs);
|
|
2755
|
+
this.pendingStart = { resolve: resolve4, reject, timer };
|
|
2756
|
+
ws.addEventListener("open", () => {
|
|
2757
|
+
const pending = this.pendingStart;
|
|
2758
|
+
if (pending === null) return;
|
|
2759
|
+
this.pendingStart = null;
|
|
2760
|
+
clearTimeout(pending.timer);
|
|
2761
|
+
pending.resolve();
|
|
2762
|
+
});
|
|
2763
|
+
ws.addEventListener("error", (ev) => {
|
|
2764
|
+
const pending = this.pendingStart;
|
|
2765
|
+
if (pending === null) {
|
|
2766
|
+
this.stop();
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
this.pendingStart = null;
|
|
2770
|
+
this.closed = true;
|
|
2771
|
+
if (this.ws === ws) this.ws = null;
|
|
2772
|
+
this.handlers.clear();
|
|
2773
|
+
clearTimeout(pending.timer);
|
|
2774
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
2775
|
+
pending.reject(new Error(message));
|
|
2776
|
+
});
|
|
2777
|
+
ws.addEventListener("close", () => {
|
|
2778
|
+
this.closed = true;
|
|
2779
|
+
if (this.ws === ws) this.ws = null;
|
|
2780
|
+
this.handlers.clear();
|
|
2781
|
+
const pending = this.pendingStart;
|
|
2782
|
+
if (pending !== null) {
|
|
2783
|
+
this.pendingStart = null;
|
|
2784
|
+
clearTimeout(pending.timer);
|
|
2785
|
+
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
ws.addEventListener("message", (ev) => {
|
|
2789
|
+
this.onData(ev.data);
|
|
2790
|
+
});
|
|
2791
|
+
});
|
|
2833
2792
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2793
|
+
send(msg) {
|
|
2794
|
+
if (this.closed || !this.ws) {
|
|
2795
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
const serialized = JSON.stringify(msg);
|
|
2799
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
|
|
2800
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
2801
|
+
this.stop();
|
|
2802
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
2803
|
+
}
|
|
2804
|
+
this.ws.send(serialized);
|
|
2805
|
+
return Promise.resolve();
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2808
|
+
}
|
|
2843
2809
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
const id = ctx.allocId();
|
|
2848
|
-
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
2849
|
-
if (isJsonRpcError(result)) {
|
|
2850
|
-
throw new ACPSessionError(
|
|
2851
|
-
"prompt_failed",
|
|
2852
|
-
`providers/disable failed: ${result.message}`,
|
|
2853
|
-
result
|
|
2854
|
-
);
|
|
2810
|
+
onMessage(handler) {
|
|
2811
|
+
this.handlers.add(handler);
|
|
2812
|
+
return () => this.handlers.delete(handler);
|
|
2855
2813
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2814
|
+
stop() {
|
|
2815
|
+
this.closed = true;
|
|
2816
|
+
this.handlers.clear();
|
|
2817
|
+
if (this.pendingStart !== null) {
|
|
2818
|
+
const pending = this.pendingStart;
|
|
2819
|
+
this.pendingStart = null;
|
|
2820
|
+
clearTimeout(pending.timer);
|
|
2821
|
+
try {
|
|
2822
|
+
pending.reject(new Error("WebSocket transport stopped while connecting"));
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
if (this.ws) {
|
|
2827
|
+
try {
|
|
2828
|
+
this.ws.close();
|
|
2829
|
+
} catch {
|
|
2830
|
+
}
|
|
2831
|
+
this.ws = null;
|
|
2832
|
+
}
|
|
2863
2833
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
"
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2834
|
+
onData(data) {
|
|
2835
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
2836
|
+
if (text.length > this.maxMessageChars) {
|
|
2837
|
+
this.stop();
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
if (!text.trim()) return;
|
|
2841
|
+
let msg;
|
|
2842
|
+
try {
|
|
2843
|
+
msg = JSON.parse(text);
|
|
2844
|
+
} catch {
|
|
2845
|
+
for (const line of text.split("\n")) {
|
|
2846
|
+
if (!line.trim()) continue;
|
|
2847
|
+
try {
|
|
2848
|
+
this.dispatch(JSON.parse(line));
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
this.dispatch(msg);
|
|
2879
2855
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2856
|
+
dispatch(msg) {
|
|
2857
|
+
for (const handler of [...this.handlers]) {
|
|
2858
|
+
try {
|
|
2859
|
+
handler(msg);
|
|
2860
|
+
} catch {
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2883
2863
|
}
|
|
2884
|
-
|
|
2864
|
+
};
|
|
2865
|
+
function finitePositiveLimit(value, fallback) {
|
|
2866
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
2885
2867
|
}
|
|
2886
2868
|
|
|
2887
2869
|
// src/client/acp-session.ts
|
|
@@ -4005,11 +3987,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4005
3987
|
options.onProgress?.(event);
|
|
4006
3988
|
};
|
|
4007
3989
|
try {
|
|
4008
|
-
const result = await session.prompt(
|
|
4009
|
-
[textContent(task.description)],
|
|
4010
|
-
ctx.signal,
|
|
4011
|
-
onProgress
|
|
4012
|
-
);
|
|
3990
|
+
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
4013
3991
|
return {
|
|
4014
3992
|
result: result.text,
|
|
4015
3993
|
iterations: 1,
|
|
@@ -4366,11 +4344,7 @@ var EnsembleRegistry = class {
|
|
|
4366
4344
|
if (this.cache && Date.now() - this.cache.at < PROBE_CACHE_MS) {
|
|
4367
4345
|
return this.cache.result;
|
|
4368
4346
|
}
|
|
4369
|
-
const result = await probeWithBound(
|
|
4370
|
-
this.catalog,
|
|
4371
|
-
(d) => this.detect(d),
|
|
4372
|
-
MAX_PARALLEL_PROBES
|
|
4373
|
-
);
|
|
4347
|
+
const result = await probeWithBound(this.catalog, (d) => this.detect(d), MAX_PARALLEL_PROBES);
|
|
4374
4348
|
this.cache = { at: Date.now(), result };
|
|
4375
4349
|
return result;
|
|
4376
4350
|
}
|