@wrongstack/acp 0.292.1 → 0.295.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/dist/agent/protocol-contract.d.ts +210 -0
- package/dist/agent/protocol-contract.d.ts.map +1 -0
- package/dist/agent/protocol-handler.d.ts +6 -189
- package/dist/agent/protocol-handler.d.ts.map +1 -1
- package/dist/agent/server-agent-turn.d.ts +7 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/stdio-transport.d.ts +12 -1
- package/dist/agent/stdio-transport.d.ts.map +1 -1
- package/dist/agent/tools-registry.d.ts +1 -1
- package/dist/agent/tools-registry.d.ts.map +1 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +2 -0
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +172 -22
- package/dist/agent.js.map +4 -4
- package/dist/client/acp-session.d.ts +19 -6
- package/dist/client/acp-session.d.ts.map +1 -1
- package/dist/client/index.d.ts +11 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/permission.d.ts +7 -3
- package/dist/client/permission.d.ts.map +1 -1
- package/dist/client/terminal-server.d.ts +5 -0
- package/dist/client/terminal-server.d.ts.map +1 -1
- package/dist/client/tool-translator.d.ts +1 -1
- package/dist/client/tool-translator.d.ts.map +1 -1
- package/dist/client/trust-boundary-permission.d.ts +16 -0
- package/dist/client/trust-boundary-permission.d.ts.map +1 -0
- package/dist/client/websocket-transport.d.ts +6 -0
- package/dist/client/websocket-transport.d.ts.map +1 -1
- package/dist/client.js +440 -225
- package/dist/client.js.map +4 -4
- package/dist/index.d.ts +29 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2433 -2121
- package/dist/index.js.map +4 -4
- package/dist/integration/acp-subagent-runner.d.ts +3 -3
- package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
- package/dist/integration/ensemble-runner.d.ts.map +1 -1
- package/dist/legacy.d.ts +8 -0
- package/dist/legacy.d.ts.map +1 -0
- package/dist/legacy.js +6 -0
- package/dist/legacy.js.map +7 -0
- package/dist/sdk.d.ts +10 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +22 -0
- package/dist/sdk.js.map +3 -3
- package/dist/v1.d.ts +3 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +12 -0
- package/dist/v1.js.map +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/wrongstack-acp-agent.js +107 -16
- package/dist/wrongstack-acp-agent.js.map +4 -4
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,628 +1,526 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
assertSafeWin32CmdArgs([command, ...args]);
|
|
8
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
9
|
-
return {
|
|
10
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
11
|
-
args: ["/d", "/c", line],
|
|
12
|
-
windowsVerbatimArguments: true
|
|
13
|
-
};
|
|
1
|
+
// src/types/acp-v1.ts
|
|
2
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
3
|
+
function assertNeverSessionUpdate(x) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`Unhandled sessionUpdate: ${JSON.stringify(x)}`
|
|
6
|
+
);
|
|
14
7
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
|
|
9
|
+
// src/version.ts
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
function readPackageVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const packageJson = require2("../package.json");
|
|
15
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
16
|
+
return packageJson.version;
|
|
21
17
|
}
|
|
18
|
+
} catch {
|
|
22
19
|
}
|
|
20
|
+
return "dev";
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
var ACP_PACKAGE_VERSION = readPackageVersion();
|
|
23
|
+
|
|
24
|
+
// src/agent/protocol-contract.ts
|
|
25
|
+
function toWire(msg) {
|
|
26
|
+
return msg;
|
|
26
27
|
}
|
|
28
|
+
var WRONGSTACK_VERSION = ACP_PACKAGE_VERSION;
|
|
27
29
|
|
|
28
|
-
// src/agent/
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
resolveRead = null;
|
|
37
|
-
messageQueue = [];
|
|
38
|
-
constructor() {
|
|
39
|
-
this.stdin.resume();
|
|
40
|
-
this.stdin.setEncoding("utf8");
|
|
41
|
-
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
42
|
-
this.stdin.on("end", () => this.handleClose());
|
|
43
|
-
this.stdin.on("error", (err) => this.failAll(err));
|
|
30
|
+
// src/agent/protocol-handler.ts
|
|
31
|
+
var WRONGSTACK_AUTH_METHODS = [
|
|
32
|
+
{
|
|
33
|
+
id: "wrongstack-auth",
|
|
34
|
+
name: "Run wstack auth",
|
|
35
|
+
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
36
|
+
type: "terminal",
|
|
37
|
+
args: ["auth"]
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
];
|
|
40
|
+
var DEFAULT_MODE_ID = "code";
|
|
41
|
+
var DEFAULT_MAX_SESSIONS = 64;
|
|
42
|
+
var DEFAULT_MODES = [
|
|
43
|
+
{
|
|
44
|
+
id: DEFAULT_MODE_ID,
|
|
45
|
+
name: "Code",
|
|
46
|
+
description: "Default agent mode for code-generation tasks."
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
];
|
|
49
|
+
var ACPProtocolHandler = class {
|
|
50
|
+
transport;
|
|
51
|
+
defaultCwd;
|
|
52
|
+
runTurn;
|
|
53
|
+
onSessionNew;
|
|
54
|
+
modes;
|
|
55
|
+
configOptions;
|
|
56
|
+
agentName;
|
|
57
|
+
replayFor;
|
|
58
|
+
seedFor;
|
|
59
|
+
disposeFor;
|
|
60
|
+
maxSessions;
|
|
61
|
+
store;
|
|
62
|
+
initialized = false;
|
|
63
|
+
clientCapabilities = {};
|
|
64
|
+
sessions = /* @__PURE__ */ new Map();
|
|
65
|
+
nextId = 1;
|
|
66
|
+
// Outbound request correlation (server → client requests, e.g.
|
|
67
|
+
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
68
|
+
pendingOut = /* @__PURE__ */ new Map();
|
|
69
|
+
nextOutId = 1;
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.transport = opts.transport;
|
|
72
|
+
this.defaultCwd = opts.defaultCwd;
|
|
73
|
+
this.runTurn = opts.runTurn;
|
|
74
|
+
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
53
75
|
});
|
|
76
|
+
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
77
|
+
this.configOptions = opts.configOptions ?? [];
|
|
78
|
+
this.agentName = opts.agentName ?? "wrongstack";
|
|
79
|
+
this.replayFor = opts.replayFor;
|
|
80
|
+
this.seedFor = opts.seedFor;
|
|
81
|
+
this.disposeFor = opts.disposeFor;
|
|
82
|
+
this.maxSessions = Number.isFinite(opts.maxSessions) && (opts.maxSessions ?? 0) > 0 ? Math.floor(opts.maxSessions) : DEFAULT_MAX_SESSIONS;
|
|
83
|
+
this.store = opts.store;
|
|
84
|
+
if (typeof this.transport.onMessage === "function") {
|
|
85
|
+
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
86
|
+
}
|
|
54
87
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Send a request to the client and await its response. Used for
|
|
90
|
+
* server-initiated calls like `session/request_permission`. Rejects on
|
|
91
|
+
* timeout or transport error so the caller can pick a safe fallback.
|
|
92
|
+
*/
|
|
93
|
+
request(method, params, timeoutMs = 6e4) {
|
|
94
|
+
const id = `srv_${this.nextOutId++}`;
|
|
95
|
+
return new Promise((resolve3, reject) => {
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
this.pendingOut.delete(id);
|
|
98
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
101
|
+
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
this.pendingOut.delete(id);
|
|
104
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
105
|
+
});
|
|
63
106
|
});
|
|
64
107
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
maybeResolvePending(m) {
|
|
109
|
+
const id = m.id;
|
|
110
|
+
if (typeof id !== "string") return;
|
|
111
|
+
const pending = this.pendingOut.get(id);
|
|
112
|
+
if (!pending) return;
|
|
113
|
+
this.pendingOut.delete(id);
|
|
114
|
+
clearTimeout(pending.timer);
|
|
115
|
+
const err = m.error;
|
|
116
|
+
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
117
|
+
else pending.resolve(m.result);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Process one inbound message. Returns true if this was a terminal
|
|
121
|
+
* message (rare; reserved for future use by the server's own
|
|
122
|
+
* shutdown signal).
|
|
123
|
+
*/
|
|
124
|
+
async handleMessage(msg) {
|
|
125
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
126
|
+
const m = msg;
|
|
127
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (m.id !== void 0 && typeof m.method === "string") {
|
|
131
|
+
return this.handleRequest(m.id, m.method, m.params);
|
|
132
|
+
}
|
|
133
|
+
if (typeof m.method === "string") {
|
|
134
|
+
return this.handleNotification(m.method, m.params);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
68
137
|
}
|
|
138
|
+
/** Abort all active turns and drop session state. */
|
|
69
139
|
close() {
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
for (const [sessionId, session] of this.sessions) {
|
|
141
|
+
session.abort.abort();
|
|
142
|
+
this.disposeSession(sessionId);
|
|
143
|
+
}
|
|
144
|
+
this.sessions.clear();
|
|
145
|
+
for (const [, p] of this.pendingOut) {
|
|
146
|
+
clearTimeout(p.timer);
|
|
147
|
+
p.reject(new Error("protocol handler closed"));
|
|
148
|
+
}
|
|
149
|
+
this.pendingOut.clear();
|
|
74
150
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const raw of lines) {
|
|
80
|
-
if (!raw.trim()) continue;
|
|
81
|
-
try {
|
|
82
|
-
this.dispatch(JSON.parse(raw));
|
|
83
|
-
} catch (err) {
|
|
84
|
-
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
85
|
-
`, "utf8");
|
|
86
|
-
}
|
|
151
|
+
disposeSession(sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
this.disposeFor?.(sessionId);
|
|
154
|
+
} catch {
|
|
87
155
|
}
|
|
88
156
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
157
|
+
// ────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Requests
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────
|
|
160
|
+
async handleRequest(id, method, params) {
|
|
161
|
+
if (method !== "initialize" && !this.initialized) {
|
|
162
|
+
await this.sendError(id, -32e3, "Not initialized");
|
|
163
|
+
return false;
|
|
96
164
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
165
|
+
try {
|
|
166
|
+
switch (method) {
|
|
167
|
+
case "initialize":
|
|
168
|
+
return await this.handleInitialize(id, params);
|
|
169
|
+
case "authenticate":
|
|
170
|
+
return await this.handleAuthenticate(id, params);
|
|
171
|
+
case "logout":
|
|
172
|
+
return await this.handleLogout(id, params);
|
|
173
|
+
case "session/new":
|
|
174
|
+
return await this.handleSessionNew(id, params);
|
|
175
|
+
case "session/load":
|
|
176
|
+
return await this.handleSessionLoad(id, params);
|
|
177
|
+
case "session/resume":
|
|
178
|
+
return await this.handleSessionResume(id, params);
|
|
179
|
+
case "session/close":
|
|
180
|
+
return await this.handleSessionClose(id, params);
|
|
181
|
+
case "session/delete":
|
|
182
|
+
return await this.handleSessionDelete(id, params);
|
|
183
|
+
case "session/prompt":
|
|
184
|
+
return await this.handleSessionPrompt(id, params);
|
|
185
|
+
case "session/set_mode":
|
|
186
|
+
return await this.handleSetMode(id, params);
|
|
187
|
+
case "session/set_config_option":
|
|
188
|
+
return await this.handleSetConfigOption(id, params);
|
|
189
|
+
case "session/list":
|
|
190
|
+
return await this.handleSessionList(id);
|
|
191
|
+
case "session/fork":
|
|
192
|
+
return await this.handleSessionFork(id, params);
|
|
193
|
+
case "providers/list":
|
|
194
|
+
return await this.handleProvidersList(id, params);
|
|
195
|
+
case "providers/set":
|
|
196
|
+
return await this.handleProvidersSet(id, params);
|
|
197
|
+
case "providers/disable":
|
|
198
|
+
return await this.handleProvidersDisable(id, params);
|
|
199
|
+
case "mcp/message":
|
|
200
|
+
return await this.handleMcpMessage(id, params);
|
|
201
|
+
default:
|
|
202
|
+
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
203
|
+
return false;
|
|
103
204
|
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
207
|
+
await this.sendError(id, code, message, data);
|
|
208
|
+
return false;
|
|
104
209
|
}
|
|
105
210
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
failAll(err) {
|
|
112
|
-
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
113
|
-
`, "utf8");
|
|
114
|
-
this.close();
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
var ClientTransport = class {
|
|
118
|
-
child = null;
|
|
119
|
-
buffer = "";
|
|
120
|
-
handlers = /* @__PURE__ */ new Set();
|
|
121
|
-
closed = false;
|
|
122
|
-
resolveRead = null;
|
|
123
|
-
messageQueue = [];
|
|
124
|
-
opts;
|
|
125
|
-
constructor(options) {
|
|
126
|
-
this.opts = {
|
|
127
|
-
handshakeTimeoutMs: 3e4,
|
|
128
|
-
...options
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async start() {
|
|
132
|
-
if (this.child) return;
|
|
133
|
-
const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
134
|
-
import("node:child_process"),
|
|
135
|
-
import("@wrongstack/core"),
|
|
136
|
-
import("node:os")
|
|
137
|
-
]);
|
|
138
|
-
return new Promise((resolve3, reject) => {
|
|
139
|
-
const timeout = setTimeout(() => {
|
|
140
|
-
reject(
|
|
141
|
-
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
142
|
-
);
|
|
143
|
-
}, this.opts.handshakeTimeoutMs);
|
|
144
|
-
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
145
|
-
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
146
|
-
try {
|
|
147
|
-
const childArgs = this.opts.args ?? [];
|
|
148
|
-
const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
|
|
149
|
-
this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
|
|
150
|
-
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
151
|
-
cwd: spawnCwd,
|
|
152
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
153
|
-
windowsHide: true,
|
|
154
|
-
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
155
|
-
});
|
|
156
|
-
} catch (err) {
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
reject(err);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const child = this.child;
|
|
162
|
-
child.stdout.setEncoding("utf8");
|
|
163
|
-
let settled = false;
|
|
164
|
-
const onSpawnFailure = (err) => {
|
|
165
|
-
if (settled) {
|
|
166
|
-
this.closed = true;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
settled = true;
|
|
170
|
-
clearTimeout(timeout);
|
|
171
|
-
reject(err);
|
|
172
|
-
};
|
|
173
|
-
child.on("error", onSpawnFailure);
|
|
174
|
-
child.stdout.on("error", onSpawnFailure);
|
|
175
|
-
if (this.opts.skipHandshakeMarker) {
|
|
176
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
177
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
178
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
179
|
-
child.once("spawn", () => {
|
|
180
|
-
if (settled) return;
|
|
181
|
-
settled = true;
|
|
182
|
-
clearTimeout(timeout);
|
|
183
|
-
resolve3();
|
|
184
|
-
});
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const onReady = () => {
|
|
188
|
-
if (settled) return;
|
|
189
|
-
settled = true;
|
|
190
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
191
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
192
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
193
|
-
clearTimeout(timeout);
|
|
194
|
-
resolve3();
|
|
195
|
-
};
|
|
196
|
-
const waitForMarker = (chunk) => {
|
|
197
|
-
this.buffer += chunk;
|
|
198
|
-
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
199
|
-
if (idx !== -1) {
|
|
200
|
-
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
201
|
-
child.stdout.removeListener("data", waitForMarker);
|
|
202
|
-
onReady();
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
child.stdout.on("data", waitForMarker);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
send(msg) {
|
|
209
|
-
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
210
|
-
return new Promise((resolve3, reject) => {
|
|
211
|
-
const line = JSON.stringify(msg) + "\n";
|
|
212
|
-
this.child?.stdin.write(line, "utf8", (err) => {
|
|
213
|
-
if (err) reject(err);
|
|
214
|
-
else resolve3();
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
read() {
|
|
219
|
-
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
220
|
-
if (this.closed) return Promise.resolve(null);
|
|
221
|
-
return new Promise((resolve3) => {
|
|
222
|
-
this.resolveRead = resolve3;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
onMessage(handler) {
|
|
226
|
-
this.handlers.add(handler);
|
|
227
|
-
return () => this.handlers.delete(handler);
|
|
228
|
-
}
|
|
229
|
-
stop() {
|
|
230
|
-
if (!this.child) return;
|
|
231
|
-
this.closed = true;
|
|
232
|
-
try {
|
|
233
|
-
this.child.kill();
|
|
234
|
-
} catch {
|
|
211
|
+
async handleInitialize(id, params) {
|
|
212
|
+
const p = params ?? {};
|
|
213
|
+
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
214
|
+
this.clientCapabilities = p.clientCapabilities;
|
|
235
215
|
}
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
216
|
+
this.initialized = true;
|
|
217
|
+
await this.transport.send(toWire({
|
|
218
|
+
jsonrpc: "2.0",
|
|
219
|
+
id,
|
|
220
|
+
result: {
|
|
221
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
222
|
+
agentCapabilities: {
|
|
223
|
+
loadSession: true,
|
|
224
|
+
promptCapabilities: {
|
|
225
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
226
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
227
|
+
// model can see them is the configured provider's concern.
|
|
228
|
+
image: true,
|
|
229
|
+
audio: false,
|
|
230
|
+
embeddedContext: true
|
|
231
|
+
},
|
|
232
|
+
mcpCapabilities: {
|
|
233
|
+
http: false,
|
|
234
|
+
sse: false
|
|
235
|
+
},
|
|
236
|
+
sessionCapabilities: {
|
|
237
|
+
close: {},
|
|
238
|
+
list: {},
|
|
239
|
+
delete: {},
|
|
240
|
+
resume: {},
|
|
241
|
+
fork: {}
|
|
242
|
+
},
|
|
243
|
+
auth: {
|
|
244
|
+
logout: {}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
agentInfo: {
|
|
248
|
+
name: this.agentName,
|
|
249
|
+
title: "WrongStack",
|
|
250
|
+
version: WRONGSTACK_VERSION
|
|
251
|
+
},
|
|
252
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
253
|
+
modes: this.modes,
|
|
254
|
+
configOptions: this.configOptions
|
|
247
255
|
}
|
|
248
|
-
}
|
|
256
|
+
}));
|
|
257
|
+
return false;
|
|
249
258
|
}
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
async handleAuthenticate(id, _params) {
|
|
260
|
+
await this.transport.send(toWire({
|
|
261
|
+
jsonrpc: "2.0",
|
|
262
|
+
id,
|
|
263
|
+
result: { outcome: "unauthenticated" }
|
|
264
|
+
}));
|
|
265
|
+
return false;
|
|
252
266
|
}
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
267
|
+
async handleLogout(id, _params) {
|
|
268
|
+
await this.transport.send(toWire({
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
result: {}
|
|
272
|
+
}));
|
|
273
|
+
return false;
|
|
261
274
|
}
|
|
262
|
-
|
|
263
|
-
if (this.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
resolve3(msg);
|
|
267
|
-
} else {
|
|
268
|
-
this.messageQueue.push(msg);
|
|
275
|
+
async handleSessionNew(id, params) {
|
|
276
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
277
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
278
|
+
return false;
|
|
269
279
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
const p = params ?? {};
|
|
281
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
282
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
283
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
|
+
const state = {
|
|
285
|
+
id: sessionId,
|
|
286
|
+
cwd,
|
|
287
|
+
abort: new AbortController(),
|
|
288
|
+
modeId: DEFAULT_MODE_ID,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now
|
|
291
|
+
};
|
|
292
|
+
this.sessions.set(sessionId, state);
|
|
293
|
+
this.onSessionNew(state);
|
|
294
|
+
await this.persist(state);
|
|
295
|
+
await this.sendNotification({
|
|
296
|
+
sessionId,
|
|
297
|
+
update: {
|
|
298
|
+
sessionUpdate: "current_mode_update",
|
|
299
|
+
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
274
300
|
}
|
|
301
|
+
});
|
|
302
|
+
if (this.configOptions.length > 0) {
|
|
303
|
+
await this.sendNotification({
|
|
304
|
+
sessionId,
|
|
305
|
+
update: {
|
|
306
|
+
sessionUpdate: "config_option_update",
|
|
307
|
+
configOptions: [...this.configOptions]
|
|
308
|
+
}
|
|
309
|
+
});
|
|
275
310
|
}
|
|
311
|
+
await this.transport.send(toWire({
|
|
312
|
+
jsonrpc: "2.0",
|
|
313
|
+
id,
|
|
314
|
+
result: {
|
|
315
|
+
sessionId,
|
|
316
|
+
modes: this.modes,
|
|
317
|
+
configOptions: this.configOptions
|
|
318
|
+
}
|
|
319
|
+
}));
|
|
320
|
+
return false;
|
|
276
321
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
322
|
+
async handleSessionLoad(id, params) {
|
|
323
|
+
const p = params ?? {};
|
|
324
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
325
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
326
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
327
|
+
if (!existing && sessionId && this.store) {
|
|
328
|
+
const persisted = await this.store.load(sessionId);
|
|
329
|
+
if (persisted) {
|
|
330
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
331
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const restored = {
|
|
335
|
+
id: sessionId,
|
|
336
|
+
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
337
|
+
abort: new AbortController(),
|
|
338
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
339
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
340
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
341
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
342
|
+
};
|
|
343
|
+
this.sessions.set(sessionId, restored);
|
|
344
|
+
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
345
|
+
for (const update of persisted.history ?? []) {
|
|
346
|
+
await this.sendNotification({ sessionId, update });
|
|
347
|
+
}
|
|
348
|
+
await this.sendNotification({
|
|
349
|
+
sessionId,
|
|
350
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
351
|
+
});
|
|
352
|
+
await this.transport.send(toWire({
|
|
353
|
+
jsonrpc: "2.0",
|
|
354
|
+
id,
|
|
355
|
+
result: {
|
|
356
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
293
361
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
list() {
|
|
309
|
-
return Array.from(this.tools.values());
|
|
310
|
-
}
|
|
311
|
-
/** Build the ACP tools/list payload from registered tools. */
|
|
312
|
-
buildToolList() {
|
|
313
|
-
return {
|
|
314
|
-
tools: Array.from(this.tools.values()).map(
|
|
315
|
-
(t) => toACPToolDefinition(t, this.owner)
|
|
316
|
-
)
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Execute a tool by name and return ACP-formatted result.
|
|
321
|
-
* Returns null if the tool is not found.
|
|
322
|
-
*/
|
|
323
|
-
async execute(name, args, ctx, signal) {
|
|
324
|
-
const tool = this.tools.get(name);
|
|
325
|
-
if (!tool) return null;
|
|
326
|
-
try {
|
|
327
|
-
const result = await tool.execute(args, ctx, {
|
|
328
|
-
signal
|
|
362
|
+
if (existing) {
|
|
363
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
364
|
+
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
365
|
+
if (replay) {
|
|
366
|
+
for (const update of replay) {
|
|
367
|
+
await this.sendNotification({ sessionId, update });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await this.sendNotification({
|
|
371
|
+
sessionId,
|
|
372
|
+
update: {
|
|
373
|
+
sessionUpdate: "session_info_update",
|
|
374
|
+
updatedAt: existing.updatedAt
|
|
375
|
+
}
|
|
329
376
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
377
|
+
await this.sendNotification({
|
|
378
|
+
sessionId,
|
|
379
|
+
update: {
|
|
380
|
+
sessionUpdate: "current_mode_update",
|
|
381
|
+
modeId: existing.modeId
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
await this.transport.send(toWire({
|
|
385
|
+
jsonrpc: "2.0",
|
|
386
|
+
id,
|
|
387
|
+
result: {
|
|
388
|
+
initialMode: {
|
|
389
|
+
currentModeId: existing.modeId,
|
|
390
|
+
availableModes: this.modes
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}));
|
|
394
|
+
return false;
|
|
334
395
|
}
|
|
396
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
397
|
+
return false;
|
|
335
398
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
399
|
+
async handleSessionResume(id, params) {
|
|
400
|
+
const p = params ?? {};
|
|
401
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
402
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
403
|
+
if (existing) {
|
|
404
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
405
|
+
await this.transport.send(toWire({
|
|
406
|
+
jsonrpc: "2.0",
|
|
407
|
+
id,
|
|
408
|
+
result: {
|
|
409
|
+
initialMode: {
|
|
410
|
+
currentModeId: existing.modeId,
|
|
411
|
+
availableModes: this.modes
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}));
|
|
415
|
+
return false;
|
|
347
416
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
function toACPInputSchema(src) {
|
|
351
|
-
if (!src || typeof src !== "object") {
|
|
352
|
-
return {};
|
|
417
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
418
|
+
return false;
|
|
353
419
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
362
|
-
if (s.items) out.items = toACPInputSchema(s.items);
|
|
363
|
-
if (s.properties && typeof s.properties === "object") {
|
|
364
|
-
const props = {};
|
|
365
|
-
for (const [k, v] of Object.entries(s.properties)) {
|
|
366
|
-
props[k] = toACPInputSchema(v);
|
|
420
|
+
async handleSessionClose(id, params) {
|
|
421
|
+
const p = params ?? {};
|
|
422
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
423
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
424
|
+
if (!session) {
|
|
425
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
426
|
+
return false;
|
|
367
427
|
}
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
blocks.push({ type: "text", text: result });
|
|
380
|
-
} else if (typeof result === "object") {
|
|
381
|
-
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
382
|
-
} else {
|
|
383
|
-
blocks.push({ type: "text", text: String(result) });
|
|
384
|
-
}
|
|
385
|
-
return { content: blocks };
|
|
386
|
-
}
|
|
387
|
-
function toolToPriority(tool) {
|
|
388
|
-
if (tool.riskTier === "destructive") return "high";
|
|
389
|
-
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
390
|
-
return "low";
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// src/types/acp-v1.ts
|
|
394
|
-
var ACP_PROTOCOL_VERSION = 1;
|
|
395
|
-
|
|
396
|
-
// src/agent/protocol-handler.ts
|
|
397
|
-
function toWire(msg) {
|
|
398
|
-
return msg;
|
|
399
|
-
}
|
|
400
|
-
var WRONGSTACK_VERSION = "0.274.1";
|
|
401
|
-
var WRONGSTACK_AUTH_METHODS = [
|
|
402
|
-
{
|
|
403
|
-
id: "wrongstack-auth",
|
|
404
|
-
name: "Run wstack auth",
|
|
405
|
-
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
406
|
-
type: "terminal",
|
|
407
|
-
args: ["auth"]
|
|
428
|
+
session.abort.abort();
|
|
429
|
+
if (sessionId) {
|
|
430
|
+
this.sessions.delete(sessionId);
|
|
431
|
+
this.disposeSession(sessionId);
|
|
432
|
+
}
|
|
433
|
+
await this.transport.send(toWire({
|
|
434
|
+
jsonrpc: "2.0",
|
|
435
|
+
id,
|
|
436
|
+
result: {}
|
|
437
|
+
}));
|
|
438
|
+
return false;
|
|
408
439
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
440
|
+
async handleSessionDelete(id, params) {
|
|
441
|
+
const p = params ?? {};
|
|
442
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
443
|
+
if (!sessionId) {
|
|
444
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (!this.sessions.has(sessionId)) {
|
|
448
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const session = this.sessions.get(sessionId);
|
|
452
|
+
session.abort.abort();
|
|
453
|
+
this.sessions.delete(sessionId);
|
|
454
|
+
this.disposeSession(sessionId);
|
|
455
|
+
await this.transport.send(toWire({
|
|
456
|
+
jsonrpc: "2.0",
|
|
457
|
+
id,
|
|
458
|
+
result: {}
|
|
459
|
+
}));
|
|
460
|
+
return false;
|
|
416
461
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
modes;
|
|
424
|
-
configOptions;
|
|
425
|
-
agentName;
|
|
426
|
-
replayFor;
|
|
427
|
-
seedFor;
|
|
428
|
-
store;
|
|
429
|
-
initialized = false;
|
|
430
|
-
clientCapabilities = {};
|
|
431
|
-
sessions = /* @__PURE__ */ new Map();
|
|
432
|
-
nextId = 1;
|
|
433
|
-
// Outbound request correlation (server → client requests, e.g.
|
|
434
|
-
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
435
|
-
pendingOut = /* @__PURE__ */ new Map();
|
|
436
|
-
nextOutId = 1;
|
|
437
|
-
constructor(opts) {
|
|
438
|
-
this.transport = opts.transport;
|
|
439
|
-
this.defaultCwd = opts.defaultCwd;
|
|
440
|
-
this.runTurn = opts.runTurn;
|
|
441
|
-
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
442
|
-
});
|
|
443
|
-
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
444
|
-
this.configOptions = opts.configOptions ?? [];
|
|
445
|
-
this.agentName = opts.agentName ?? "wrongstack";
|
|
446
|
-
this.replayFor = opts.replayFor;
|
|
447
|
-
this.seedFor = opts.seedFor;
|
|
448
|
-
this.store = opts.store;
|
|
449
|
-
if (typeof this.transport.onMessage === "function") {
|
|
450
|
-
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Send a request to the client and await its response. Used for
|
|
455
|
-
* server-initiated calls like `session/request_permission`. Rejects on
|
|
456
|
-
* timeout or transport error so the caller can pick a safe fallback.
|
|
457
|
-
*/
|
|
458
|
-
request(method, params, timeoutMs = 6e4) {
|
|
459
|
-
const id = `srv_${this.nextOutId++}`;
|
|
460
|
-
return new Promise((resolve3, reject) => {
|
|
461
|
-
const timer = setTimeout(() => {
|
|
462
|
-
this.pendingOut.delete(id);
|
|
463
|
-
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
464
|
-
}, timeoutMs);
|
|
465
|
-
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
466
|
-
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
this.pendingOut.delete(id);
|
|
469
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
maybeResolvePending(m) {
|
|
474
|
-
const id = m.id;
|
|
475
|
-
if (typeof id !== "string") return;
|
|
476
|
-
const pending = this.pendingOut.get(id);
|
|
477
|
-
if (!pending) return;
|
|
478
|
-
this.pendingOut.delete(id);
|
|
479
|
-
clearTimeout(pending.timer);
|
|
480
|
-
const err = m.error;
|
|
481
|
-
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
482
|
-
else pending.resolve(m.result);
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Process one inbound message. Returns true if this was a terminal
|
|
486
|
-
* message (rare; reserved for future use by the server's own
|
|
487
|
-
* shutdown signal).
|
|
488
|
-
*/
|
|
489
|
-
async handleMessage(msg) {
|
|
490
|
-
if (typeof msg !== "object" || msg === null) return false;
|
|
491
|
-
const m = msg;
|
|
492
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
if (m.id !== void 0 && typeof m.method === "string") {
|
|
496
|
-
return this.handleRequest(m.id, m.method, m.params);
|
|
497
|
-
}
|
|
498
|
-
if (typeof m.method === "string") {
|
|
499
|
-
return this.handleNotification(m.method, m.params);
|
|
500
|
-
}
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
/** Abort all active turns and drop session state. */
|
|
504
|
-
close() {
|
|
505
|
-
for (const [, session] of this.sessions) {
|
|
506
|
-
session.abort.abort();
|
|
507
|
-
}
|
|
508
|
-
this.sessions.clear();
|
|
509
|
-
for (const [, p] of this.pendingOut) {
|
|
510
|
-
clearTimeout(p.timer);
|
|
511
|
-
p.reject(new Error("protocol handler closed"));
|
|
512
|
-
}
|
|
513
|
-
this.pendingOut.clear();
|
|
514
|
-
}
|
|
515
|
-
// ────────────────────────────────────────────────────────────────────
|
|
516
|
-
// Requests
|
|
517
|
-
// ────────────────────────────────────────────────────────────────────
|
|
518
|
-
async handleRequest(id, method, params) {
|
|
519
|
-
if (method !== "initialize" && !this.initialized) {
|
|
520
|
-
await this.sendError(id, -32e3, "Not initialized");
|
|
462
|
+
async handleSessionFork(id, params) {
|
|
463
|
+
const p = params ?? {};
|
|
464
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
465
|
+
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
466
|
+
if (!sourceId || !source) {
|
|
467
|
+
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
521
468
|
return false;
|
|
522
469
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
case "initialize":
|
|
526
|
-
return await this.handleInitialize(id, params);
|
|
527
|
-
case "authenticate":
|
|
528
|
-
return await this.handleAuthenticate(id, params);
|
|
529
|
-
case "logout":
|
|
530
|
-
return await this.handleLogout(id, params);
|
|
531
|
-
case "session/new":
|
|
532
|
-
return await this.handleSessionNew(id, params);
|
|
533
|
-
case "session/load":
|
|
534
|
-
return await this.handleSessionLoad(id, params);
|
|
535
|
-
case "session/resume":
|
|
536
|
-
return await this.handleSessionResume(id, params);
|
|
537
|
-
case "session/close":
|
|
538
|
-
return await this.handleSessionClose(id, params);
|
|
539
|
-
case "session/delete":
|
|
540
|
-
return await this.handleSessionDelete(id, params);
|
|
541
|
-
case "session/prompt":
|
|
542
|
-
return await this.handleSessionPrompt(id, params);
|
|
543
|
-
case "session/set_mode":
|
|
544
|
-
return await this.handleSetMode(id, params);
|
|
545
|
-
case "session/set_config_option":
|
|
546
|
-
return await this.handleSetConfigOption(id, params);
|
|
547
|
-
case "session/list":
|
|
548
|
-
return await this.handleSessionList(id);
|
|
549
|
-
case "session/fork":
|
|
550
|
-
return await this.handleSessionFork(id, params);
|
|
551
|
-
case "providers/list":
|
|
552
|
-
return await this.handleProvidersList(id, params);
|
|
553
|
-
case "providers/set":
|
|
554
|
-
return await this.handleProvidersSet(id, params);
|
|
555
|
-
case "providers/disable":
|
|
556
|
-
return await this.handleProvidersDisable(id, params);
|
|
557
|
-
case "mcp/message":
|
|
558
|
-
return await this.handleMcpMessage(id, params);
|
|
559
|
-
default:
|
|
560
|
-
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
} catch (err) {
|
|
564
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
565
|
-
await this.sendError(id, code, message, data);
|
|
470
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
471
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
566
472
|
return false;
|
|
567
473
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
474
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
475
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
476
|
+
const forked = {
|
|
477
|
+
id: sessionId,
|
|
478
|
+
cwd: typeof p.cwd === "string" ? p.cwd : source.cwd,
|
|
479
|
+
abort: new AbortController(),
|
|
480
|
+
modeId: source.modeId,
|
|
481
|
+
createdAt: now,
|
|
482
|
+
updatedAt: now,
|
|
483
|
+
...source.title !== void 0 ? { title: source.title } : {}
|
|
484
|
+
};
|
|
485
|
+
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
486
|
+
sessionUpdate: update.sessionUpdate,
|
|
487
|
+
content: structuredClone(update.content)
|
|
488
|
+
}));
|
|
489
|
+
this.sessions.set(sessionId, forked);
|
|
490
|
+
this.seedFor?.(sessionId, history);
|
|
491
|
+
this.onSessionNew(forked);
|
|
492
|
+
await this.persist(forked, history);
|
|
493
|
+
await this.sendNotification({
|
|
494
|
+
sessionId,
|
|
495
|
+
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
496
|
+
});
|
|
575
497
|
await this.transport.send(toWire({
|
|
576
498
|
jsonrpc: "2.0",
|
|
577
499
|
id,
|
|
578
500
|
result: {
|
|
579
|
-
|
|
580
|
-
agentCapabilities: {
|
|
581
|
-
loadSession: true,
|
|
582
|
-
promptCapabilities: {
|
|
583
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
584
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
585
|
-
// model can see them is the configured provider's concern.
|
|
586
|
-
image: true,
|
|
587
|
-
audio: false,
|
|
588
|
-
embeddedContext: true
|
|
589
|
-
},
|
|
590
|
-
mcpCapabilities: {
|
|
591
|
-
http: false,
|
|
592
|
-
sse: false
|
|
593
|
-
},
|
|
594
|
-
sessionCapabilities: {
|
|
595
|
-
close: {},
|
|
596
|
-
list: {},
|
|
597
|
-
delete: {},
|
|
598
|
-
resume: {},
|
|
599
|
-
fork: {}
|
|
600
|
-
},
|
|
601
|
-
auth: {
|
|
602
|
-
logout: {}
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
agentInfo: {
|
|
606
|
-
name: this.agentName,
|
|
607
|
-
title: "WrongStack",
|
|
608
|
-
version: WRONGSTACK_VERSION
|
|
609
|
-
},
|
|
610
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
501
|
+
sessionId,
|
|
611
502
|
modes: this.modes,
|
|
612
503
|
configOptions: this.configOptions
|
|
613
504
|
}
|
|
614
505
|
}));
|
|
615
506
|
return false;
|
|
616
507
|
}
|
|
617
|
-
async
|
|
508
|
+
async handleProvidersList(id, _params) {
|
|
618
509
|
await this.transport.send(toWire({
|
|
619
510
|
jsonrpc: "2.0",
|
|
620
511
|
id,
|
|
621
|
-
result: {
|
|
512
|
+
result: {
|
|
513
|
+
providers: [],
|
|
514
|
+
currentProviderId: null
|
|
515
|
+
}
|
|
622
516
|
}));
|
|
623
517
|
return false;
|
|
624
518
|
}
|
|
625
|
-
async
|
|
519
|
+
async handleProvidersSet(id, _params) {
|
|
520
|
+
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
async handleProvidersDisable(id, _params) {
|
|
626
524
|
await this.transport.send(toWire({
|
|
627
525
|
jsonrpc: "2.0",
|
|
628
526
|
id,
|
|
@@ -630,251 +528,11 @@ var ACPProtocolHandler = class {
|
|
|
630
528
|
}));
|
|
631
529
|
return false;
|
|
632
530
|
}
|
|
633
|
-
async
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const state = {
|
|
639
|
-
id: sessionId,
|
|
640
|
-
cwd,
|
|
641
|
-
abort: new AbortController(),
|
|
642
|
-
modeId: DEFAULT_MODE_ID,
|
|
643
|
-
createdAt: now,
|
|
644
|
-
updatedAt: now
|
|
645
|
-
};
|
|
646
|
-
this.sessions.set(sessionId, state);
|
|
647
|
-
this.onSessionNew(state);
|
|
648
|
-
await this.persist(state);
|
|
649
|
-
await this.sendNotification({
|
|
650
|
-
sessionId,
|
|
651
|
-
update: {
|
|
652
|
-
sessionUpdate: "current_mode_update",
|
|
653
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
if (this.configOptions.length > 0) {
|
|
657
|
-
await this.sendNotification({
|
|
658
|
-
sessionId,
|
|
659
|
-
update: {
|
|
660
|
-
sessionUpdate: "config_option_update",
|
|
661
|
-
configOptions: [...this.configOptions]
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
await this.transport.send(toWire({
|
|
666
|
-
jsonrpc: "2.0",
|
|
667
|
-
id,
|
|
668
|
-
result: {
|
|
669
|
-
sessionId,
|
|
670
|
-
modes: this.modes,
|
|
671
|
-
configOptions: this.configOptions
|
|
672
|
-
}
|
|
673
|
-
}));
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
async handleSessionLoad(id, params) {
|
|
677
|
-
const p = params ?? {};
|
|
678
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
679
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
680
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
681
|
-
if (!existing && sessionId && this.store) {
|
|
682
|
-
const persisted = await this.store.load(sessionId);
|
|
683
|
-
if (persisted) {
|
|
684
|
-
const restored = {
|
|
685
|
-
id: sessionId,
|
|
686
|
-
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
687
|
-
abort: new AbortController(),
|
|
688
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
689
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
690
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
691
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
692
|
-
};
|
|
693
|
-
this.sessions.set(sessionId, restored);
|
|
694
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
695
|
-
for (const update of persisted.history ?? []) {
|
|
696
|
-
await this.sendNotification({ sessionId, update });
|
|
697
|
-
}
|
|
698
|
-
await this.sendNotification({
|
|
699
|
-
sessionId,
|
|
700
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
701
|
-
});
|
|
702
|
-
await this.transport.send(toWire({
|
|
703
|
-
jsonrpc: "2.0",
|
|
704
|
-
id,
|
|
705
|
-
result: {
|
|
706
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
707
|
-
}
|
|
708
|
-
}));
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (existing) {
|
|
713
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
714
|
-
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
715
|
-
if (replay) {
|
|
716
|
-
for (const update of replay) {
|
|
717
|
-
await this.sendNotification({ sessionId, update });
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
await this.sendNotification({
|
|
721
|
-
sessionId,
|
|
722
|
-
update: {
|
|
723
|
-
sessionUpdate: "session_info_update",
|
|
724
|
-
updatedAt: existing.updatedAt
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
await this.sendNotification({
|
|
728
|
-
sessionId,
|
|
729
|
-
update: {
|
|
730
|
-
sessionUpdate: "current_mode_update",
|
|
731
|
-
modeId: existing.modeId
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
await this.transport.send(toWire({
|
|
735
|
-
jsonrpc: "2.0",
|
|
736
|
-
id,
|
|
737
|
-
result: {
|
|
738
|
-
initialMode: {
|
|
739
|
-
currentModeId: existing.modeId,
|
|
740
|
-
availableModes: this.modes
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}));
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
async handleSessionResume(id, params) {
|
|
750
|
-
const p = params ?? {};
|
|
751
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
752
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
753
|
-
if (existing) {
|
|
754
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
755
|
-
await this.transport.send(toWire({
|
|
756
|
-
jsonrpc: "2.0",
|
|
757
|
-
id,
|
|
758
|
-
result: {
|
|
759
|
-
initialMode: {
|
|
760
|
-
currentModeId: existing.modeId,
|
|
761
|
-
availableModes: this.modes
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}));
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
async handleSessionClose(id, params) {
|
|
771
|
-
const p = params ?? {};
|
|
772
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
773
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
774
|
-
if (!session) {
|
|
775
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
776
|
-
return false;
|
|
777
|
-
}
|
|
778
|
-
session.abort.abort();
|
|
779
|
-
if (sessionId) this.sessions.delete(sessionId);
|
|
780
|
-
await this.transport.send(toWire({
|
|
781
|
-
jsonrpc: "2.0",
|
|
782
|
-
id,
|
|
783
|
-
result: {}
|
|
784
|
-
}));
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
async handleSessionDelete(id, params) {
|
|
788
|
-
const p = params ?? {};
|
|
789
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
790
|
-
if (!sessionId) {
|
|
791
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
if (!this.sessions.has(sessionId)) {
|
|
795
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
const session = this.sessions.get(sessionId);
|
|
799
|
-
session.abort.abort();
|
|
800
|
-
this.sessions.delete(sessionId);
|
|
801
|
-
await this.transport.send(toWire({
|
|
802
|
-
jsonrpc: "2.0",
|
|
803
|
-
id,
|
|
804
|
-
result: {}
|
|
805
|
-
}));
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
async handleSessionFork(id, params) {
|
|
809
|
-
const p = params ?? {};
|
|
810
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
811
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
812
|
-
if (!sourceId || !source) {
|
|
813
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
817
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
818
|
-
const forked = {
|
|
819
|
-
id: sessionId,
|
|
820
|
-
cwd: typeof p.cwd === "string" ? p.cwd : source.cwd,
|
|
821
|
-
abort: new AbortController(),
|
|
822
|
-
modeId: source.modeId,
|
|
823
|
-
createdAt: now,
|
|
824
|
-
updatedAt: now,
|
|
825
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
826
|
-
};
|
|
827
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
828
|
-
sessionUpdate: update.sessionUpdate,
|
|
829
|
-
content: structuredClone(update.content)
|
|
830
|
-
}));
|
|
831
|
-
this.sessions.set(sessionId, forked);
|
|
832
|
-
this.seedFor?.(sessionId, history);
|
|
833
|
-
this.onSessionNew(forked);
|
|
834
|
-
await this.persist(forked, history);
|
|
835
|
-
await this.sendNotification({
|
|
836
|
-
sessionId,
|
|
837
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
838
|
-
});
|
|
839
|
-
await this.transport.send(toWire({
|
|
840
|
-
jsonrpc: "2.0",
|
|
841
|
-
id,
|
|
842
|
-
result: {
|
|
843
|
-
sessionId,
|
|
844
|
-
modes: this.modes,
|
|
845
|
-
configOptions: this.configOptions
|
|
846
|
-
}
|
|
847
|
-
}));
|
|
848
|
-
return false;
|
|
849
|
-
}
|
|
850
|
-
async handleProvidersList(id, _params) {
|
|
851
|
-
await this.transport.send(toWire({
|
|
852
|
-
jsonrpc: "2.0",
|
|
853
|
-
id,
|
|
854
|
-
result: {
|
|
855
|
-
providers: [],
|
|
856
|
-
currentProviderId: null
|
|
857
|
-
}
|
|
858
|
-
}));
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
async handleProvidersSet(id, _params) {
|
|
862
|
-
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
async handleProvidersDisable(id, _params) {
|
|
866
|
-
await this.transport.send(toWire({
|
|
867
|
-
jsonrpc: "2.0",
|
|
868
|
-
id,
|
|
869
|
-
result: {}
|
|
870
|
-
}));
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
async handleMcpMessage(id, _params) {
|
|
874
|
-
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
async handleSessionPrompt(id, params) {
|
|
531
|
+
async handleMcpMessage(id, _params) {
|
|
532
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
async handleSessionPrompt(id, params) {
|
|
878
536
|
const p = params ?? {};
|
|
879
537
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
880
538
|
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
@@ -1078,259 +736,276 @@ function errorToJsonRpc(err) {
|
|
|
1078
736
|
return { code: -32603, message };
|
|
1079
737
|
}
|
|
1080
738
|
|
|
1081
|
-
// src/agent/
|
|
1082
|
-
import {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
1102
|
-
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
1103
|
-
...opts.store ? { store: opts.store } : {}
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Start the server. Mode depends on `options.transport`:
|
|
1108
|
-
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
1109
|
-
* - number: listens as HTTP on the given port.
|
|
1110
|
-
*/
|
|
1111
|
-
async start() {
|
|
1112
|
-
const transportMode = this.options.transport;
|
|
1113
|
-
if (typeof transportMode === "number") {
|
|
1114
|
-
await this.startHttp(transportMode);
|
|
1115
|
-
} else {
|
|
1116
|
-
await this.startStdio();
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
async startStdio() {
|
|
1120
|
-
if (this.options.legacyStartupMarker) {
|
|
1121
|
-
this.transport.sendStartupMarker();
|
|
1122
|
-
}
|
|
1123
|
-
this.running = true;
|
|
1124
|
-
while (this.running) {
|
|
1125
|
-
const msg = await this.transport.read();
|
|
1126
|
-
if (!msg) break;
|
|
1127
|
-
const terminal = await this.handler.handleMessage(msg);
|
|
1128
|
-
if (terminal) break;
|
|
739
|
+
// src/agent/stdio-transport.ts
|
|
740
|
+
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
741
|
+
|
|
742
|
+
// src/win32-cmd.ts
|
|
743
|
+
var WIN32_CMD_META = /[&|<>"\r\n\0]/;
|
|
744
|
+
function buildWin32CmdShimInvocation(command, args = []) {
|
|
745
|
+
assertSafeWin32CmdArgs([command, ...args]);
|
|
746
|
+
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
747
|
+
return {
|
|
748
|
+
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
749
|
+
args: ["/d", "/c", line],
|
|
750
|
+
windowsVerbatimArguments: true
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function assertSafeWin32CmdArgs(args) {
|
|
754
|
+
for (const arg of args) {
|
|
755
|
+
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
758
|
+
);
|
|
1129
759
|
}
|
|
1130
|
-
this.transport.close();
|
|
1131
760
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
761
|
+
}
|
|
762
|
+
function quoteWin32CmdArg(arg) {
|
|
763
|
+
return `"${arg}"`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/agent/stdio-transport.ts
|
|
767
|
+
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
768
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
769
|
+
function positiveLimit(value, fallback) {
|
|
770
|
+
return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
|
|
771
|
+
}
|
|
772
|
+
var StdioTransport = class {
|
|
773
|
+
stdin = process.stdin;
|
|
774
|
+
stdout = process.stdout;
|
|
775
|
+
stderr = process.stderr;
|
|
776
|
+
buffer = "";
|
|
777
|
+
handlers = /* @__PURE__ */ new Set();
|
|
778
|
+
closed = false;
|
|
779
|
+
resolveRead = null;
|
|
780
|
+
messageQueue = [];
|
|
781
|
+
maxFrameChars;
|
|
782
|
+
maxQueuedMessages;
|
|
783
|
+
constructor(opts = {}) {
|
|
784
|
+
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
785
|
+
this.maxQueuedMessages = positiveLimit(
|
|
786
|
+
opts.maxQueuedMessages,
|
|
787
|
+
DEFAULT_MAX_QUEUED_MESSAGES
|
|
788
|
+
);
|
|
789
|
+
this.stdin.resume();
|
|
790
|
+
this.stdin.setEncoding("utf8");
|
|
791
|
+
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
792
|
+
this.stdin.on("end", () => this.handleClose());
|
|
793
|
+
this.stdin.on("error", (err) => this.failAll(err));
|
|
794
|
+
}
|
|
795
|
+
sendStartupMarker() {
|
|
796
|
+
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
797
|
+
}
|
|
798
|
+
send(msg) {
|
|
799
|
+
if (this.closed) return Promise.resolve();
|
|
800
|
+
return new Promise((resolve3) => {
|
|
801
|
+
const line = JSON.stringify(msg) + "\n";
|
|
802
|
+
this.stdout.write(line, "utf8", () => resolve3());
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
sendRaw(chunk) {
|
|
806
|
+
this.stdout.write(chunk, "utf8");
|
|
807
|
+
}
|
|
808
|
+
read() {
|
|
809
|
+
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
810
|
+
if (this.closed) return Promise.resolve(null);
|
|
811
|
+
return new Promise((resolve3) => {
|
|
812
|
+
this.resolveRead = resolve3;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
onMessage(handler) {
|
|
816
|
+
this.handlers.add(handler);
|
|
817
|
+
return () => this.handlers.delete(handler);
|
|
818
|
+
}
|
|
819
|
+
close() {
|
|
820
|
+
this.closed = true;
|
|
821
|
+
this.stdin.pause();
|
|
822
|
+
this.resolveRead?.(null);
|
|
823
|
+
this.resolveRead = null;
|
|
824
|
+
this.buffer = "";
|
|
825
|
+
this.messageQueue.length = 0;
|
|
826
|
+
this.handlers.clear();
|
|
827
|
+
}
|
|
828
|
+
onData(chunk) {
|
|
829
|
+
this.buffer += chunk;
|
|
830
|
+
const lines = this.buffer.split("\n");
|
|
831
|
+
this.buffer = lines.pop() ?? "";
|
|
832
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
833
|
+
this.stderr.write(
|
|
834
|
+
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
835
|
+
`,
|
|
836
|
+
"utf8"
|
|
837
|
+
);
|
|
838
|
+
this.close();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
for (const raw of lines) {
|
|
842
|
+
if (!raw.trim()) continue;
|
|
843
|
+
if (raw.length > this.maxFrameChars) {
|
|
844
|
+
this.stderr.write(
|
|
845
|
+
`[wstack-acp frame error] frame exceeds ${this.maxFrameChars} characters
|
|
846
|
+
`,
|
|
847
|
+
"utf8"
|
|
848
|
+
);
|
|
849
|
+
this.close();
|
|
1185
850
|
return;
|
|
1186
851
|
}
|
|
1187
|
-
let msg;
|
|
1188
852
|
try {
|
|
1189
|
-
|
|
1190
|
-
} catch {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return;
|
|
853
|
+
this.dispatch(JSON.parse(raw));
|
|
854
|
+
} catch (err) {
|
|
855
|
+
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
856
|
+
`, "utf8");
|
|
1194
857
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
dispatch(msg) {
|
|
861
|
+
if (this.resolveRead) {
|
|
862
|
+
const resolve3 = this.resolveRead;
|
|
863
|
+
this.resolveRead = null;
|
|
864
|
+
resolve3(msg);
|
|
865
|
+
} else {
|
|
866
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
867
|
+
this.stderr.write(
|
|
868
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries
|
|
869
|
+
`,
|
|
870
|
+
"utf8"
|
|
871
|
+
);
|
|
872
|
+
this.close();
|
|
1203
873
|
return;
|
|
1204
874
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const originalSend = this.transport.send.bind(this.transport);
|
|
1209
|
-
this.transport.send = async (m) => {
|
|
1210
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1211
|
-
response = m;
|
|
1212
|
-
} else if (m.method === "session/update") {
|
|
1213
|
-
notifications.push(m.params);
|
|
1214
|
-
} else {
|
|
1215
|
-
notifications.push(m);
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
try {
|
|
1219
|
-
await handler.handleMessage(msg);
|
|
1220
|
-
} finally {
|
|
1221
|
-
this.transport.send = originalSend;
|
|
1222
|
-
}
|
|
1223
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1224
|
-
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1225
|
-
res.end(JSON.stringify(responseBody));
|
|
1226
|
-
});
|
|
1227
|
-
httpChain = requestPromise.catch(() => void 0);
|
|
875
|
+
this.messageQueue.push(msg);
|
|
876
|
+
}
|
|
877
|
+
for (const handler of this.handlers) {
|
|
1228
878
|
try {
|
|
1229
|
-
|
|
1230
|
-
} catch {
|
|
879
|
+
handler(msg);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
882
|
+
`, "utf8");
|
|
1231
883
|
}
|
|
1232
|
-
});
|
|
1233
|
-
return new Promise((resolve3) => {
|
|
1234
|
-
this.httpServer.listen(port, host, () => {
|
|
1235
|
-
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1236
|
-
`);
|
|
1237
|
-
this.running = true;
|
|
1238
|
-
resolve3();
|
|
1239
|
-
});
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
/** Stop the server. */
|
|
1243
|
-
stop() {
|
|
1244
|
-
this.running = false;
|
|
1245
|
-
this.transport.close();
|
|
1246
|
-
if (this.httpServer) {
|
|
1247
|
-
this.httpServer.close();
|
|
1248
|
-
this.httpServer = null;
|
|
1249
884
|
}
|
|
1250
885
|
}
|
|
886
|
+
handleClose() {
|
|
887
|
+
this.close();
|
|
888
|
+
}
|
|
889
|
+
failAll(err) {
|
|
890
|
+
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
891
|
+
`, "utf8");
|
|
892
|
+
this.close();
|
|
893
|
+
}
|
|
1251
894
|
};
|
|
1252
|
-
var
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
async function main() {
|
|
1256
|
-
const server = new WrongStackACPServer();
|
|
1257
|
-
await server.start();
|
|
1258
|
-
}
|
|
1259
|
-
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1260
|
-
if (isEntrypoint) {
|
|
1261
|
-
main().catch((err) => {
|
|
1262
|
-
writeErr2(`[wstack-acp fatal] ${err}
|
|
1263
|
-
`);
|
|
1264
|
-
process.exit(1);
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// src/client/websocket-transport.ts
|
|
1269
|
-
var WebSocketClientTransport = class {
|
|
1270
|
-
ws = null;
|
|
895
|
+
var ClientTransport = class {
|
|
896
|
+
child = null;
|
|
897
|
+
buffer = "";
|
|
1271
898
|
handlers = /* @__PURE__ */ new Set();
|
|
1272
899
|
closed = false;
|
|
900
|
+
resolveRead = null;
|
|
901
|
+
messageQueue = [];
|
|
1273
902
|
opts;
|
|
1274
|
-
|
|
1275
|
-
|
|
903
|
+
maxFrameChars;
|
|
904
|
+
maxQueuedMessages;
|
|
905
|
+
constructor(options) {
|
|
906
|
+
this.opts = {
|
|
907
|
+
handshakeTimeoutMs: 3e4,
|
|
908
|
+
...options
|
|
909
|
+
};
|
|
910
|
+
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
911
|
+
this.maxQueuedMessages = positiveLimit(
|
|
912
|
+
options.maxQueuedMessages,
|
|
913
|
+
DEFAULT_MAX_QUEUED_MESSAGES
|
|
914
|
+
);
|
|
1276
915
|
}
|
|
1277
|
-
start() {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
916
|
+
async start() {
|
|
917
|
+
if (this.child) return;
|
|
918
|
+
const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
919
|
+
import("node:child_process"),
|
|
920
|
+
import("@wrongstack/core/utils"),
|
|
921
|
+
import("node:os")
|
|
922
|
+
]);
|
|
1287
923
|
return new Promise((resolve3, reject) => {
|
|
924
|
+
const timeout = setTimeout(() => {
|
|
925
|
+
reject(
|
|
926
|
+
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
927
|
+
);
|
|
928
|
+
}, this.opts.handshakeTimeoutMs);
|
|
929
|
+
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
930
|
+
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
931
|
+
try {
|
|
932
|
+
const childArgs = this.opts.args ?? [];
|
|
933
|
+
const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
|
|
934
|
+
this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
|
|
935
|
+
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
936
|
+
cwd: spawnCwd,
|
|
937
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
938
|
+
windowsHide: true,
|
|
939
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
940
|
+
});
|
|
941
|
+
} catch (err) {
|
|
942
|
+
clearTimeout(timeout);
|
|
943
|
+
reject(err);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const child = this.child;
|
|
947
|
+
child.stdout.setEncoding("utf8");
|
|
1288
948
|
let settled = false;
|
|
1289
|
-
const
|
|
1290
|
-
this.ws = ws;
|
|
1291
|
-
const timer = setTimeout(() => {
|
|
1292
|
-
if (settled) return;
|
|
1293
|
-
settled = true;
|
|
1294
|
-
try {
|
|
1295
|
-
ws.close();
|
|
1296
|
-
} catch {
|
|
1297
|
-
}
|
|
1298
|
-
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1299
|
-
}, timeoutMs);
|
|
1300
|
-
ws.addEventListener("open", () => {
|
|
1301
|
-
if (settled) return;
|
|
1302
|
-
settled = true;
|
|
1303
|
-
clearTimeout(timer);
|
|
1304
|
-
resolve3();
|
|
1305
|
-
});
|
|
1306
|
-
ws.addEventListener("error", (ev) => {
|
|
949
|
+
const onSpawnFailure = (err) => {
|
|
1307
950
|
if (settled) {
|
|
1308
951
|
this.closed = true;
|
|
1309
952
|
return;
|
|
1310
953
|
}
|
|
1311
954
|
settled = true;
|
|
1312
|
-
clearTimeout(
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
this.
|
|
1321
|
-
|
|
955
|
+
clearTimeout(timeout);
|
|
956
|
+
reject(err);
|
|
957
|
+
};
|
|
958
|
+
child.on("error", onSpawnFailure);
|
|
959
|
+
child.stdout.on("error", onSpawnFailure);
|
|
960
|
+
if (this.opts.skipHandshakeMarker) {
|
|
961
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
962
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
963
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
964
|
+
child.once("spawn", () => {
|
|
965
|
+
if (settled) return;
|
|
966
|
+
settled = true;
|
|
967
|
+
clearTimeout(timeout);
|
|
968
|
+
resolve3();
|
|
969
|
+
});
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const onReady = () => {
|
|
973
|
+
if (settled) return;
|
|
974
|
+
settled = true;
|
|
975
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
976
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
977
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
978
|
+
clearTimeout(timeout);
|
|
979
|
+
resolve3();
|
|
980
|
+
};
|
|
981
|
+
const waitForMarker = (chunk) => {
|
|
982
|
+
this.buffer += chunk;
|
|
983
|
+
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
984
|
+
if (idx !== -1) {
|
|
985
|
+
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
986
|
+
child.stdout.removeListener("data", waitForMarker);
|
|
987
|
+
onReady();
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
child.stdout.on("data", waitForMarker);
|
|
1322
991
|
});
|
|
1323
992
|
}
|
|
1324
993
|
send(msg) {
|
|
1325
|
-
if (this.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
994
|
+
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
995
|
+
return new Promise((resolve3, reject) => {
|
|
996
|
+
const line = JSON.stringify(msg) + "\n";
|
|
997
|
+
this.child?.stdin.write(line, "utf8", (err) => {
|
|
998
|
+
if (err) reject(err);
|
|
999
|
+
else resolve3();
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
read() {
|
|
1004
|
+
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
1005
|
+
if (this.closed) return Promise.resolve(null);
|
|
1006
|
+
return new Promise((resolve3) => {
|
|
1007
|
+
this.resolveRead = resolve3;
|
|
1008
|
+
});
|
|
1334
1009
|
}
|
|
1335
1010
|
onMessage(handler) {
|
|
1336
1011
|
this.handlers.add(handler);
|
|
@@ -1338,34 +1013,73 @@ var WebSocketClientTransport = class {
|
|
|
1338
1013
|
}
|
|
1339
1014
|
stop() {
|
|
1340
1015
|
this.closed = true;
|
|
1341
|
-
|
|
1016
|
+
this.resolveRead?.(null);
|
|
1017
|
+
this.resolveRead = null;
|
|
1018
|
+
this.buffer = "";
|
|
1019
|
+
this.messageQueue.length = 0;
|
|
1020
|
+
this.handlers.clear();
|
|
1021
|
+
const child = this.child;
|
|
1022
|
+
if (!child) return;
|
|
1023
|
+
try {
|
|
1024
|
+
child.kill();
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
this.child = null;
|
|
1028
|
+
}
|
|
1029
|
+
onChildData(chunk) {
|
|
1030
|
+
this.buffer += chunk;
|
|
1031
|
+
const lines = this.buffer.split("\n");
|
|
1032
|
+
this.buffer = lines.pop() ?? "";
|
|
1033
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
1034
|
+
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
1035
|
+
`);
|
|
1036
|
+
this.stop();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
for (const raw of lines) {
|
|
1040
|
+
if (!raw.trim()) continue;
|
|
1041
|
+
if (raw.length > this.maxFrameChars) {
|
|
1042
|
+
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
1043
|
+
`);
|
|
1044
|
+
this.stop();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1342
1047
|
try {
|
|
1343
|
-
this.
|
|
1048
|
+
this.dispatch(JSON.parse(raw));
|
|
1344
1049
|
} catch {
|
|
1345
1050
|
}
|
|
1346
|
-
this.ws = null;
|
|
1347
1051
|
}
|
|
1348
1052
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
return;
|
|
1053
|
+
onChildError(chunk) {
|
|
1054
|
+
writeErr(`[acp-child stderr] ${chunk}`);
|
|
1055
|
+
}
|
|
1056
|
+
onChildClose(code) {
|
|
1057
|
+
this.closed = true;
|
|
1058
|
+
this.resolveRead?.(null);
|
|
1059
|
+
this.resolveRead = null;
|
|
1060
|
+
this.buffer = "";
|
|
1061
|
+
this.messageQueue.length = 0;
|
|
1062
|
+
this.handlers.clear();
|
|
1063
|
+
if (code !== 0 && code !== null) {
|
|
1064
|
+
writeErr(`[acp-child exited with code ${code}]
|
|
1065
|
+
`);
|
|
1364
1066
|
}
|
|
1365
|
-
this.dispatch(msg);
|
|
1366
1067
|
}
|
|
1367
1068
|
dispatch(msg) {
|
|
1368
|
-
|
|
1069
|
+
if (this.resolveRead) {
|
|
1070
|
+
const resolve3 = this.resolveRead;
|
|
1071
|
+
this.resolveRead = null;
|
|
1072
|
+
resolve3(msg);
|
|
1073
|
+
} else if (this.handlers.size === 0) {
|
|
1074
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
1075
|
+
writeErr(`[acp-child message queue exceeds ${this.maxQueuedMessages} entries]
|
|
1076
|
+
`);
|
|
1077
|
+
this.stop();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
this.messageQueue.push(msg);
|
|
1081
|
+
}
|
|
1082
|
+
for (const handler of this.handlers) {
|
|
1369
1083
|
try {
|
|
1370
1084
|
handler(msg);
|
|
1371
1085
|
} catch {
|
|
@@ -1374,495 +1088,978 @@ var WebSocketClientTransport = class {
|
|
|
1374
1088
|
}
|
|
1375
1089
|
};
|
|
1376
1090
|
|
|
1377
|
-
// src/
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
};
|
|
1384
|
-
var ToolTranslator = class {
|
|
1385
|
-
opts;
|
|
1386
|
-
pending = /* @__PURE__ */ new Map();
|
|
1387
|
-
constructor(opts = {}) {
|
|
1388
|
-
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
1091
|
+
// src/agent/tools-registry.ts
|
|
1092
|
+
var ACPToolsRegistry = class {
|
|
1093
|
+
tools = /* @__PURE__ */ new Map();
|
|
1094
|
+
owner;
|
|
1095
|
+
constructor(owner = "wrongstack") {
|
|
1096
|
+
this.owner = owner;
|
|
1389
1097
|
}
|
|
1390
1098
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1099
|
+
* Register one or more tools.
|
|
1100
|
+
* Throws on duplicate name unless force=true.
|
|
1393
1101
|
*/
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if (pending) {
|
|
1399
|
-
clearTimeout(pending.timeout);
|
|
1400
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
1401
|
-
pending.resolve(msg);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
1405
|
-
const pending = this.pending.get(msg.id);
|
|
1406
|
-
if (pending) {
|
|
1407
|
-
clearTimeout(pending.timeout);
|
|
1408
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
1409
|
-
pending.reject(new Error("Call cancelled by client"));
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
});
|
|
1102
|
+
register(tools) {
|
|
1103
|
+
for (const tool of tools) {
|
|
1104
|
+
this.tools.set(tool.name, tool);
|
|
1105
|
+
}
|
|
1413
1106
|
}
|
|
1414
1107
|
/**
|
|
1415
|
-
*
|
|
1416
|
-
* If asyncTools is true, polls for progress and resolves when the final
|
|
1417
|
-
* response arrives.
|
|
1108
|
+
* Replace the current tool set.
|
|
1418
1109
|
*/
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
method: "tools/call",
|
|
1423
|
-
id: callId,
|
|
1424
|
-
params: { name, arguments: args }
|
|
1425
|
-
});
|
|
1426
|
-
return new Promise((resolve3, reject) => {
|
|
1427
|
-
const timeout = setTimeout(() => {
|
|
1428
|
-
this.pending.delete(callId);
|
|
1429
|
-
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
1430
|
-
}, this.opts.totalTimeoutMs);
|
|
1431
|
-
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
1432
|
-
});
|
|
1110
|
+
setTools(tools) {
|
|
1111
|
+
this.tools.clear();
|
|
1112
|
+
for (const tool of tools) this.tools.set(tool.name, tool);
|
|
1433
1113
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
clearTimeout(p.timeout);
|
|
1437
|
-
}
|
|
1438
|
-
this.pending.clear();
|
|
1114
|
+
get(name) {
|
|
1115
|
+
return this.tools.get(name);
|
|
1439
1116
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
// src/client/file-server.ts
|
|
1443
|
-
import { randomBytes } from "node:crypto";
|
|
1444
|
-
import { realpathSync } from "node:fs";
|
|
1445
|
-
import * as fsp from "node:fs/promises";
|
|
1446
|
-
import * as path from "node:path";
|
|
1447
|
-
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1448
|
-
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1449
|
-
var FsError = class extends Error {
|
|
1450
|
-
code;
|
|
1451
|
-
path;
|
|
1452
|
-
constructor(code, path4, message) {
|
|
1453
|
-
super(message);
|
|
1454
|
-
this.name = "FsError";
|
|
1455
|
-
this.code = code;
|
|
1456
|
-
this.path = path4;
|
|
1117
|
+
has(name) {
|
|
1118
|
+
return this.tools.has(name);
|
|
1457
1119
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
root;
|
|
1461
|
-
realRoot;
|
|
1462
|
-
timeoutMs;
|
|
1463
|
-
maxReadBytes;
|
|
1464
|
-
maxWriteBytes;
|
|
1465
|
-
constructor(opts) {
|
|
1466
|
-
this.root = path.resolve(opts.projectRoot);
|
|
1467
|
-
this.realRoot = safeRealpathSync(this.root);
|
|
1468
|
-
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1469
|
-
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1470
|
-
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
1120
|
+
list() {
|
|
1121
|
+
return Array.from(this.tools.values());
|
|
1471
1122
|
}
|
|
1472
|
-
/**
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
throw mapFsError(err, safe);
|
|
1480
|
-
});
|
|
1481
|
-
if (stat2.size > this.maxReadBytes) {
|
|
1482
|
-
throw new FsError(
|
|
1483
|
-
"TOO_LARGE",
|
|
1484
|
-
safe,
|
|
1485
|
-
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
const content = await fsp.readFile(safe, {
|
|
1489
|
-
encoding: "utf8",
|
|
1490
|
-
signal: controller.signal
|
|
1491
|
-
});
|
|
1492
|
-
return { content };
|
|
1493
|
-
} catch (err) {
|
|
1494
|
-
if (err instanceof FsError) throw err;
|
|
1495
|
-
if (controller.signal.aborted) {
|
|
1496
|
-
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1497
|
-
}
|
|
1498
|
-
throw mapFsError(err, safe);
|
|
1499
|
-
} finally {
|
|
1500
|
-
clearTimeout(timer);
|
|
1501
|
-
}
|
|
1123
|
+
/** Build the ACP tools/list payload from registered tools. */
|
|
1124
|
+
buildToolList() {
|
|
1125
|
+
return {
|
|
1126
|
+
tools: Array.from(this.tools.values()).map(
|
|
1127
|
+
(t) => toACPToolDefinition(t, this.owner)
|
|
1128
|
+
)
|
|
1129
|
+
};
|
|
1502
1130
|
}
|
|
1503
|
-
/**
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
const safe = await this.resolveInside(params.path);
|
|
1514
|
-
const controller = new AbortController();
|
|
1515
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1516
|
-
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1131
|
+
/**
|
|
1132
|
+
* Execute a tool by name and return ACP-formatted result.
|
|
1133
|
+
* Returns null if the tool is not found.
|
|
1134
|
+
*/
|
|
1135
|
+
async execute(name, args, ctx, signal) {
|
|
1136
|
+
const tool = this.tools.get(name);
|
|
1137
|
+
if (!tool) return null;
|
|
1517
1138
|
try {
|
|
1518
|
-
await
|
|
1519
|
-
|
|
1520
|
-
signal: controller.signal
|
|
1139
|
+
const result = await tool.execute(args, ctx, {
|
|
1140
|
+
signal
|
|
1521
1141
|
});
|
|
1522
|
-
|
|
1523
|
-
await this.assertRealInside(path.dirname(safe));
|
|
1524
|
-
await fsp.rename(tmp, safe);
|
|
1142
|
+
return toACPToolResult(result);
|
|
1525
1143
|
} catch (err) {
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
throw err;
|
|
1529
|
-
}
|
|
1530
|
-
try {
|
|
1531
|
-
await fsp.unlink(tmp);
|
|
1532
|
-
} catch {
|
|
1533
|
-
}
|
|
1534
|
-
if (controller.signal.aborted) {
|
|
1535
|
-
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
1536
|
-
}
|
|
1537
|
-
throw mapFsError(err, safe);
|
|
1538
|
-
} finally {
|
|
1539
|
-
clearTimeout(timer);
|
|
1144
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1145
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
1540
1146
|
}
|
|
1541
1147
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
}
|
|
1554
|
-
if (!path.isAbsolute(p)) {
|
|
1555
|
-
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
1556
|
-
}
|
|
1557
|
-
const resolved = path.resolve(p);
|
|
1558
|
-
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
1559
|
-
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
1560
|
-
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
1148
|
+
};
|
|
1149
|
+
function toACPToolDefinition(tool, _owner) {
|
|
1150
|
+
return {
|
|
1151
|
+
name: tool.name,
|
|
1152
|
+
description: tool.description,
|
|
1153
|
+
inputSchema: toACPInputSchema(tool.inputSchema),
|
|
1154
|
+
annotations: {
|
|
1155
|
+
title: tool.name,
|
|
1156
|
+
description: tool.usageHint ?? tool.description,
|
|
1157
|
+
priority: toolToPriority(tool),
|
|
1158
|
+
alwaysAccept: tool.permission === "auto"
|
|
1561
1159
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function toACPInputSchema(src) {
|
|
1163
|
+
if (!src || typeof src !== "object") {
|
|
1164
|
+
return {};
|
|
1564
1165
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
if (code === "ENOENT") {
|
|
1579
|
-
const parent = path.dirname(probe);
|
|
1580
|
-
if (parent === probe) return;
|
|
1581
|
-
probe = parent;
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
throw mapFsError(err, resolvedPath);
|
|
1585
|
-
}
|
|
1586
|
-
if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
|
|
1587
|
-
throw new FsError(
|
|
1588
|
-
"OUTSIDE_ROOT",
|
|
1589
|
-
resolvedPath,
|
|
1590
|
-
"path resolves through a symlink outside the project root"
|
|
1591
|
-
);
|
|
1166
|
+
const s = src;
|
|
1167
|
+
const out = {};
|
|
1168
|
+
if (typeof s.type === "string") out.type = s.type;
|
|
1169
|
+
if (Array.isArray(s.enum)) out.enum = s.enum;
|
|
1170
|
+
if (typeof s.description === "string") out.description = s.description;
|
|
1171
|
+
if ("default" in s) out.default = s.default;
|
|
1172
|
+
if (typeof s.minimum === "number") out.minimum = s.minimum;
|
|
1173
|
+
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
1174
|
+
if (s.items) out.items = toACPInputSchema(s.items);
|
|
1175
|
+
if (s.properties && typeof s.properties === "object") {
|
|
1176
|
+
const props = {};
|
|
1177
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
1178
|
+
props[k] = toACPInputSchema(v);
|
|
1592
1179
|
}
|
|
1180
|
+
out.properties = props;
|
|
1181
|
+
if (Array.isArray(s.required)) out.required = s.required;
|
|
1593
1182
|
}
|
|
1594
|
-
|
|
1595
|
-
function mapFsError(err, p) {
|
|
1596
|
-
const code = err?.code;
|
|
1597
|
-
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
1598
|
-
if (code === "EACCES" || code === "EPERM") {
|
|
1599
|
-
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
1600
|
-
}
|
|
1601
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1602
|
-
return new FsError("INVALID_PATH", p, msg);
|
|
1183
|
+
return out;
|
|
1603
1184
|
}
|
|
1604
|
-
function
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
return p;
|
|
1185
|
+
function toACPToolResult(result) {
|
|
1186
|
+
const blocks = [];
|
|
1187
|
+
if (result === void 0 || result === null) {
|
|
1188
|
+
return { content: [{ type: "text", text: "ok" }] };
|
|
1609
1189
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (k === "allow_once") return 0;
|
|
1617
|
-
if (k === "allow_always") return 1;
|
|
1618
|
-
if (k === "reject_once") return 2;
|
|
1619
|
-
return 3;
|
|
1620
|
-
};
|
|
1621
|
-
return score(a.kind) - score(b.kind);
|
|
1622
|
-
});
|
|
1623
|
-
const chosen = ranked[0];
|
|
1624
|
-
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1625
|
-
return { outcome: "cancelled" };
|
|
1190
|
+
if (typeof result === "string") {
|
|
1191
|
+
blocks.push({ type: "text", text: result });
|
|
1192
|
+
} else if (typeof result === "object") {
|
|
1193
|
+
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
1194
|
+
} else {
|
|
1195
|
+
blocks.push({ type: "text", text: String(result) });
|
|
1626
1196
|
}
|
|
1627
|
-
return {
|
|
1197
|
+
return { content: blocks };
|
|
1628
1198
|
}
|
|
1629
|
-
function
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
1634
|
-
}
|
|
1635
|
-
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
1636
|
-
var defaultPermissionPolicy = async (req) => {
|
|
1637
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1638
|
-
return pickAllow(req.options);
|
|
1639
|
-
};
|
|
1640
|
-
var readOnlyPermissionPolicy = async (req) => {
|
|
1641
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1642
|
-
const kind = req.toolCall.kind;
|
|
1643
|
-
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1644
|
-
return pickAllow(req.options);
|
|
1645
|
-
}
|
|
1646
|
-
return pickReject(req.options);
|
|
1647
|
-
};
|
|
1648
|
-
function makePermissionPolicy(decide) {
|
|
1649
|
-
return async (req) => {
|
|
1650
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1651
|
-
const allow = await decide(req);
|
|
1652
|
-
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1653
|
-
};
|
|
1199
|
+
function toolToPriority(tool) {
|
|
1200
|
+
if (tool.riskTier === "destructive") return "high";
|
|
1201
|
+
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
1202
|
+
return "low";
|
|
1654
1203
|
}
|
|
1655
1204
|
|
|
1656
|
-
// src/
|
|
1657
|
-
import {
|
|
1658
|
-
import {
|
|
1659
|
-
import
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
this.
|
|
1670
|
-
|
|
1671
|
-
this.
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const cwd = this.resolveCwd(params.cwd);
|
|
1681
|
-
const proc = spawn(params.command, params.args ?? [], {
|
|
1682
|
-
cwd,
|
|
1683
|
-
env: this.buildEnv(params.env),
|
|
1684
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1685
|
-
windowsHide: true
|
|
1686
|
-
// shell: false on purpose. The terminal server is invoked with
|
|
1687
|
-
// the agent's explicit argv; turning on shell-mode would make
|
|
1688
|
-
// the command a single shell-parsed string, which breaks
|
|
1689
|
-
// Windows cmd quoting for the common case of running node with
|
|
1690
|
-
// `-e "<script>"`. If a future feature needs shell features
|
|
1691
|
-
// (pipes, redirects), it should be opt-in per-call, not the
|
|
1692
|
-
// default.
|
|
1205
|
+
// src/agent/wrongstack-acp-agent.ts
|
|
1206
|
+
import { fileURLToPath } from "node:url";
|
|
1207
|
+
import { createServer } from "node:http";
|
|
1208
|
+
import { writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1209
|
+
var WrongStackACPServer = class {
|
|
1210
|
+
transport;
|
|
1211
|
+
handler;
|
|
1212
|
+
options;
|
|
1213
|
+
/** HTTP server when transport mode is HTTP. */
|
|
1214
|
+
httpServer = null;
|
|
1215
|
+
running = false;
|
|
1216
|
+
constructor(opts = {}) {
|
|
1217
|
+
this.options = opts;
|
|
1218
|
+
this.transport = new StdioTransport();
|
|
1219
|
+
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
1220
|
+
this.handler = new ACPProtocolHandler({
|
|
1221
|
+
transport: this.transport,
|
|
1222
|
+
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
1223
|
+
runTurn,
|
|
1224
|
+
agentName: opts.agentName,
|
|
1225
|
+
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
1226
|
+
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
1227
|
+
...opts.disposeFor ? { disposeFor: opts.disposeFor } : {},
|
|
1228
|
+
...opts.store ? { store: opts.store } : {}
|
|
1693
1229
|
});
|
|
1694
|
-
const state = {
|
|
1695
|
-
proc,
|
|
1696
|
-
cwd,
|
|
1697
|
-
command: params.command,
|
|
1698
|
-
args: params.args ?? [],
|
|
1699
|
-
output: "",
|
|
1700
|
-
retainedBytes: 0,
|
|
1701
|
-
truncated: false,
|
|
1702
|
-
exitStatus: void 0,
|
|
1703
|
-
timeoutHandle: null,
|
|
1704
|
-
exitPromise: new Promise((resolve3) => {
|
|
1705
|
-
proc.on("close", (code, signalName) => {
|
|
1706
|
-
if (state.timeoutHandle) {
|
|
1707
|
-
clearTimeout(state.timeoutHandle);
|
|
1708
|
-
state.timeoutHandle = null;
|
|
1709
|
-
}
|
|
1710
|
-
const exitStatus = {
|
|
1711
|
-
exitCode: typeof code === "number" ? code : null,
|
|
1712
|
-
signal: typeof signalName === "string" ? signalName : null
|
|
1713
|
-
};
|
|
1714
|
-
state.exitStatus = exitStatus;
|
|
1715
|
-
resolve3(exitStatus);
|
|
1716
|
-
});
|
|
1717
|
-
proc.on("error", (err) => {
|
|
1718
|
-
if (state.timeoutHandle) {
|
|
1719
|
-
clearTimeout(state.timeoutHandle);
|
|
1720
|
-
state.timeoutHandle = null;
|
|
1721
|
-
}
|
|
1722
|
-
const exitStatus = { exitCode: 127, signal: null };
|
|
1723
|
-
state.exitStatus = exitStatus;
|
|
1724
|
-
state.output += `[spawn error] ${err.message}
|
|
1725
|
-
`;
|
|
1726
|
-
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
1727
|
-
resolve3(exitStatus);
|
|
1728
|
-
});
|
|
1729
|
-
})
|
|
1730
|
-
};
|
|
1731
|
-
const perCallByteLimit = Math.min(
|
|
1732
|
-
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1733
|
-
this.maxOutputByteLimit
|
|
1734
|
-
);
|
|
1735
|
-
proc.stdout?.setEncoding("utf8");
|
|
1736
|
-
proc.stderr?.setEncoding("utf8");
|
|
1737
|
-
const onData = (chunk) => {
|
|
1738
|
-
state.output += chunk;
|
|
1739
|
-
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
1740
|
-
while (state.retainedBytes > perCallByteLimit) {
|
|
1741
|
-
const trimmed = state.output.slice(1);
|
|
1742
|
-
state.output = trimmed;
|
|
1743
|
-
const newBytes = Buffer.byteLength(state.output, "utf8");
|
|
1744
|
-
if (newBytes >= state.retainedBytes) {
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
state.retainedBytes = newBytes;
|
|
1748
|
-
state.truncated = true;
|
|
1749
|
-
}
|
|
1750
|
-
};
|
|
1751
|
-
proc.stdout?.on("data", onData);
|
|
1752
|
-
proc.stderr?.on("data", onData);
|
|
1753
|
-
state.timeoutHandle = setTimeout(() => {
|
|
1754
|
-
try {
|
|
1755
|
-
proc.kill("SIGTERM");
|
|
1756
|
-
} catch {
|
|
1757
|
-
}
|
|
1758
|
-
}, this.commandTimeoutMs);
|
|
1759
|
-
this.terminals.set(id, state);
|
|
1760
|
-
return { terminalId: id };
|
|
1761
1230
|
}
|
|
1762
|
-
/**
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
async waitForExit(terminalId) {
|
|
1774
|
-
const state = this.terminals.get(terminalId);
|
|
1775
|
-
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1776
|
-
return state.exitPromise;
|
|
1777
|
-
}
|
|
1778
|
-
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
1779
|
-
kill(terminalId) {
|
|
1780
|
-
const state = this.terminals.get(terminalId);
|
|
1781
|
-
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1782
|
-
try {
|
|
1783
|
-
state.proc.kill("SIGTERM");
|
|
1784
|
-
} catch {
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
/** Kill the process if alive and remove the record. */
|
|
1788
|
-
release(terminalId) {
|
|
1789
|
-
const state = this.terminals.get(terminalId);
|
|
1790
|
-
if (!state) return;
|
|
1791
|
-
if (state.timeoutHandle) {
|
|
1792
|
-
clearTimeout(state.timeoutHandle);
|
|
1793
|
-
state.timeoutHandle = null;
|
|
1794
|
-
}
|
|
1795
|
-
try {
|
|
1796
|
-
state.proc.kill("SIGKILL");
|
|
1797
|
-
} catch {
|
|
1798
|
-
}
|
|
1799
|
-
this.terminals.delete(terminalId);
|
|
1800
|
-
}
|
|
1801
|
-
/** Kill all active terminals. Used on session close. */
|
|
1802
|
-
releaseAll() {
|
|
1803
|
-
for (const id of [...this.terminals.keys()]) {
|
|
1804
|
-
this.release(id);
|
|
1231
|
+
/**
|
|
1232
|
+
* Start the server. Mode depends on `options.transport`:
|
|
1233
|
+
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
1234
|
+
* - number: listens as HTTP on the given port.
|
|
1235
|
+
*/
|
|
1236
|
+
async start() {
|
|
1237
|
+
const transportMode = this.options.transport;
|
|
1238
|
+
if (typeof transportMode === "number") {
|
|
1239
|
+
await this.startHttp(transportMode);
|
|
1240
|
+
} else {
|
|
1241
|
+
await this.startStdio();
|
|
1805
1242
|
}
|
|
1806
1243
|
}
|
|
1807
|
-
|
|
1808
|
-
if (
|
|
1809
|
-
|
|
1810
|
-
const rootWithSep = this.projectRoot.endsWith(path2.sep) ? this.projectRoot : this.projectRoot + path2.sep;
|
|
1811
|
-
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1812
|
-
return this.projectRoot;
|
|
1244
|
+
async startStdio() {
|
|
1245
|
+
if (this.options.legacyStartupMarker) {
|
|
1246
|
+
this.transport.sendStartupMarker();
|
|
1813
1247
|
}
|
|
1248
|
+
this.running = true;
|
|
1814
1249
|
try {
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1250
|
+
while (this.running) {
|
|
1251
|
+
const msg = await this.transport.read();
|
|
1252
|
+
if (!msg) break;
|
|
1253
|
+
const terminal = await this.handler.handleMessage(msg);
|
|
1254
|
+
if (terminal) break;
|
|
1820
1255
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1256
|
+
} finally {
|
|
1257
|
+
this.handler.close();
|
|
1258
|
+
this.transport.close();
|
|
1824
1259
|
}
|
|
1825
1260
|
}
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1261
|
+
async startHttp(port) {
|
|
1262
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
1263
|
+
const handler = this.handler;
|
|
1264
|
+
const authToken = this.options.authToken;
|
|
1265
|
+
let httpChain = Promise.resolve();
|
|
1266
|
+
this.httpServer = createServer(async (req, res) => {
|
|
1267
|
+
if (authToken) {
|
|
1268
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
1269
|
+
const queryToken = url.searchParams.get("token");
|
|
1270
|
+
const authHeader = req.headers["authorization"];
|
|
1271
|
+
const bearerToken = Array.isArray(authHeader) ? authHeader[0]?.replace(/^Bearer\s+/i, "") : authHeader?.replace(/^Bearer\s+/i, "");
|
|
1272
|
+
const supplied = queryToken ?? bearerToken ?? "";
|
|
1273
|
+
if (supplied !== authToken) {
|
|
1274
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1275
|
+
res.end(JSON.stringify({ error: { code: -32001, message: "Unauthorized" } }));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1833
1278
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1279
|
+
const selfOrigin = `http://${host}:${port}`;
|
|
1280
|
+
const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
|
|
1281
|
+
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
1282
|
+
res.writeHead(403);
|
|
1283
|
+
res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
1287
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1288
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
|
|
1289
|
+
if (req.method === "OPTIONS") {
|
|
1290
|
+
res.writeHead(204);
|
|
1291
|
+
res.end();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (req.method !== "POST") {
|
|
1295
|
+
res.writeHead(405);
|
|
1296
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1300
|
+
let body = "";
|
|
1301
|
+
let bodyBytes = 0;
|
|
1302
|
+
let tooLarge = false;
|
|
1303
|
+
for await (const chunk of req) {
|
|
1304
|
+
bodyBytes += chunk.length;
|
|
1305
|
+
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1306
|
+
tooLarge = true;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
body += chunk;
|
|
1310
|
+
}
|
|
1311
|
+
if (tooLarge) {
|
|
1312
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1313
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Request body too large" } }));
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
let msg;
|
|
1317
|
+
try {
|
|
1318
|
+
msg = JSON.parse(body);
|
|
1319
|
+
} catch {
|
|
1320
|
+
res.writeHead(400);
|
|
1321
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const isNotification = typeof msg === "object" && msg !== null && msg.id === void 0 && typeof msg.method === "string";
|
|
1325
|
+
if (isNotification) {
|
|
1326
|
+
try {
|
|
1327
|
+
await handler.handleMessage(msg);
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1331
|
+
res.end(JSON.stringify({ notifications: [] }));
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const requestPromise = httpChain.then(async () => {
|
|
1335
|
+
const notifications = [];
|
|
1336
|
+
let response = null;
|
|
1337
|
+
const originalSend = this.transport.send.bind(this.transport);
|
|
1338
|
+
this.transport.send = async (m) => {
|
|
1339
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1340
|
+
response = m;
|
|
1341
|
+
} else if (m.method === "session/update") {
|
|
1342
|
+
notifications.push(m.params);
|
|
1343
|
+
} else {
|
|
1344
|
+
notifications.push(m);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
try {
|
|
1348
|
+
await handler.handleMessage(msg);
|
|
1349
|
+
} finally {
|
|
1350
|
+
this.transport.send = originalSend;
|
|
1351
|
+
}
|
|
1352
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1353
|
+
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1354
|
+
res.end(JSON.stringify(responseBody));
|
|
1355
|
+
});
|
|
1356
|
+
httpChain = requestPromise.catch(() => void 0);
|
|
1357
|
+
try {
|
|
1358
|
+
await requestPromise;
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
return new Promise((resolve3) => {
|
|
1363
|
+
this.httpServer.listen(port, host, () => {
|
|
1364
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1365
|
+
`);
|
|
1366
|
+
this.running = true;
|
|
1367
|
+
resolve3();
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/** Stop the server. */
|
|
1372
|
+
stop() {
|
|
1373
|
+
this.running = false;
|
|
1374
|
+
this.handler.close();
|
|
1375
|
+
this.transport.close();
|
|
1376
|
+
if (this.httpServer) {
|
|
1377
|
+
this.httpServer.close();
|
|
1378
|
+
this.httpServer = null;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1383
|
+
return { stopReason: "end_turn" };
|
|
1384
|
+
};
|
|
1385
|
+
async function main() {
|
|
1386
|
+
const server = new WrongStackACPServer();
|
|
1387
|
+
await server.start();
|
|
1388
|
+
}
|
|
1389
|
+
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1390
|
+
if (isEntrypoint) {
|
|
1391
|
+
main().catch((err) => {
|
|
1392
|
+
writeErr2(`[wstack-acp fatal] ${err}
|
|
1393
|
+
`);
|
|
1394
|
+
process.exit(1);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/client/file-server.ts
|
|
1399
|
+
import { randomBytes } from "node:crypto";
|
|
1400
|
+
import { realpathSync } from "node:fs";
|
|
1401
|
+
import * as fsp from "node:fs/promises";
|
|
1402
|
+
import * as path from "node:path";
|
|
1403
|
+
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1404
|
+
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1405
|
+
var FsError = class extends Error {
|
|
1406
|
+
code;
|
|
1407
|
+
path;
|
|
1408
|
+
constructor(code, path4, message) {
|
|
1409
|
+
super(message);
|
|
1410
|
+
this.name = "FsError";
|
|
1411
|
+
this.code = code;
|
|
1412
|
+
this.path = path4;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
var FileServer = class {
|
|
1416
|
+
root;
|
|
1417
|
+
realRoot;
|
|
1418
|
+
timeoutMs;
|
|
1419
|
+
maxReadBytes;
|
|
1420
|
+
maxWriteBytes;
|
|
1421
|
+
constructor(opts) {
|
|
1422
|
+
this.root = path.resolve(opts.projectRoot);
|
|
1423
|
+
this.realRoot = safeRealpathSync(this.root);
|
|
1424
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1425
|
+
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1426
|
+
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
1427
|
+
}
|
|
1428
|
+
/** Read a text file. Returns the content as a string. */
|
|
1429
|
+
async readTextFile(params) {
|
|
1430
|
+
const safe = await this.resolveInside(params.path);
|
|
1431
|
+
const controller = new AbortController();
|
|
1432
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1433
|
+
try {
|
|
1434
|
+
const stat2 = await fsp.stat(safe).catch((err) => {
|
|
1435
|
+
throw mapFsError(err, safe);
|
|
1436
|
+
});
|
|
1437
|
+
if (stat2.size > this.maxReadBytes) {
|
|
1438
|
+
throw new FsError(
|
|
1439
|
+
"TOO_LARGE",
|
|
1440
|
+
safe,
|
|
1441
|
+
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
const content = await fsp.readFile(safe, {
|
|
1445
|
+
encoding: "utf8",
|
|
1446
|
+
signal: controller.signal
|
|
1447
|
+
});
|
|
1448
|
+
return { content };
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
if (err instanceof FsError) throw err;
|
|
1451
|
+
if (controller.signal.aborted) {
|
|
1452
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1453
|
+
}
|
|
1454
|
+
throw mapFsError(err, safe);
|
|
1455
|
+
} finally {
|
|
1456
|
+
clearTimeout(timer);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
1460
|
+
async writeTextFile(params) {
|
|
1461
|
+
const byteLength = Buffer.byteLength(params.content, "utf8");
|
|
1462
|
+
if (byteLength > this.maxWriteBytes) {
|
|
1463
|
+
throw new FsError(
|
|
1464
|
+
"TOO_LARGE",
|
|
1465
|
+
params.path,
|
|
1466
|
+
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const safe = await this.resolveInside(params.path);
|
|
1470
|
+
const controller = new AbortController();
|
|
1471
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1472
|
+
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1473
|
+
try {
|
|
1474
|
+
await fsp.writeFile(tmp, params.content, {
|
|
1475
|
+
encoding: "utf8",
|
|
1476
|
+
signal: controller.signal
|
|
1477
|
+
});
|
|
1478
|
+
await this.assertRealInside(tmp);
|
|
1479
|
+
await this.assertRealInside(path.dirname(safe));
|
|
1480
|
+
await fsp.rename(tmp, safe);
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
if (err instanceof FsError) {
|
|
1483
|
+
await fsp.unlink(tmp).catch(() => void 0);
|
|
1484
|
+
throw err;
|
|
1485
|
+
}
|
|
1486
|
+
try {
|
|
1487
|
+
await fsp.unlink(tmp);
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
if (controller.signal.aborted) {
|
|
1491
|
+
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
1492
|
+
}
|
|
1493
|
+
throw mapFsError(err, safe);
|
|
1494
|
+
} finally {
|
|
1495
|
+
clearTimeout(timer);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Resolve a path and verify it is inside the project root by realpath.
|
|
1500
|
+
* Rejects with `FsError` if the textual path, the resolved path, or the
|
|
1501
|
+
* real (symlink-resolved) path escapes the project root.
|
|
1502
|
+
*
|
|
1503
|
+
* For files that don't exist yet (e.g. a write to a new file), the
|
|
1504
|
+
* nearest existing ancestor directory is realpath-checked instead.
|
|
1505
|
+
*/
|
|
1506
|
+
async resolveInside(p) {
|
|
1507
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
1508
|
+
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
1509
|
+
}
|
|
1510
|
+
if (!path.isAbsolute(p)) {
|
|
1511
|
+
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
1512
|
+
}
|
|
1513
|
+
const resolved = path.resolve(p);
|
|
1514
|
+
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
1515
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
1516
|
+
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
1517
|
+
}
|
|
1518
|
+
await this.assertRealInside(resolved);
|
|
1519
|
+
return resolved;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Resolve `resolvedPath` through `fs.realpath` and verify the result is
|
|
1523
|
+
* inside `realRoot`. For non-existent paths (new files), walk up to the
|
|
1524
|
+
* nearest existing ancestor and check that instead.
|
|
1525
|
+
*/
|
|
1526
|
+
async assertRealInside(resolvedPath) {
|
|
1527
|
+
let probe = resolvedPath;
|
|
1528
|
+
for (; ; ) {
|
|
1529
|
+
let real;
|
|
1530
|
+
try {
|
|
1531
|
+
real = await fsp.realpath(probe);
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
const code = err.code;
|
|
1534
|
+
if (code === "ENOENT") {
|
|
1535
|
+
const parent = path.dirname(probe);
|
|
1536
|
+
if (parent === probe) return;
|
|
1537
|
+
probe = parent;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
throw mapFsError(err, resolvedPath);
|
|
1541
|
+
}
|
|
1542
|
+
if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
|
|
1543
|
+
throw new FsError(
|
|
1544
|
+
"OUTSIDE_ROOT",
|
|
1545
|
+
resolvedPath,
|
|
1546
|
+
"path resolves through a symlink outside the project root"
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
function mapFsError(err, p) {
|
|
1552
|
+
const code = err?.code;
|
|
1553
|
+
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
1554
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
1555
|
+
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
1556
|
+
}
|
|
1557
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1558
|
+
return new FsError("INVALID_PATH", p, msg);
|
|
1559
|
+
}
|
|
1560
|
+
function safeRealpathSync(p) {
|
|
1561
|
+
try {
|
|
1562
|
+
return realpathSync(p);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return p;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/client/permission.ts
|
|
1569
|
+
function pickAllow(options) {
|
|
1570
|
+
const ranked = [...options].sort((a, b) => {
|
|
1571
|
+
const score = (k) => {
|
|
1572
|
+
if (k === "allow_once") return 0;
|
|
1573
|
+
if (k === "allow_always") return 1;
|
|
1574
|
+
if (k === "reject_once") return 2;
|
|
1575
|
+
return 3;
|
|
1576
|
+
};
|
|
1577
|
+
return score(a.kind) - score(b.kind);
|
|
1578
|
+
});
|
|
1579
|
+
const chosen = ranked[0];
|
|
1580
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1581
|
+
return { outcome: "cancelled" };
|
|
1582
|
+
}
|
|
1583
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
1584
|
+
}
|
|
1585
|
+
function pickReject(options) {
|
|
1586
|
+
const reject = options.find(
|
|
1587
|
+
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
1588
|
+
);
|
|
1589
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
1590
|
+
}
|
|
1591
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
1592
|
+
var defaultPermissionPolicy = async (req) => {
|
|
1593
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1594
|
+
return pickAllow(req.options);
|
|
1595
|
+
};
|
|
1596
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
1597
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1598
|
+
const kind = req.toolCall.kind;
|
|
1599
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1600
|
+
return pickAllow(req.options);
|
|
1601
|
+
}
|
|
1602
|
+
return pickReject(req.options);
|
|
1603
|
+
};
|
|
1604
|
+
function makePermissionPolicy(decide) {
|
|
1605
|
+
return async (req) => {
|
|
1606
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1607
|
+
const allow = await decide(req);
|
|
1608
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/client/terminal-server.ts
|
|
1613
|
+
import { spawn } from "node:child_process";
|
|
1614
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1615
|
+
import * as path2 from "node:path";
|
|
1616
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1617
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1618
|
+
var TerminalServer = class {
|
|
1619
|
+
terminals = /* @__PURE__ */ new Map();
|
|
1620
|
+
projectRoot;
|
|
1621
|
+
commandTimeoutMs;
|
|
1622
|
+
outputByteLimit;
|
|
1623
|
+
maxOutputByteLimit;
|
|
1624
|
+
maxTerminals;
|
|
1625
|
+
abortSignal;
|
|
1626
|
+
abortHandler = () => this.releaseAll();
|
|
1627
|
+
nextId = 1;
|
|
1628
|
+
constructor(opts) {
|
|
1629
|
+
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
1630
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1631
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1632
|
+
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1633
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1634
|
+
if (this.maxTerminals < 1) throw new RangeError("maxTerminals must be at least 1");
|
|
1635
|
+
this.abortSignal = opts.signal;
|
|
1636
|
+
if (opts.signal) {
|
|
1637
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
1641
|
+
create(params) {
|
|
1642
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const id = `term_${this.nextId++}`;
|
|
1648
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
1649
|
+
const perCallByteLimit = Math.min(
|
|
1650
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1651
|
+
this.maxOutputByteLimit
|
|
1652
|
+
);
|
|
1653
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
1654
|
+
cwd,
|
|
1655
|
+
env: this.buildEnv(params.env),
|
|
1656
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1657
|
+
windowsHide: true
|
|
1658
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
1659
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
1660
|
+
// the command a single shell-parsed string, which breaks
|
|
1661
|
+
// Windows cmd quoting for the common case of running node with
|
|
1662
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
1663
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
1664
|
+
// default.
|
|
1665
|
+
});
|
|
1666
|
+
const state = {
|
|
1667
|
+
proc,
|
|
1668
|
+
cwd,
|
|
1669
|
+
command: params.command,
|
|
1670
|
+
args: params.args ?? [],
|
|
1671
|
+
outputChunks: [],
|
|
1672
|
+
outputHead: 0,
|
|
1673
|
+
retainedBytes: 0,
|
|
1674
|
+
truncated: false,
|
|
1675
|
+
exitStatus: void 0,
|
|
1676
|
+
timeoutHandle: null,
|
|
1677
|
+
exitPromise: new Promise((resolve3) => {
|
|
1678
|
+
proc.on("close", (code, signalName) => {
|
|
1679
|
+
if (state.timeoutHandle) {
|
|
1680
|
+
clearTimeout(state.timeoutHandle);
|
|
1681
|
+
state.timeoutHandle = null;
|
|
1682
|
+
}
|
|
1683
|
+
const exitStatus = {
|
|
1684
|
+
exitCode: typeof code === "number" ? code : null,
|
|
1685
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
1686
|
+
};
|
|
1687
|
+
state.exitStatus = exitStatus;
|
|
1688
|
+
resolve3(exitStatus);
|
|
1689
|
+
});
|
|
1690
|
+
proc.on("error", (err) => {
|
|
1691
|
+
if (state.timeoutHandle) {
|
|
1692
|
+
clearTimeout(state.timeoutHandle);
|
|
1693
|
+
state.timeoutHandle = null;
|
|
1694
|
+
}
|
|
1695
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
1696
|
+
state.exitStatus = exitStatus;
|
|
1697
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
1698
|
+
`, "utf8");
|
|
1699
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
1700
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
1701
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
1702
|
+
errorOutput = errorOutput.subarray(start);
|
|
1703
|
+
state.truncated = true;
|
|
1704
|
+
}
|
|
1705
|
+
state.outputChunks.push(errorOutput);
|
|
1706
|
+
state.retainedBytes = errorOutput.length;
|
|
1707
|
+
resolve3(exitStatus);
|
|
1708
|
+
});
|
|
1709
|
+
})
|
|
1710
|
+
};
|
|
1711
|
+
proc.stdout?.setEncoding("utf8");
|
|
1712
|
+
proc.stderr?.setEncoding("utf8");
|
|
1713
|
+
const onData = (chunk) => {
|
|
1714
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
1715
|
+
state.outputChunks.push(outputChunk);
|
|
1716
|
+
state.retainedBytes += outputChunk.length;
|
|
1717
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
1718
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
1719
|
+
const first = state.outputChunks[state.outputHead];
|
|
1720
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
1721
|
+
if (first.length <= overflow) {
|
|
1722
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
1723
|
+
state.outputHead++;
|
|
1724
|
+
state.retainedBytes -= first.length;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
let start = overflow;
|
|
1728
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
1729
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
1730
|
+
state.retainedBytes -= start;
|
|
1731
|
+
}
|
|
1732
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
1733
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
1734
|
+
state.outputHead = 0;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
proc.stdout?.on("data", onData);
|
|
1738
|
+
proc.stderr?.on("data", onData);
|
|
1739
|
+
state.timeoutHandle = setTimeout(() => {
|
|
1740
|
+
try {
|
|
1741
|
+
proc.kill("SIGTERM");
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
}, this.commandTimeoutMs);
|
|
1745
|
+
this.terminals.set(id, state);
|
|
1746
|
+
return { terminalId: id };
|
|
1747
|
+
}
|
|
1748
|
+
/** Return captured output and (if available) the exit status. */
|
|
1749
|
+
output(terminalId) {
|
|
1750
|
+
const state = this.terminals.get(terminalId);
|
|
1751
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1752
|
+
return {
|
|
1753
|
+
output: Buffer.concat(
|
|
1754
|
+
state.outputChunks.slice(state.outputHead),
|
|
1755
|
+
state.retainedBytes
|
|
1756
|
+
).toString("utf8"),
|
|
1757
|
+
truncated: state.truncated,
|
|
1758
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
1762
|
+
async waitForExit(terminalId) {
|
|
1763
|
+
const state = this.terminals.get(terminalId);
|
|
1764
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1765
|
+
return state.exitPromise;
|
|
1766
|
+
}
|
|
1767
|
+
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
1768
|
+
kill(terminalId) {
|
|
1769
|
+
const state = this.terminals.get(terminalId);
|
|
1770
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1771
|
+
try {
|
|
1772
|
+
state.proc.kill("SIGTERM");
|
|
1773
|
+
} catch {
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/** Kill the process if alive and remove the record. */
|
|
1777
|
+
release(terminalId) {
|
|
1778
|
+
const state = this.terminals.get(terminalId);
|
|
1779
|
+
if (!state) return;
|
|
1780
|
+
if (state.timeoutHandle) {
|
|
1781
|
+
clearTimeout(state.timeoutHandle);
|
|
1782
|
+
state.timeoutHandle = null;
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
state.proc.kill("SIGKILL");
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
this.terminals.delete(terminalId);
|
|
1789
|
+
}
|
|
1790
|
+
/** Kill all active terminals. Used on session close. */
|
|
1791
|
+
releaseAll() {
|
|
1792
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
1793
|
+
for (const id of [...this.terminals.keys()]) {
|
|
1794
|
+
this.release(id);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
resolveCwd(cwd) {
|
|
1798
|
+
if (!cwd) return this.projectRoot;
|
|
1799
|
+
const resolved = path2.resolve(cwd);
|
|
1800
|
+
const rootWithSep = this.projectRoot.endsWith(path2.sep) ? this.projectRoot : this.projectRoot + path2.sep;
|
|
1801
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1802
|
+
return this.projectRoot;
|
|
1803
|
+
}
|
|
1804
|
+
try {
|
|
1805
|
+
const realRoot = realpathSync2(this.projectRoot);
|
|
1806
|
+
const realCwd = realpathSync2(resolved);
|
|
1807
|
+
const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
|
|
1808
|
+
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
1809
|
+
return realRoot;
|
|
1810
|
+
}
|
|
1811
|
+
return realCwd;
|
|
1812
|
+
} catch {
|
|
1813
|
+
return this.projectRoot;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
buildEnv(agentEnv) {
|
|
1817
|
+
const env = buildChildEnv();
|
|
1818
|
+
if (agentEnv) {
|
|
1819
|
+
for (const { name, value } of agentEnv) {
|
|
1820
|
+
const upper = name.toUpperCase();
|
|
1821
|
+
if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
|
|
1822
|
+
env[name] = value;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return env;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Clamp an agent-supplied numeric to a finite positive safe integer, falling
|
|
1829
|
+
* back to `defaultValue` for undefined/NaN/non-finite values. Prevents
|
|
1830
|
+
* negative, NaN, or Infinity values from disabling output caps or causing
|
|
1831
|
+
* unbounded memory growth.
|
|
1832
|
+
*/
|
|
1833
|
+
clampFiniteInt(value, defaultValue) {
|
|
1834
|
+
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
1835
|
+
return defaultValue;
|
|
1836
|
+
}
|
|
1837
|
+
return Math.trunc(value);
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1841
|
+
"NODE_OPTIONS",
|
|
1842
|
+
"LD_PRELOAD",
|
|
1843
|
+
"LD_LIBRARY_PATH",
|
|
1844
|
+
"DYLD_INSERT_LIBRARIES",
|
|
1845
|
+
"DYLD_LIBRARY_PATH",
|
|
1846
|
+
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
1847
|
+
"PATH",
|
|
1848
|
+
"PYTHONPATH",
|
|
1849
|
+
"PYTHONSTARTUP",
|
|
1850
|
+
"PERL5OPT",
|
|
1851
|
+
"PERLLIB",
|
|
1852
|
+
"RUBYOPT",
|
|
1853
|
+
"RUBYLIB"
|
|
1854
|
+
]);
|
|
1855
|
+
|
|
1856
|
+
// src/client/trust-boundary-permission.ts
|
|
1857
|
+
function pickOption(options, allowed) {
|
|
1858
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
1859
|
+
for (const kind of kinds) {
|
|
1860
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
1861
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
1862
|
+
}
|
|
1863
|
+
return { outcome: "cancelled" };
|
|
1864
|
+
}
|
|
1865
|
+
function riskFor(kind) {
|
|
1866
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
1867
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
1868
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
1869
|
+
return "elevated";
|
|
1870
|
+
}
|
|
1871
|
+
function capabilityFor(request) {
|
|
1872
|
+
const raw = request.toolCall.rawInput;
|
|
1873
|
+
if (typeof raw?.path === "string") {
|
|
1874
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
1875
|
+
}
|
|
1876
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
1877
|
+
return "process.spawn";
|
|
1878
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
1879
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
1880
|
+
}
|
|
1881
|
+
function subjectFor(request) {
|
|
1882
|
+
const raw = request.toolCall.rawInput;
|
|
1883
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
1884
|
+
if (typeof raw?.path === "string") {
|
|
1885
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
1886
|
+
}
|
|
1887
|
+
if (typeof raw?.command === "string") {
|
|
1888
|
+
return {
|
|
1889
|
+
kind: "command",
|
|
1890
|
+
id: raw.command,
|
|
1891
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
return {
|
|
1895
|
+
kind: "resource",
|
|
1896
|
+
id: title,
|
|
1897
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function isAllowed(decision) {
|
|
1901
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
1902
|
+
}
|
|
1903
|
+
function toTrustBoundaryRequest(request, options) {
|
|
1904
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
1905
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
1906
|
+
return {
|
|
1907
|
+
version: 1,
|
|
1908
|
+
requestId: String(request.toolCall.toolCallId),
|
|
1909
|
+
actor: {
|
|
1910
|
+
...options.actor ?? { kind: "agent" },
|
|
1911
|
+
...sessionId ? { sessionId } : {}
|
|
1912
|
+
},
|
|
1913
|
+
surface: "acp",
|
|
1914
|
+
capability: capabilityFor(request),
|
|
1915
|
+
subject: subjectFor(request),
|
|
1916
|
+
risk: riskFor(request.toolCall.kind),
|
|
1917
|
+
scope: {
|
|
1918
|
+
...options.scope ?? {},
|
|
1919
|
+
...sessionId ? { sessionId } : {}
|
|
1920
|
+
},
|
|
1921
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
1922
|
+
metadata: {
|
|
1923
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
1924
|
+
toolKind: request.toolCall.kind ?? null
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
1929
|
+
return async (request) => {
|
|
1930
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1931
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
1932
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1933
|
+
return pickOption(request.options, isAllowed(decision));
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/client/websocket-transport.ts
|
|
1938
|
+
var WebSocketClientTransport = class {
|
|
1939
|
+
ws = null;
|
|
1940
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1941
|
+
closed = false;
|
|
1942
|
+
opts;
|
|
1943
|
+
maxBufferedBytes;
|
|
1944
|
+
maxMessageChars;
|
|
1945
|
+
constructor(opts) {
|
|
1946
|
+
this.opts = opts;
|
|
1947
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
1948
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
1949
|
+
}
|
|
1950
|
+
start() {
|
|
1951
|
+
const WS = globalThis.WebSocket;
|
|
1952
|
+
if (!WS) {
|
|
1953
|
+
return Promise.reject(
|
|
1954
|
+
new Error(
|
|
1955
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
1956
|
+
)
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1960
|
+
return new Promise((resolve3, reject) => {
|
|
1961
|
+
let settled = false;
|
|
1962
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1963
|
+
this.ws = ws;
|
|
1964
|
+
const timer = setTimeout(() => {
|
|
1965
|
+
if (settled) return;
|
|
1966
|
+
settled = true;
|
|
1967
|
+
try {
|
|
1968
|
+
ws.close();
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1972
|
+
}, timeoutMs);
|
|
1973
|
+
ws.addEventListener("open", () => {
|
|
1974
|
+
if (settled) return;
|
|
1975
|
+
settled = true;
|
|
1976
|
+
clearTimeout(timer);
|
|
1977
|
+
resolve3();
|
|
1978
|
+
});
|
|
1979
|
+
ws.addEventListener("error", (ev) => {
|
|
1980
|
+
if (settled) {
|
|
1981
|
+
this.closed = true;
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
settled = true;
|
|
1985
|
+
clearTimeout(timer);
|
|
1986
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
1987
|
+
reject(new Error(message));
|
|
1988
|
+
});
|
|
1989
|
+
ws.addEventListener("close", () => {
|
|
1990
|
+
this.closed = true;
|
|
1991
|
+
});
|
|
1992
|
+
ws.addEventListener("message", (ev) => {
|
|
1993
|
+
this.onData(ev.data);
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
send(msg) {
|
|
1998
|
+
if (this.closed || !this.ws) {
|
|
1999
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
2000
|
+
}
|
|
2001
|
+
try {
|
|
2002
|
+
const serialized = JSON.stringify(msg);
|
|
2003
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount ?? 0 : 0;
|
|
2004
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
2005
|
+
this.stop();
|
|
2006
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
2007
|
+
}
|
|
2008
|
+
this.ws.send(serialized);
|
|
2009
|
+
return Promise.resolve();
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
onMessage(handler) {
|
|
2015
|
+
this.handlers.add(handler);
|
|
2016
|
+
return () => this.handlers.delete(handler);
|
|
2017
|
+
}
|
|
2018
|
+
stop() {
|
|
2019
|
+
this.closed = true;
|
|
2020
|
+
if (this.ws) {
|
|
2021
|
+
try {
|
|
2022
|
+
this.ws.close();
|
|
2023
|
+
} catch {
|
|
2024
|
+
}
|
|
2025
|
+
this.ws = null;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
onData(data) {
|
|
2029
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
2030
|
+
if (text.length > this.maxMessageChars) {
|
|
2031
|
+
this.stop();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (!text.trim()) return;
|
|
2035
|
+
let msg;
|
|
2036
|
+
try {
|
|
2037
|
+
msg = JSON.parse(text);
|
|
2038
|
+
} catch {
|
|
2039
|
+
for (const line of text.split("\n")) {
|
|
2040
|
+
if (!line.trim()) continue;
|
|
2041
|
+
try {
|
|
2042
|
+
this.dispatch(JSON.parse(line));
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
this.dispatch(msg);
|
|
2049
|
+
}
|
|
2050
|
+
dispatch(msg) {
|
|
2051
|
+
for (const handler of [...this.handlers]) {
|
|
2052
|
+
try {
|
|
2053
|
+
handler(msg);
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
function finitePositiveLimit(value, fallback) {
|
|
2060
|
+
return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1866
2063
|
// src/client/acp-session.ts
|
|
1867
2064
|
var ACPSessionError = class extends Error {
|
|
1868
2065
|
kind;
|
|
@@ -1884,6 +2081,7 @@ var ACPSession = class _ACPSession {
|
|
|
1884
2081
|
permissionPolicy;
|
|
1885
2082
|
timeoutMs;
|
|
1886
2083
|
opts;
|
|
2084
|
+
transportOff = null;
|
|
1887
2085
|
state = "init";
|
|
1888
2086
|
sessionId = null;
|
|
1889
2087
|
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
@@ -1915,8 +2113,19 @@ var ACPSession = class _ACPSession {
|
|
|
1915
2113
|
if (opts.terminalOutputByteLimit !== void 0) {
|
|
1916
2114
|
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
1917
2115
|
}
|
|
2116
|
+
if (opts.terminalMaxCount !== void 0) {
|
|
2117
|
+
termOpts.maxTerminals = opts.terminalMaxCount;
|
|
2118
|
+
}
|
|
1918
2119
|
this.terminalServer = new TerminalServer(termOpts);
|
|
1919
|
-
|
|
2120
|
+
if (opts.permissionPolicy && opts.trustBoundary) {
|
|
2121
|
+
throw new TypeError("permissionPolicy and trustBoundary are mutually exclusive");
|
|
2122
|
+
}
|
|
2123
|
+
this.permissionPolicy = opts.trustBoundary ? makeTrustBoundaryPermissionPolicy({
|
|
2124
|
+
boundary: opts.trustBoundary,
|
|
2125
|
+
...opts.trustActor ? { actor: opts.trustActor } : {},
|
|
2126
|
+
scope: opts.trustScope ?? { cwd: opts.projectRoot },
|
|
2127
|
+
...opts.trustAuthContext ? { authContext: opts.trustAuthContext } : {}
|
|
2128
|
+
}) : opts.permissionPolicy ?? readOnlyPermissionPolicy;
|
|
1920
2129
|
}
|
|
1921
2130
|
// ──────────────────────────────────────────────────────────────────────
|
|
1922
2131
|
// Public accessors
|
|
@@ -1990,10 +2199,12 @@ var ACPSession = class _ACPSession {
|
|
|
1990
2199
|
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
1991
2200
|
}
|
|
1992
2201
|
const session = new _ACPSession(opts, transport);
|
|
1993
|
-
transport.onMessage((msg) => session.handleMessage(msg));
|
|
2202
|
+
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
1994
2203
|
try {
|
|
1995
2204
|
await session.initialize();
|
|
1996
2205
|
} catch (err) {
|
|
2206
|
+
session.transportOff?.();
|
|
2207
|
+
session.transportOff = null;
|
|
1997
2208
|
try {
|
|
1998
2209
|
transport.stop();
|
|
1999
2210
|
} catch {
|
|
@@ -2157,7 +2368,11 @@ var ACPSession = class _ACPSession {
|
|
|
2157
2368
|
mcpServers: servers
|
|
2158
2369
|
});
|
|
2159
2370
|
if (isJsonRpcError(result)) {
|
|
2160
|
-
throw new ACPSessionError(
|
|
2371
|
+
throw new ACPSessionError(
|
|
2372
|
+
"prompt_failed",
|
|
2373
|
+
`session/resume failed: ${result.message}`,
|
|
2374
|
+
result
|
|
2375
|
+
);
|
|
2161
2376
|
}
|
|
2162
2377
|
this.sessionId = sessionId;
|
|
2163
2378
|
}
|
|
@@ -2208,7 +2423,11 @@ var ACPSession = class _ACPSession {
|
|
|
2208
2423
|
const id = this.allocId();
|
|
2209
2424
|
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
2210
2425
|
if (isJsonRpcError(result)) {
|
|
2211
|
-
throw new ACPSessionError(
|
|
2426
|
+
throw new ACPSessionError(
|
|
2427
|
+
"prompt_failed",
|
|
2428
|
+
`session/delete failed: ${result.message}`,
|
|
2429
|
+
result
|
|
2430
|
+
);
|
|
2212
2431
|
}
|
|
2213
2432
|
if (this.sessionId === sessionId) {
|
|
2214
2433
|
this.sessionId = null;
|
|
@@ -2243,7 +2462,11 @@ var ACPSession = class _ACPSession {
|
|
|
2243
2462
|
const id = this.allocId();
|
|
2244
2463
|
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2245
2464
|
if (isJsonRpcError(result)) {
|
|
2246
|
-
throw new ACPSessionError(
|
|
2465
|
+
throw new ACPSessionError(
|
|
2466
|
+
"prompt_failed",
|
|
2467
|
+
`session/set_mode failed: ${result.message}`,
|
|
2468
|
+
result
|
|
2469
|
+
);
|
|
2247
2470
|
}
|
|
2248
2471
|
}
|
|
2249
2472
|
/**
|
|
@@ -2258,7 +2481,11 @@ var ACPSession = class _ACPSession {
|
|
|
2258
2481
|
value
|
|
2259
2482
|
});
|
|
2260
2483
|
if (isJsonRpcError(result)) {
|
|
2261
|
-
throw new ACPSessionError(
|
|
2484
|
+
throw new ACPSessionError(
|
|
2485
|
+
"prompt_failed",
|
|
2486
|
+
`session/set_config_option failed: ${result.message}`,
|
|
2487
|
+
result
|
|
2488
|
+
);
|
|
2262
2489
|
}
|
|
2263
2490
|
}
|
|
2264
2491
|
/**
|
|
@@ -2269,7 +2496,11 @@ var ACPSession = class _ACPSession {
|
|
|
2269
2496
|
const id = this.allocId();
|
|
2270
2497
|
const result = await this.sendRequest(id, "providers/list", {});
|
|
2271
2498
|
if (isJsonRpcError(result)) {
|
|
2272
|
-
throw new ACPSessionError(
|
|
2499
|
+
throw new ACPSessionError(
|
|
2500
|
+
"prompt_failed",
|
|
2501
|
+
`providers/list failed: ${result.message}`,
|
|
2502
|
+
result
|
|
2503
|
+
);
|
|
2273
2504
|
}
|
|
2274
2505
|
const r = result;
|
|
2275
2506
|
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
@@ -2305,7 +2536,11 @@ var ACPSession = class _ACPSession {
|
|
|
2305
2536
|
const id = this.allocId();
|
|
2306
2537
|
const result = await this.sendRequest(id, "providers/disable", {});
|
|
2307
2538
|
if (isJsonRpcError(result)) {
|
|
2308
|
-
throw new ACPSessionError(
|
|
2539
|
+
throw new ACPSessionError(
|
|
2540
|
+
"prompt_failed",
|
|
2541
|
+
`providers/disable failed: ${result.message}`,
|
|
2542
|
+
result
|
|
2543
|
+
);
|
|
2309
2544
|
}
|
|
2310
2545
|
}
|
|
2311
2546
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -2412,11 +2647,7 @@ var ACPSession = class _ACPSession {
|
|
|
2412
2647
|
}
|
|
2413
2648
|
const sessionId = result.sessionId;
|
|
2414
2649
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2415
|
-
throw new ACPSessionError(
|
|
2416
|
-
"protocol_error",
|
|
2417
|
-
"session/new returned no sessionId",
|
|
2418
|
-
result
|
|
2419
|
-
);
|
|
2650
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
2420
2651
|
}
|
|
2421
2652
|
this.sessionId = sessionId;
|
|
2422
2653
|
}
|
|
@@ -2459,6 +2690,8 @@ var ACPSession = class _ACPSession {
|
|
|
2459
2690
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
2460
2691
|
}
|
|
2461
2692
|
this.pending.clear();
|
|
2693
|
+
this.transportOff?.();
|
|
2694
|
+
this.transportOff = null;
|
|
2462
2695
|
try {
|
|
2463
2696
|
this.transport.stop();
|
|
2464
2697
|
} catch {
|
|
@@ -2494,10 +2727,7 @@ var ACPSession = class _ACPSession {
|
|
|
2494
2727
|
const handle = setTimeout(() => {
|
|
2495
2728
|
this.pending.delete(id);
|
|
2496
2729
|
reject(
|
|
2497
|
-
new ACPSessionError(
|
|
2498
|
-
"protocol_error",
|
|
2499
|
-
`${method} timed out after ${effectiveTimeout}ms`
|
|
2500
|
-
)
|
|
2730
|
+
new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
|
|
2501
2731
|
);
|
|
2502
2732
|
}, effectiveTimeout);
|
|
2503
2733
|
this.pending.set(id, {
|
|
@@ -2606,297 +2836,580 @@ var ACPSession = class _ACPSession {
|
|
|
2606
2836
|
this.scratch.text += text;
|
|
2607
2837
|
this.emitProgress({ type: "message", text });
|
|
2608
2838
|
}
|
|
2609
|
-
return;
|
|
2610
|
-
}
|
|
2611
|
-
case "thought_chunk": {
|
|
2612
|
-
const text = extractText(u.content);
|
|
2613
|
-
if (text) {
|
|
2614
|
-
this.scratch.thoughts += text;
|
|
2615
|
-
this.emitProgress({ type: "thought", text });
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
case "thought_chunk": {
|
|
2842
|
+
const text = extractText(u.content);
|
|
2843
|
+
if (text) {
|
|
2844
|
+
this.scratch.thoughts += text;
|
|
2845
|
+
this.emitProgress({ type: "thought", text });
|
|
2846
|
+
}
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
case "tool_call":
|
|
2850
|
+
case "tool_call_update": {
|
|
2851
|
+
this.captureToolCall(u, u.sessionUpdate === "tool_call");
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
case "plan":
|
|
2855
|
+
if (Array.isArray(u.entries)) {
|
|
2856
|
+
this.scratch.plan = u.entries;
|
|
2857
|
+
this.emitProgress({ type: "plan", entries: u.entries });
|
|
2858
|
+
}
|
|
2859
|
+
return;
|
|
2860
|
+
case "usage_update":
|
|
2861
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
2862
|
+
const usage = {
|
|
2863
|
+
used: u.used,
|
|
2864
|
+
size: u.size,
|
|
2865
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2866
|
+
};
|
|
2867
|
+
this.scratch.usage = usage;
|
|
2868
|
+
this.emitProgress({ type: "usage", usage });
|
|
2869
|
+
}
|
|
2870
|
+
return;
|
|
2871
|
+
case "available_commands_update":
|
|
2872
|
+
case "current_mode_update":
|
|
2873
|
+
case "config_option_update":
|
|
2874
|
+
case "session_info_update":
|
|
2875
|
+
case "user_message_chunk":
|
|
2876
|
+
case "next_edit_suggestions":
|
|
2877
|
+
case "elicitation":
|
|
2878
|
+
return;
|
|
2879
|
+
default:
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Fold a `tool_call` / `tool_call_update` notification into the scratch
|
|
2885
|
+
* tool-call map (deduped by toolCallId), extract any `diff` content into
|
|
2886
|
+
* the diffs list, and emit live progress.
|
|
2887
|
+
*/
|
|
2888
|
+
captureToolCall(u, isNew) {
|
|
2889
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
2890
|
+
if (!toolCallId) return;
|
|
2891
|
+
const prev = this.scratch.toolCalls.get(toolCallId);
|
|
2892
|
+
const record = {
|
|
2893
|
+
toolCallId,
|
|
2894
|
+
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
2895
|
+
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
2896
|
+
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
2897
|
+
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
2898
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
2899
|
+
};
|
|
2900
|
+
this.scratch.toolCalls.set(toolCallId, record);
|
|
2901
|
+
if (Array.isArray(u.content)) {
|
|
2902
|
+
for (const c of u.content) {
|
|
2903
|
+
if (c && typeof c === "object" && c.type === "diff") {
|
|
2904
|
+
const diff = {
|
|
2905
|
+
path: c.path,
|
|
2906
|
+
oldText: c.oldText,
|
|
2907
|
+
newText: c.newText
|
|
2908
|
+
};
|
|
2909
|
+
this.scratch.diffs.push(diff);
|
|
2910
|
+
this.emitProgress({ type: "diff", diff });
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
this.emitProgress({
|
|
2915
|
+
type: isNew ? "tool_call" : "tool_call_update",
|
|
2916
|
+
toolCall: record
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
emitProgress(event) {
|
|
2920
|
+
if (!this.progressHandler) return;
|
|
2921
|
+
try {
|
|
2922
|
+
this.progressHandler(event);
|
|
2923
|
+
} catch {
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
2927
|
+
progressHandler = null;
|
|
2928
|
+
// Per-prompt scratch state
|
|
2929
|
+
scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
2930
|
+
resetScratch() {
|
|
2931
|
+
this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
2932
|
+
}
|
|
2933
|
+
async handlePermissionRequest(msg) {
|
|
2934
|
+
const id = msg.id;
|
|
2935
|
+
if (id === void 0) return;
|
|
2936
|
+
const params = msg.params;
|
|
2937
|
+
const toolCall = params?.toolCall;
|
|
2938
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
2939
|
+
if (!toolCall) {
|
|
2940
|
+
await this.sendErrorResponse(id, -32602, "toolCall is required");
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
const policyAbort = new AbortController();
|
|
2944
|
+
try {
|
|
2945
|
+
const outcome = await this.permissionPolicy({
|
|
2946
|
+
toolCall,
|
|
2947
|
+
options,
|
|
2948
|
+
signal: policyAbort.signal
|
|
2949
|
+
});
|
|
2950
|
+
await this.sendResult(id, { outcome });
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2953
|
+
await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Enforce authorization at privileged callback sinks (fs/write,
|
|
2958
|
+
* terminal/create). Unlike `handlePermissionRequest` which responds to
|
|
2959
|
+
* agent-initiated `session/request_permission` messages, this method is
|
|
2960
|
+
* called by the handler BEFORE dispatching to FileServer/TerminalServer,
|
|
2961
|
+
* closing the gap where the agent simply skips the voluntary permission
|
|
2962
|
+
* request and sends the privileged callback directly.
|
|
2963
|
+
*
|
|
2964
|
+
* Uses the session's permission policy. The default
|
|
2965
|
+
* (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
|
|
2966
|
+
* calls (read/search/fetch/think) and rejects everything else — this is
|
|
2967
|
+
* the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
|
|
2968
|
+
* Director fan-out), inject `defaultPermissionPolicy` to grant
|
|
2969
|
+
* write/execute access.
|
|
2970
|
+
*
|
|
2971
|
+
* Returns true if the callback is authorized, false if denied.
|
|
2972
|
+
*/
|
|
2973
|
+
async authorizeCallback(partial) {
|
|
2974
|
+
try {
|
|
2975
|
+
const outcome = await this.permissionPolicy({
|
|
2976
|
+
toolCall: {
|
|
2977
|
+
sessionUpdate: "tool_call_update",
|
|
2978
|
+
toolCallId: partial.toolCallId,
|
|
2979
|
+
title: partial.title,
|
|
2980
|
+
kind: partial.kind,
|
|
2981
|
+
status: "pending",
|
|
2982
|
+
...partial.rawInput ? { rawInput: partial.rawInput } : {}
|
|
2983
|
+
},
|
|
2984
|
+
options: [
|
|
2985
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
2986
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
2987
|
+
],
|
|
2988
|
+
signal: new AbortController().signal
|
|
2989
|
+
});
|
|
2990
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
|
|
2991
|
+
} catch {
|
|
2992
|
+
return false;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
async handleFsRequest(msg) {
|
|
2996
|
+
const id = msg.id;
|
|
2997
|
+
if (id === void 0) return;
|
|
2998
|
+
const params = msg.params;
|
|
2999
|
+
if (!params?.path) {
|
|
3000
|
+
await this.sendErrorResponse(id, -32602, "path is required");
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
if (msg.method === "fs/write_text_file") {
|
|
3004
|
+
const allowed = await this.authorizeCallback({
|
|
3005
|
+
toolCallId: `acp-fs-write-${id}`,
|
|
3006
|
+
title: `Write file: ${params.path}`,
|
|
3007
|
+
kind: "edit",
|
|
3008
|
+
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
3009
|
+
});
|
|
3010
|
+
if (!allowed) {
|
|
3011
|
+
await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
try {
|
|
3016
|
+
if (msg.method === "fs/read_text_file") {
|
|
3017
|
+
const result = await this.fileServer.readTextFile({
|
|
3018
|
+
sessionId: params.sessionId ?? "",
|
|
3019
|
+
path: params.path
|
|
3020
|
+
});
|
|
3021
|
+
await this.sendResult(id, result);
|
|
3022
|
+
} else {
|
|
3023
|
+
await this.fileServer.writeTextFile({
|
|
3024
|
+
sessionId: params.sessionId ?? "",
|
|
3025
|
+
path: params.path,
|
|
3026
|
+
content: params.content ?? ""
|
|
3027
|
+
});
|
|
3028
|
+
await this.sendResult(id, {});
|
|
3029
|
+
}
|
|
3030
|
+
} catch (err) {
|
|
3031
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
3032
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3033
|
+
await this.sendErrorResponse(id, code, message);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
async handleTerminalRequest(msg) {
|
|
3037
|
+
const id = msg.id;
|
|
3038
|
+
if (id === void 0) return;
|
|
3039
|
+
const params = msg.params ?? {};
|
|
3040
|
+
try {
|
|
3041
|
+
switch (msg.method) {
|
|
3042
|
+
case "terminal/create": {
|
|
3043
|
+
const allowed = await this.authorizeCallback({
|
|
3044
|
+
toolCallId: `acp-terminal-create-${id}`,
|
|
3045
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
3046
|
+
kind: "execute",
|
|
3047
|
+
rawInput: {
|
|
3048
|
+
command: params.command,
|
|
3049
|
+
args: params.args,
|
|
3050
|
+
cwd: params.cwd,
|
|
3051
|
+
sessionId: params.sessionId
|
|
3052
|
+
}
|
|
3053
|
+
});
|
|
3054
|
+
if (!allowed) {
|
|
3055
|
+
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
const createOpts = {
|
|
3059
|
+
sessionId: String(params.sessionId ?? ""),
|
|
3060
|
+
command: String(params.command ?? ""),
|
|
3061
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
3062
|
+
};
|
|
3063
|
+
if (Array.isArray(params.env)) {
|
|
3064
|
+
createOpts.env = params.env;
|
|
3065
|
+
}
|
|
3066
|
+
if (typeof params.cwd === "string") {
|
|
3067
|
+
createOpts.cwd = params.cwd;
|
|
3068
|
+
}
|
|
3069
|
+
if (typeof params.outputByteLimit === "number") {
|
|
3070
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
3071
|
+
}
|
|
3072
|
+
const result = this.terminalServer.create(createOpts);
|
|
3073
|
+
await this.sendResult(id, result);
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
case "terminal/output": {
|
|
3077
|
+
const terminalId = String(params.terminalId ?? "");
|
|
3078
|
+
const out = this.terminalServer.output(terminalId);
|
|
3079
|
+
await this.sendResult(id, out);
|
|
3080
|
+
return;
|
|
2616
3081
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
return;
|
|
2623
|
-
}
|
|
2624
|
-
case "plan":
|
|
2625
|
-
if (Array.isArray(u.entries)) {
|
|
2626
|
-
this.scratch.plan = u.entries;
|
|
2627
|
-
this.emitProgress({ type: "plan", entries: u.entries });
|
|
3082
|
+
case "terminal/wait_for_exit": {
|
|
3083
|
+
const terminalId = String(params.terminalId ?? "");
|
|
3084
|
+
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
3085
|
+
await this.sendResult(id, exit);
|
|
3086
|
+
return;
|
|
2628
3087
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
size: u.size,
|
|
2635
|
-
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2636
|
-
};
|
|
2637
|
-
this.scratch.usage = usage;
|
|
2638
|
-
this.emitProgress({ type: "usage", usage });
|
|
3088
|
+
case "terminal/kill": {
|
|
3089
|
+
const terminalId = String(params.terminalId ?? "");
|
|
3090
|
+
this.terminalServer.kill(terminalId);
|
|
3091
|
+
await this.sendResult(id, {});
|
|
3092
|
+
return;
|
|
2639
3093
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3094
|
+
case "terminal/release": {
|
|
3095
|
+
const terminalId = String(params.terminalId ?? "");
|
|
3096
|
+
this.terminalServer.release(terminalId);
|
|
3097
|
+
await this.sendResult(id, {});
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
default:
|
|
3101
|
+
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
3102
|
+
}
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3105
|
+
await this.sendErrorResponse(id, -32603, message);
|
|
2651
3106
|
}
|
|
2652
3107
|
}
|
|
3108
|
+
};
|
|
3109
|
+
function textContent(text) {
|
|
3110
|
+
return { type: "text", text };
|
|
3111
|
+
}
|
|
3112
|
+
function imageContent(mimeType, data) {
|
|
3113
|
+
return { type: "image", mimeType, data };
|
|
3114
|
+
}
|
|
3115
|
+
function audioContent(mimeType, data) {
|
|
3116
|
+
return { type: "audio", mimeType, data };
|
|
3117
|
+
}
|
|
3118
|
+
function extractText(block) {
|
|
3119
|
+
if (typeof block !== "object" || block === null) return "";
|
|
3120
|
+
const b = block;
|
|
3121
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
3122
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
3123
|
+
return b.resource.text;
|
|
3124
|
+
}
|
|
3125
|
+
return "";
|
|
3126
|
+
}
|
|
3127
|
+
function isRecord(v) {
|
|
3128
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3129
|
+
}
|
|
3130
|
+
function emptyRunResult(stopReason) {
|
|
3131
|
+
return {
|
|
3132
|
+
text: "",
|
|
3133
|
+
stopReason,
|
|
3134
|
+
hasText: false,
|
|
3135
|
+
toolCalls: [],
|
|
3136
|
+
diffs: [],
|
|
3137
|
+
thoughts: ""
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/client/tool-translator.ts
|
|
3142
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
3143
|
+
var DEFAULT_OPTIONS = {
|
|
3144
|
+
asyncTools: true,
|
|
3145
|
+
pollIntervalMs: 500,
|
|
3146
|
+
totalTimeoutMs: 12e4
|
|
3147
|
+
};
|
|
3148
|
+
var ToolTranslator = class {
|
|
3149
|
+
opts;
|
|
3150
|
+
pending = /* @__PURE__ */ new Map();
|
|
3151
|
+
constructor(opts = {}) {
|
|
3152
|
+
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
3153
|
+
}
|
|
2653
3154
|
/**
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
* the diffs list, and emit live progress.
|
|
3155
|
+
* Start listening to a transport for tool responses and cancellations.
|
|
3156
|
+
* Call this once after constructing the translator and before sending tasks.
|
|
2657
3157
|
*/
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
const diff = {
|
|
2675
|
-
path: c.path,
|
|
2676
|
-
oldText: c.oldText,
|
|
2677
|
-
newText: c.newText
|
|
2678
|
-
};
|
|
2679
|
-
this.scratch.diffs.push(diff);
|
|
2680
|
-
this.emitProgress({ type: "diff", diff });
|
|
3158
|
+
attachToTransport(transport) {
|
|
3159
|
+
transport.onMessage((msg) => {
|
|
3160
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
3161
|
+
const pending = this.pending.get(msg.id);
|
|
3162
|
+
if (pending) {
|
|
3163
|
+
clearTimeout(pending.timeout);
|
|
3164
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
3165
|
+
pending.resolve(msg);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
3169
|
+
const pending = this.pending.get(msg.id);
|
|
3170
|
+
if (pending) {
|
|
3171
|
+
clearTimeout(pending.timeout);
|
|
3172
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
3173
|
+
pending.reject(new Error("Call cancelled by client"));
|
|
2681
3174
|
}
|
|
2682
3175
|
}
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Send a tool call over the transport and wait for a response.
|
|
3180
|
+
* If asyncTools is true, polls for progress and resolves when the final
|
|
3181
|
+
* response arrives.
|
|
3182
|
+
*/
|
|
3183
|
+
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
3184
|
+
await transport.send({
|
|
3185
|
+
jsonrpc: "2.0",
|
|
3186
|
+
method: "tools/call",
|
|
3187
|
+
id: callId,
|
|
3188
|
+
params: { name, arguments: args }
|
|
3189
|
+
});
|
|
3190
|
+
return new Promise((resolve3, reject) => {
|
|
3191
|
+
const timeout = setTimeout(() => {
|
|
3192
|
+
this.pending.delete(callId);
|
|
3193
|
+
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
3194
|
+
}, this.opts.totalTimeoutMs);
|
|
3195
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
cancelAll() {
|
|
3199
|
+
for (const [, p] of this.pending) {
|
|
3200
|
+
clearTimeout(p.timeout);
|
|
2683
3201
|
}
|
|
2684
|
-
this.
|
|
2685
|
-
|
|
2686
|
-
|
|
3202
|
+
this.pending.clear();
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
|
|
3206
|
+
// src/integration/acp-bench.ts
|
|
3207
|
+
import * as fsp2 from "node:fs/promises";
|
|
3208
|
+
import * as path3 from "node:path";
|
|
3209
|
+
function firstLine(s) {
|
|
3210
|
+
const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
3211
|
+
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
3212
|
+
}
|
|
3213
|
+
function randomMarker() {
|
|
3214
|
+
return `ACP_OK_${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
|
|
3215
|
+
}
|
|
3216
|
+
async function benchOne(agentId, cmd, opts) {
|
|
3217
|
+
const checks = [];
|
|
3218
|
+
const startedAt = opts.now();
|
|
3219
|
+
let session = null;
|
|
3220
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
3221
|
+
const hsStart = opts.now();
|
|
3222
|
+
try {
|
|
3223
|
+
session = await ACPSession.start({
|
|
3224
|
+
command: cmd.command,
|
|
3225
|
+
...cmd.args !== void 0 ? { args: [...cmd.args] } : {},
|
|
3226
|
+
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3227
|
+
projectRoot: opts.projectRoot,
|
|
3228
|
+
timeoutMs: opts.timeoutMs
|
|
3229
|
+
});
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
const reason2 = err instanceof Error ? err.message : String(err);
|
|
3232
|
+
checks.push({ name: "handshake", ok: false, detail: reason2 });
|
|
3233
|
+
return {
|
|
3234
|
+
agentId,
|
|
3235
|
+
status: "fail",
|
|
3236
|
+
checks,
|
|
3237
|
+
reason: reason2,
|
|
3238
|
+
handshakeMs: opts.now() - hsStart,
|
|
3239
|
+
durationMs: opts.now() - startedAt
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
const handshakeMs = opts.now() - hsStart;
|
|
3243
|
+
const agentInfo = session.getAgentInfo() ?? void 0;
|
|
3244
|
+
checks.push({
|
|
3245
|
+
name: "handshake",
|
|
3246
|
+
ok: true,
|
|
3247
|
+
detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
|
|
3248
|
+
});
|
|
3249
|
+
let promptMs;
|
|
3250
|
+
let sample;
|
|
3251
|
+
let reason;
|
|
3252
|
+
try {
|
|
3253
|
+
const pStart = opts.now();
|
|
3254
|
+
const res = await session.prompt(
|
|
3255
|
+
[textContent(`Reply with exactly this token and nothing else: ${opts.marker}`)],
|
|
3256
|
+
signal
|
|
3257
|
+
);
|
|
3258
|
+
promptMs = opts.now() - pStart;
|
|
3259
|
+
sample = res.text ? firstLine(res.text) : void 0;
|
|
3260
|
+
const promptOk = res.hasText && res.stopReason !== "refusal";
|
|
3261
|
+
checks.push({
|
|
3262
|
+
name: "prompt",
|
|
3263
|
+
ok: promptOk,
|
|
3264
|
+
detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
|
|
2687
3265
|
});
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
});
|
|
2720
|
-
await this.sendResult(id, { outcome });
|
|
2721
|
-
} catch (err) {
|
|
2722
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2723
|
-
await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
3266
|
+
const markerOk = res.text.includes(opts.marker);
|
|
3267
|
+
checks.push({
|
|
3268
|
+
name: "marker",
|
|
3269
|
+
ok: markerOk,
|
|
3270
|
+
detail: markerOk ? void 0 : "reply did not contain the token"
|
|
3271
|
+
});
|
|
3272
|
+
if (opts.checkFs) {
|
|
3273
|
+
const fileToken = `FILE_${opts.marker}`;
|
|
3274
|
+
const fileName = `acp-bench-${opts.marker}.txt`;
|
|
3275
|
+
const filePath = path3.join(opts.projectRoot, fileName);
|
|
3276
|
+
let fsOk = false;
|
|
3277
|
+
let fsDetail;
|
|
3278
|
+
try {
|
|
3279
|
+
await fsp2.writeFile(filePath, fileToken, "utf8");
|
|
3280
|
+
const fsRes = await session.prompt(
|
|
3281
|
+
[
|
|
3282
|
+
textContent(
|
|
3283
|
+
`Read the file "${fileName}" in the current directory and reply with its exact contents.`
|
|
3284
|
+
)
|
|
3285
|
+
],
|
|
3286
|
+
signal
|
|
3287
|
+
);
|
|
3288
|
+
fsOk = fsRes.text.includes(fileToken);
|
|
3289
|
+
if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
|
|
3290
|
+
} catch (err) {
|
|
3291
|
+
fsDetail = err instanceof Error ? err.message : String(err);
|
|
3292
|
+
} finally {
|
|
3293
|
+
await fsp2.rm(filePath, { force: true }).catch(() => {
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
|
|
2724
3297
|
}
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
* agent-initiated `session/request_permission` messages, this method is
|
|
2730
|
-
* called by the handler BEFORE dispatching to FileServer/TerminalServer,
|
|
2731
|
-
* closing the gap where the agent simply skips the voluntary permission
|
|
2732
|
-
* request and sends the privileged callback directly.
|
|
2733
|
-
*
|
|
2734
|
-
* Uses the session's permission policy. The default policy
|
|
2735
|
-
* (`defaultPermissionPolicy`) auto-approves everything — this is correct
|
|
2736
|
-
* for trusted local agents (CLI `acp spawn`, Director fan-out). For
|
|
2737
|
-
* untrusted/remote agents, the host should inject
|
|
2738
|
-
* `readOnlyPermissionPolicy` or an interactive policy.
|
|
2739
|
-
*
|
|
2740
|
-
* Returns true if the callback is authorized, false if denied.
|
|
2741
|
-
*/
|
|
2742
|
-
async authorizeCallback(partial) {
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
reason = err instanceof Error ? err.message : String(err);
|
|
3300
|
+
checks.push({ name: "prompt", ok: false, detail: reason });
|
|
3301
|
+
} finally {
|
|
2743
3302
|
try {
|
|
2744
|
-
|
|
2745
|
-
toolCall: {
|
|
2746
|
-
sessionUpdate: "tool_call_update",
|
|
2747
|
-
toolCallId: partial.toolCallId,
|
|
2748
|
-
title: partial.title,
|
|
2749
|
-
kind: partial.kind,
|
|
2750
|
-
status: "pending"
|
|
2751
|
-
},
|
|
2752
|
-
options: [
|
|
2753
|
-
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
2754
|
-
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
2755
|
-
],
|
|
2756
|
-
signal: new AbortController().signal
|
|
2757
|
-
});
|
|
2758
|
-
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
|
|
3303
|
+
await session.close();
|
|
2759
3304
|
} catch {
|
|
2760
|
-
return false;
|
|
2761
3305
|
}
|
|
2762
3306
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
content: params.content ?? ""
|
|
2794
|
-
});
|
|
2795
|
-
await this.sendResult(id, {});
|
|
2796
|
-
}
|
|
2797
|
-
} catch (err) {
|
|
2798
|
-
const code = err instanceof FsError ? -32602 : -32603;
|
|
2799
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2800
|
-
await this.sendErrorResponse(id, code, message);
|
|
3307
|
+
const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
|
|
3308
|
+
const allReq = required.every((c) => c.ok);
|
|
3309
|
+
const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
|
|
3310
|
+
const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
|
|
3311
|
+
return {
|
|
3312
|
+
agentId,
|
|
3313
|
+
status,
|
|
3314
|
+
checks,
|
|
3315
|
+
...agentInfo ? { agentInfo } : {},
|
|
3316
|
+
handshakeMs,
|
|
3317
|
+
...promptMs !== void 0 ? { promptMs } : {},
|
|
3318
|
+
...sample ? { sample } : {},
|
|
3319
|
+
...reason ? { reason } : {},
|
|
3320
|
+
durationMs: opts.now() - startedAt
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
async function runAcpBench(opts) {
|
|
3324
|
+
const now = opts.now ?? Date.now;
|
|
3325
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
3326
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
3327
|
+
const checkFs = opts.checkFs ?? false;
|
|
3328
|
+
const marker = opts.marker ?? randomMarker();
|
|
3329
|
+
const concurrency = Math.max(1, opts.concurrency ?? 2);
|
|
3330
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3331
|
+
const ids = [];
|
|
3332
|
+
for (const raw of opts.agentIds) {
|
|
3333
|
+
const id = raw.trim();
|
|
3334
|
+
if (id && !seen.has(id)) {
|
|
3335
|
+
seen.add(id);
|
|
3336
|
+
ids.push(id);
|
|
2801
3337
|
}
|
|
2802
3338
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
if (
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
return;
|
|
2848
|
-
}
|
|
2849
|
-
case "terminal/kill": {
|
|
2850
|
-
const terminalId = String(params.terminalId ?? "");
|
|
2851
|
-
this.terminalServer.kill(terminalId);
|
|
2852
|
-
await this.sendResult(id, {});
|
|
2853
|
-
return;
|
|
2854
|
-
}
|
|
2855
|
-
case "terminal/release": {
|
|
2856
|
-
const terminalId = String(params.terminalId ?? "");
|
|
2857
|
-
this.terminalServer.release(terminalId);
|
|
2858
|
-
await this.sendResult(id, {});
|
|
2859
|
-
return;
|
|
3339
|
+
const results = ids.map((agentId) => ({
|
|
3340
|
+
agentId,
|
|
3341
|
+
status: "skipped",
|
|
3342
|
+
checks: [],
|
|
3343
|
+
durationMs: 0,
|
|
3344
|
+
reason: "unknown agent"
|
|
3345
|
+
}));
|
|
3346
|
+
const startMs = now();
|
|
3347
|
+
const runnable = [];
|
|
3348
|
+
ids.forEach((id, index) => {
|
|
3349
|
+
const cmd = opts.resolveCmd(id);
|
|
3350
|
+
if (cmd) runnable.push({ id, cmd, index });
|
|
3351
|
+
});
|
|
3352
|
+
let next = 0;
|
|
3353
|
+
const workers = [];
|
|
3354
|
+
const workerCount = Math.min(concurrency, runnable.length);
|
|
3355
|
+
for (let w = 0; w < workerCount; w++) {
|
|
3356
|
+
workers.push(
|
|
3357
|
+
(async () => {
|
|
3358
|
+
while (true) {
|
|
3359
|
+
const current = next++;
|
|
3360
|
+
if (current >= runnable.length) return;
|
|
3361
|
+
const { id, cmd, index } = runnable[current];
|
|
3362
|
+
if (opts.signal?.aborted) {
|
|
3363
|
+
results[index] = {
|
|
3364
|
+
agentId: id,
|
|
3365
|
+
status: "skipped",
|
|
3366
|
+
checks: [],
|
|
3367
|
+
durationMs: 0,
|
|
3368
|
+
reason: "aborted"
|
|
3369
|
+
};
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
opts.onProgress?.(id, "start");
|
|
3373
|
+
const r = await benchOne(id, cmd, {
|
|
3374
|
+
projectRoot,
|
|
3375
|
+
timeoutMs,
|
|
3376
|
+
checkFs,
|
|
3377
|
+
marker,
|
|
3378
|
+
now,
|
|
3379
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
3380
|
+
});
|
|
3381
|
+
results[index] = r;
|
|
3382
|
+
opts.onProgress?.(id, "done", r);
|
|
2860
3383
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
}
|
|
2864
|
-
} catch (err) {
|
|
2865
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2866
|
-
await this.sendErrorResponse(id, -32603, message);
|
|
2867
|
-
}
|
|
3384
|
+
})()
|
|
3385
|
+
);
|
|
2868
3386
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
}
|
|
2873
|
-
function imageContent(mimeType, data) {
|
|
2874
|
-
return { type: "image", mimeType, data };
|
|
2875
|
-
}
|
|
2876
|
-
function audioContent(mimeType, data) {
|
|
2877
|
-
return { type: "audio", mimeType, data };
|
|
3387
|
+
await Promise.all(workers);
|
|
3388
|
+
const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
|
|
3389
|
+
for (const r of results) summary[r.status]++;
|
|
3390
|
+
return { results, summary, totalDurationMs: now() - startMs };
|
|
2878
3391
|
}
|
|
2879
|
-
function
|
|
2880
|
-
|
|
2881
|
-
const
|
|
2882
|
-
if (
|
|
2883
|
-
|
|
2884
|
-
return
|
|
3392
|
+
function renderAcpBenchText(result) {
|
|
3393
|
+
const icon = (s) => s === "pass" ? "\u2713" : s === "partial" ? "\u25D0" : s === "skipped" ? "\u2013" : "\u2717";
|
|
3394
|
+
const lines = ["ACP client bench:", ""];
|
|
3395
|
+
if (result.results.length === 0) {
|
|
3396
|
+
lines.push("No agents to bench.");
|
|
3397
|
+
return lines.join("\n");
|
|
2885
3398
|
}
|
|
2886
|
-
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3399
|
+
for (const r of result.results) {
|
|
3400
|
+
const checks = r.checks.map((c) => `${c.ok ? "\u2713" : "\u2717"}${c.name}`).join(" ");
|
|
3401
|
+
const timing = r.handshakeMs !== void 0 ? ` hs=${r.handshakeMs}ms${r.promptMs !== void 0 ? ` prompt=${r.promptMs}ms` : ""}` : "";
|
|
3402
|
+
lines.push(` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`);
|
|
3403
|
+
if (r.agentInfo) lines.push(` agent: ${r.agentInfo.name} ${r.agentInfo.version}`);
|
|
3404
|
+
if (r.sample) lines.push(` reply: ${r.sample}`);
|
|
3405
|
+
if (r.reason) lines.push(` reason: ${r.reason}`);
|
|
3406
|
+
}
|
|
3407
|
+
const { pass, partial, fail, skipped } = result.summary;
|
|
3408
|
+
lines.push("");
|
|
3409
|
+
lines.push(
|
|
3410
|
+
`Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
3411
|
+
);
|
|
3412
|
+
return lines.join("\n");
|
|
2900
3413
|
}
|
|
2901
3414
|
|
|
2902
3415
|
// src/registry/agents.catalog.ts
|
|
@@ -3135,50 +3648,6 @@ function findAgentDescriptor(id) {
|
|
|
3135
3648
|
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
3136
3649
|
}
|
|
3137
3650
|
|
|
3138
|
-
// src/integration/run-one-acp-task.ts
|
|
3139
|
-
import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
3140
|
-
async function runOneAcpTask(opts) {
|
|
3141
|
-
const role = opts.role ?? "acp";
|
|
3142
|
-
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
3143
|
-
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3144
|
-
command: opts.command,
|
|
3145
|
-
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
3146
|
-
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
3147
|
-
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
3148
|
-
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
3149
|
-
role,
|
|
3150
|
-
timeoutMs,
|
|
3151
|
-
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
3152
|
-
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3153
|
-
});
|
|
3154
|
-
try {
|
|
3155
|
-
const budget = new SubagentBudget({
|
|
3156
|
-
timeoutMs,
|
|
3157
|
-
maxIterations: 2e3,
|
|
3158
|
-
maxToolCalls: 5e3
|
|
3159
|
-
});
|
|
3160
|
-
budget.start();
|
|
3161
|
-
const ctx = {
|
|
3162
|
-
subagentId: role,
|
|
3163
|
-
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
3164
|
-
budget,
|
|
3165
|
-
signal: opts.signal ?? new AbortController().signal,
|
|
3166
|
-
bridge: null
|
|
3167
|
-
};
|
|
3168
|
-
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
3169
|
-
return {
|
|
3170
|
-
result: result.result == null ? "" : String(result.result),
|
|
3171
|
-
iterations: result.iterations,
|
|
3172
|
-
toolCalls: result.toolCalls
|
|
3173
|
-
};
|
|
3174
|
-
} finally {
|
|
3175
|
-
try {
|
|
3176
|
-
await stop();
|
|
3177
|
-
} catch {
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
3651
|
// src/integration/acp-subagent-runner.ts
|
|
3183
3652
|
var ACP_AGENT_COMMANDS = {
|
|
3184
3653
|
cline: {
|
|
@@ -3646,93 +4115,8 @@ var EnsembleRegistry = class {
|
|
|
3646
4115
|
}
|
|
3647
4116
|
};
|
|
3648
4117
|
|
|
3649
|
-
// src/registry/acp-registry-fetch.ts
|
|
3650
|
-
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
3651
|
-
function currentPlatformKey() {
|
|
3652
|
-
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
|
|
3653
|
-
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
|
|
3654
|
-
return `${os}-${arch}`;
|
|
3655
|
-
}
|
|
3656
|
-
function basename(cmd) {
|
|
3657
|
-
const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
3658
|
-
const parts = cleaned.split("/");
|
|
3659
|
-
return parts[parts.length - 1] || cleaned;
|
|
3660
|
-
}
|
|
3661
|
-
function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
3662
|
-
if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
3663
|
-
const dist = entry.distribution;
|
|
3664
|
-
let acp = null;
|
|
3665
|
-
if (dist?.npx?.package) {
|
|
3666
|
-
acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
|
|
3667
|
-
} else if (dist?.uvx?.package) {
|
|
3668
|
-
acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
|
|
3669
|
-
} else if (dist?.binary) {
|
|
3670
|
-
const target = dist.binary[platformKey];
|
|
3671
|
-
if (target?.cmd) {
|
|
3672
|
-
acp = {
|
|
3673
|
-
command: basename(target.cmd),
|
|
3674
|
-
args: [...target.args ?? []],
|
|
3675
|
-
...target.env ? { env: target.env } : {}
|
|
3676
|
-
};
|
|
3677
|
-
}
|
|
3678
|
-
}
|
|
3679
|
-
if (!acp) return null;
|
|
3680
|
-
const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
|
|
3681
|
-
return {
|
|
3682
|
-
id: entry.id,
|
|
3683
|
-
displayName: entry.name ?? entry.id,
|
|
3684
|
-
vendor: inferVendor(entry),
|
|
3685
|
-
probe: { command: probeCmd, args: ["--version"] },
|
|
3686
|
-
acp,
|
|
3687
|
-
supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
|
|
3688
|
-
integration: "native",
|
|
3689
|
-
docs: entry.repository ?? entry.website ?? ""
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
function inferVendor(entry) {
|
|
3693
|
-
const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
|
|
3694
|
-
if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
|
|
3695
|
-
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
3696
|
-
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
3697
|
-
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
3698
|
-
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
3699
|
-
return "community";
|
|
3700
|
-
}
|
|
3701
|
-
async function fetchAcpRegistry(opts = {}) {
|
|
3702
|
-
const url = opts.url ?? ACP_REGISTRY_URL;
|
|
3703
|
-
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
3704
|
-
const controller = new AbortController();
|
|
3705
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3706
|
-
const onParentAbort = () => controller.abort();
|
|
3707
|
-
if (opts.signal) {
|
|
3708
|
-
if (opts.signal.aborted) controller.abort();
|
|
3709
|
-
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
3710
|
-
}
|
|
3711
|
-
try {
|
|
3712
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
3713
|
-
if (!res.ok) {
|
|
3714
|
-
throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
|
|
3715
|
-
}
|
|
3716
|
-
const body = await res.json();
|
|
3717
|
-
const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
|
|
3718
|
-
if (!rawAgents) {
|
|
3719
|
-
throw new Error("ACP registry response had no agents array");
|
|
3720
|
-
}
|
|
3721
|
-
const platformKey = opts.platformKey ?? currentPlatformKey();
|
|
3722
|
-
const agents = [];
|
|
3723
|
-
for (const raw of rawAgents) {
|
|
3724
|
-
const mapped = mapRegistryEntry(raw, platformKey);
|
|
3725
|
-
if (mapped) agents.push(mapped);
|
|
3726
|
-
}
|
|
3727
|
-
return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
|
|
3728
|
-
} finally {
|
|
3729
|
-
clearTimeout(timer);
|
|
3730
|
-
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
3731
|
-
}
|
|
3732
|
-
}
|
|
3733
|
-
|
|
3734
4118
|
// src/integration/ensemble-runner.ts
|
|
3735
|
-
import { SubagentBudget
|
|
4119
|
+
import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
3736
4120
|
var DEFAULT_MAX_CONCURRENCY = 4;
|
|
3737
4121
|
async function mapBound(items, worker, limit) {
|
|
3738
4122
|
const results = new Array(items.length);
|
|
@@ -3768,10 +4152,13 @@ async function runOne(agentId, cmd, task, timeoutMs, signal, onProgress) {
|
|
|
3768
4152
|
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3769
4153
|
...cmd,
|
|
3770
4154
|
timeoutMs,
|
|
4155
|
+
// CLI /acp parallel and Director fan-out are trusted local agents — grant write/execute access.
|
|
4156
|
+
// The session default is read-only for untrusted agents (acp-session.ts:221).
|
|
4157
|
+
permissionPolicy: defaultPermissionPolicy,
|
|
3771
4158
|
...onProgress ? { onProgress: (event) => onProgress(agentId, event) } : {}
|
|
3772
4159
|
});
|
|
3773
4160
|
try {
|
|
3774
|
-
const budget = new
|
|
4161
|
+
const budget = new SubagentBudget({
|
|
3775
4162
|
timeoutMs,
|
|
3776
4163
|
maxIterations: 2e3,
|
|
3777
4164
|
maxToolCalls: 5e3
|
|
@@ -3948,213 +4335,133 @@ Ensemble summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancell
|
|
|
3948
4335
|
return lines.join("\n");
|
|
3949
4336
|
}
|
|
3950
4337
|
|
|
3951
|
-
// src/integration/acp-
|
|
3952
|
-
import
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
const hsStart = opts.now();
|
|
3967
|
-
try {
|
|
3968
|
-
session = await ACPSession.start({
|
|
3969
|
-
command: cmd.command,
|
|
3970
|
-
...cmd.args !== void 0 ? { args: [...cmd.args] } : {},
|
|
3971
|
-
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3972
|
-
projectRoot: opts.projectRoot,
|
|
3973
|
-
timeoutMs: opts.timeoutMs
|
|
3974
|
-
});
|
|
3975
|
-
} catch (err) {
|
|
3976
|
-
const reason2 = err instanceof Error ? err.message : String(err);
|
|
3977
|
-
checks.push({ name: "handshake", ok: false, detail: reason2 });
|
|
3978
|
-
return {
|
|
3979
|
-
agentId,
|
|
3980
|
-
status: "fail",
|
|
3981
|
-
checks,
|
|
3982
|
-
reason: reason2,
|
|
3983
|
-
handshakeMs: opts.now() - hsStart,
|
|
3984
|
-
durationMs: opts.now() - startedAt
|
|
3985
|
-
};
|
|
3986
|
-
}
|
|
3987
|
-
const handshakeMs = opts.now() - hsStart;
|
|
3988
|
-
const agentInfo = session.getAgentInfo() ?? void 0;
|
|
3989
|
-
checks.push({
|
|
3990
|
-
name: "handshake",
|
|
3991
|
-
ok: true,
|
|
3992
|
-
detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
|
|
4338
|
+
// src/integration/run-one-acp-task.ts
|
|
4339
|
+
import { SubagentBudget as SubagentBudget2 } from "@wrongstack/core/coordination";
|
|
4340
|
+
async function runOneAcpTask(opts) {
|
|
4341
|
+
const role = opts.role ?? "acp";
|
|
4342
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
4343
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
4344
|
+
command: opts.command,
|
|
4345
|
+
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
4346
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
4347
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
4348
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
4349
|
+
role,
|
|
4350
|
+
timeoutMs,
|
|
4351
|
+
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
4352
|
+
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3993
4353
|
});
|
|
3994
|
-
let promptMs;
|
|
3995
|
-
let sample;
|
|
3996
|
-
let reason;
|
|
3997
4354
|
try {
|
|
3998
|
-
const
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
);
|
|
4003
|
-
promptMs = opts.now() - pStart;
|
|
4004
|
-
sample = res.text ? firstLine(res.text) : void 0;
|
|
4005
|
-
const promptOk = res.hasText && res.stopReason !== "refusal";
|
|
4006
|
-
checks.push({
|
|
4007
|
-
name: "prompt",
|
|
4008
|
-
ok: promptOk,
|
|
4009
|
-
detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
|
|
4010
|
-
});
|
|
4011
|
-
const markerOk = res.text.includes(opts.marker);
|
|
4012
|
-
checks.push({
|
|
4013
|
-
name: "marker",
|
|
4014
|
-
ok: markerOk,
|
|
4015
|
-
detail: markerOk ? void 0 : "reply did not contain the token"
|
|
4355
|
+
const budget = new SubagentBudget2({
|
|
4356
|
+
timeoutMs,
|
|
4357
|
+
maxIterations: 2e3,
|
|
4358
|
+
maxToolCalls: 5e3
|
|
4016
4359
|
});
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
signal
|
|
4032
|
-
);
|
|
4033
|
-
fsOk = fsRes.text.includes(fileToken);
|
|
4034
|
-
if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
|
|
4035
|
-
} catch (err) {
|
|
4036
|
-
fsDetail = err instanceof Error ? err.message : String(err);
|
|
4037
|
-
} finally {
|
|
4038
|
-
await fsp2.rm(filePath, { force: true }).catch(() => {
|
|
4039
|
-
});
|
|
4040
|
-
}
|
|
4041
|
-
checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
|
|
4042
|
-
}
|
|
4043
|
-
} catch (err) {
|
|
4044
|
-
reason = err instanceof Error ? err.message : String(err);
|
|
4045
|
-
checks.push({ name: "prompt", ok: false, detail: reason });
|
|
4360
|
+
budget.start();
|
|
4361
|
+
const ctx = {
|
|
4362
|
+
subagentId: role,
|
|
4363
|
+
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
4364
|
+
budget,
|
|
4365
|
+
signal: opts.signal ?? new AbortController().signal,
|
|
4366
|
+
bridge: null
|
|
4367
|
+
};
|
|
4368
|
+
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
4369
|
+
return {
|
|
4370
|
+
result: result.result == null ? "" : String(result.result),
|
|
4371
|
+
iterations: result.iterations,
|
|
4372
|
+
toolCalls: result.toolCalls
|
|
4373
|
+
};
|
|
4046
4374
|
} finally {
|
|
4047
4375
|
try {
|
|
4048
|
-
await
|
|
4376
|
+
await stop();
|
|
4049
4377
|
} catch {
|
|
4050
4378
|
}
|
|
4051
4379
|
}
|
|
4052
|
-
const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
|
|
4053
|
-
const allReq = required.every((c) => c.ok);
|
|
4054
|
-
const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
|
|
4055
|
-
const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
|
|
4056
|
-
return {
|
|
4057
|
-
agentId,
|
|
4058
|
-
status,
|
|
4059
|
-
checks,
|
|
4060
|
-
...agentInfo ? { agentInfo } : {},
|
|
4061
|
-
handshakeMs,
|
|
4062
|
-
...promptMs !== void 0 ? { promptMs } : {},
|
|
4063
|
-
...sample ? { sample } : {},
|
|
4064
|
-
...reason ? { reason } : {},
|
|
4065
|
-
durationMs: opts.now() - startedAt
|
|
4066
|
-
};
|
|
4067
4380
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
const
|
|
4073
|
-
const
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4381
|
+
|
|
4382
|
+
// src/registry/acp-registry-fetch.ts
|
|
4383
|
+
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
4384
|
+
function currentPlatformKey() {
|
|
4385
|
+
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
|
|
4386
|
+
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
|
|
4387
|
+
return `${os}-${arch}`;
|
|
4388
|
+
}
|
|
4389
|
+
function basename(cmd) {
|
|
4390
|
+
const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
4391
|
+
const parts = cleaned.split("/");
|
|
4392
|
+
return parts[parts.length - 1] || cleaned;
|
|
4393
|
+
}
|
|
4394
|
+
function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
4395
|
+
if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
4396
|
+
const dist = entry.distribution;
|
|
4397
|
+
let acp = null;
|
|
4398
|
+
if (dist?.npx?.package) {
|
|
4399
|
+
acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
|
|
4400
|
+
} else if (dist?.uvx?.package) {
|
|
4401
|
+
acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
|
|
4402
|
+
} else if (dist?.binary) {
|
|
4403
|
+
const target = dist.binary[platformKey];
|
|
4404
|
+
if (target?.cmd) {
|
|
4405
|
+
acp = {
|
|
4406
|
+
command: basename(target.cmd),
|
|
4407
|
+
args: [...target.args ?? []],
|
|
4408
|
+
...target.env ? { env: target.env } : {}
|
|
4409
|
+
};
|
|
4082
4410
|
}
|
|
4083
4411
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
});
|
|
4097
|
-
let next = 0;
|
|
4098
|
-
const workers = [];
|
|
4099
|
-
const workerCount = Math.min(concurrency, runnable.length);
|
|
4100
|
-
for (let w = 0; w < workerCount; w++) {
|
|
4101
|
-
workers.push(
|
|
4102
|
-
(async () => {
|
|
4103
|
-
while (true) {
|
|
4104
|
-
const current = next++;
|
|
4105
|
-
if (current >= runnable.length) return;
|
|
4106
|
-
const { id, cmd, index } = runnable[current];
|
|
4107
|
-
if (opts.signal?.aborted) {
|
|
4108
|
-
results[index] = {
|
|
4109
|
-
agentId: id,
|
|
4110
|
-
status: "skipped",
|
|
4111
|
-
checks: [],
|
|
4112
|
-
durationMs: 0,
|
|
4113
|
-
reason: "aborted"
|
|
4114
|
-
};
|
|
4115
|
-
continue;
|
|
4116
|
-
}
|
|
4117
|
-
opts.onProgress?.(id, "start");
|
|
4118
|
-
const r = await benchOne(id, cmd, {
|
|
4119
|
-
projectRoot,
|
|
4120
|
-
timeoutMs,
|
|
4121
|
-
checkFs,
|
|
4122
|
-
marker,
|
|
4123
|
-
now,
|
|
4124
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
4125
|
-
});
|
|
4126
|
-
results[index] = r;
|
|
4127
|
-
opts.onProgress?.(id, "done", r);
|
|
4128
|
-
}
|
|
4129
|
-
})()
|
|
4130
|
-
);
|
|
4131
|
-
}
|
|
4132
|
-
await Promise.all(workers);
|
|
4133
|
-
const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
|
|
4134
|
-
for (const r of results) summary[r.status]++;
|
|
4135
|
-
return { results, summary, totalDurationMs: now() - startMs };
|
|
4412
|
+
if (!acp) return null;
|
|
4413
|
+
const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
|
|
4414
|
+
return {
|
|
4415
|
+
id: entry.id,
|
|
4416
|
+
displayName: entry.name ?? entry.id,
|
|
4417
|
+
vendor: inferVendor(entry),
|
|
4418
|
+
probe: { command: probeCmd, args: ["--version"] },
|
|
4419
|
+
acp,
|
|
4420
|
+
supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
|
|
4421
|
+
integration: "native",
|
|
4422
|
+
docs: entry.repository ?? entry.website ?? ""
|
|
4423
|
+
};
|
|
4136
4424
|
}
|
|
4137
|
-
function
|
|
4138
|
-
const
|
|
4139
|
-
|
|
4140
|
-
if (
|
|
4141
|
-
|
|
4142
|
-
|
|
4425
|
+
function inferVendor(entry) {
|
|
4426
|
+
const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
|
|
4427
|
+
if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
|
|
4428
|
+
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
4429
|
+
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
4430
|
+
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
4431
|
+
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
4432
|
+
return "community";
|
|
4433
|
+
}
|
|
4434
|
+
async function fetchAcpRegistry(opts = {}) {
|
|
4435
|
+
const url = opts.url ?? ACP_REGISTRY_URL;
|
|
4436
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
4437
|
+
const controller = new AbortController();
|
|
4438
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4439
|
+
const onParentAbort = () => controller.abort();
|
|
4440
|
+
if (opts.signal) {
|
|
4441
|
+
if (opts.signal.aborted) controller.abort();
|
|
4442
|
+
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
4143
4443
|
}
|
|
4144
|
-
|
|
4145
|
-
const
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4444
|
+
try {
|
|
4445
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
4446
|
+
if (!res.ok) {
|
|
4447
|
+
throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
|
|
4448
|
+
}
|
|
4449
|
+
const body = await res.json();
|
|
4450
|
+
const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
|
|
4451
|
+
if (!rawAgents) {
|
|
4452
|
+
throw new Error("ACP registry response had no agents array");
|
|
4453
|
+
}
|
|
4454
|
+
const platformKey = opts.platformKey ?? currentPlatformKey();
|
|
4455
|
+
const agents = [];
|
|
4456
|
+
for (const raw of rawAgents) {
|
|
4457
|
+
const mapped = mapRegistryEntry(raw, platformKey);
|
|
4458
|
+
if (mapped) agents.push(mapped);
|
|
4459
|
+
}
|
|
4460
|
+
return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
|
|
4461
|
+
} finally {
|
|
4462
|
+
clearTimeout(timer);
|
|
4463
|
+
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
4151
4464
|
}
|
|
4152
|
-
const { pass, partial, fail, skipped } = result.summary;
|
|
4153
|
-
lines.push("");
|
|
4154
|
-
lines.push(
|
|
4155
|
-
`Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
4156
|
-
);
|
|
4157
|
-
return lines.join("\n");
|
|
4158
4465
|
}
|
|
4159
4466
|
export {
|
|
4160
4467
|
ACPProtocolHandler,
|
|
@@ -4162,6 +4469,8 @@ export {
|
|
|
4162
4469
|
ACPSessionError,
|
|
4163
4470
|
ACPToolsRegistry,
|
|
4164
4471
|
ACP_AGENT_COMMANDS,
|
|
4472
|
+
ACP_PACKAGE_VERSION,
|
|
4473
|
+
ACP_PROTOCOL_VERSION,
|
|
4165
4474
|
ACP_REGISTRY_URL,
|
|
4166
4475
|
AGENTS_CATALOG,
|
|
4167
4476
|
ClientTransport,
|
|
@@ -4174,6 +4483,7 @@ export {
|
|
|
4174
4483
|
ToolTranslator,
|
|
4175
4484
|
WebSocketClientTransport,
|
|
4176
4485
|
WrongStackACPServer,
|
|
4486
|
+
assertNeverSessionUpdate,
|
|
4177
4487
|
audioContent,
|
|
4178
4488
|
defaultEnsembleCmdResolver,
|
|
4179
4489
|
defaultPermissionPolicy,
|
|
@@ -4183,6 +4493,7 @@ export {
|
|
|
4183
4493
|
makeACPSubagentRunner,
|
|
4184
4494
|
makeACPSubagentRunnerWithStop,
|
|
4185
4495
|
makePermissionPolicy,
|
|
4496
|
+
makeTrustBoundaryPermissionPolicy,
|
|
4186
4497
|
mapRegistryEntry,
|
|
4187
4498
|
probeAcpAgent,
|
|
4188
4499
|
probeAcpAgents,
|
|
@@ -4193,6 +4504,7 @@ export {
|
|
|
4193
4504
|
runAcpBench,
|
|
4194
4505
|
runEnsemble,
|
|
4195
4506
|
runOneAcpTask,
|
|
4196
|
-
textContent
|
|
4507
|
+
textContent,
|
|
4508
|
+
toTrustBoundaryRequest
|
|
4197
4509
|
};
|
|
4198
4510
|
//# sourceMappingURL=index.js.map
|