@wrongstack/acp 0.296.4 → 0.298.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/protocol-handler.d.ts +39 -46
- package/dist/agent/session-store.d.ts +55 -0
- package/dist/agent/stdio-transport.d.ts +74 -1
- package/dist/agent.js +472 -210
- package/dist/client/terminal-server.d.ts +5 -1
- package/dist/client/websocket-transport.d.ts +2 -0
- package/dist/client.js +122 -56
- package/dist/index.js +484 -264
- package/dist/win32-cmd.d.ts +9 -6
- package/dist/wrongstack-acp-agent.js +312 -157
- package/package.json +4 -3
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/protocol-contract.d.ts.map +0 -1
- package/dist/agent/protocol-handler.d.ts.map +0 -1
- package/dist/agent/server-agent-turn.d.ts.map +0 -1
- package/dist/agent/session-store.d.ts.map +0 -1
- package/dist/agent/stdio-transport.d.ts.map +0 -1
- package/dist/agent/tools-registry.d.ts.map +0 -1
- package/dist/agent/wrongstack-acp-agent.d.ts.map +0 -1
- package/dist/agent/ws-bridge-transport.d.ts.map +0 -1
- package/dist/agent.js.map +0 -7
- package/dist/client/acp-message-routing.d.ts.map +0 -1
- package/dist/client/acp-session-callbacks.d.ts.map +0 -1
- package/dist/client/acp-session-content.d.ts.map +0 -1
- package/dist/client/acp-session-errors.d.ts.map +0 -1
- package/dist/client/acp-session-types.d.ts.map +0 -1
- package/dist/client/acp-session-updates.d.ts.map +0 -1
- package/dist/client/acp-session.d.ts.map +0 -1
- package/dist/client/file-server.d.ts.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/permission.d.ts.map +0 -1
- package/dist/client/terminal-server.d.ts.map +0 -1
- package/dist/client/tool-translator.d.ts.map +0 -1
- package/dist/client/trust-boundary-permission.d.ts.map +0 -1
- package/dist/client/websocket-transport.d.ts.map +0 -1
- package/dist/client.js.map +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/integration/acp-bench.d.ts.map +0 -1
- package/dist/integration/acp-subagent-runner.d.ts.map +0 -1
- package/dist/integration/ensemble-runner.d.ts.map +0 -1
- package/dist/integration/run-one-acp-task.d.ts.map +0 -1
- package/dist/legacy.d.ts.map +0 -1
- package/dist/legacy.js.map +0 -7
- package/dist/registry/acp-registry-fetch.d.ts.map +0 -1
- package/dist/registry/agents.catalog.d.ts.map +0 -1
- package/dist/registry/ensemble-registry.d.ts.map +0 -1
- package/dist/sdk.d.ts.map +0 -1
- package/dist/sdk.js.map +0 -7
- package/dist/types/acp-messages.d.ts.map +0 -1
- package/dist/types/acp-v1.d.ts.map +0 -1
- package/dist/v1.d.ts.map +0 -1
- package/dist/v1.js.map +0 -7
- package/dist/version.d.ts.map +0 -1
- package/dist/win32-cmd.d.ts.map +0 -1
- package/dist/wrongstack-acp-agent.js.map +0 -7
|
@@ -55,7 +55,11 @@ export declare class TerminalServer {
|
|
|
55
55
|
exitCode: number | null;
|
|
56
56
|
signal: string | null;
|
|
57
57
|
}>;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Kill the process but keep the terminal record (agent can still read output).
|
|
60
|
+
* On POSIX this signals only the direct child; descendants may survive because
|
|
61
|
+
* terminal processes are not spawned as process-group leaders.
|
|
62
|
+
*/
|
|
59
63
|
kill(terminalId: string): void;
|
|
60
64
|
/** Kill the process if alive and remove the record. */
|
|
61
65
|
release(terminalId: string): void;
|
|
@@ -36,6 +36,8 @@ export declare class WebSocketClientTransport implements ACPClientTransport {
|
|
|
36
36
|
private readonly maxBufferedBytes;
|
|
37
37
|
private readonly maxMessageChars;
|
|
38
38
|
constructor(opts: WebSocketClientTransportOptions);
|
|
39
|
+
/** Pending start() promise resolve/reject — settled in stop() to avoid leaking. */
|
|
40
|
+
private pendingStart;
|
|
39
41
|
start(): Promise<void>;
|
|
40
42
|
send(msg: ACPMessage): Promise<void>;
|
|
41
43
|
onMessage(handler: (msg: ACPMessage) => void): () => void;
|
package/dist/client.js
CHANGED
|
@@ -3,32 +3,14 @@ import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
|
3
3
|
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
4
4
|
|
|
5
5
|
// src/win32-cmd.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
10
|
-
return {
|
|
11
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
12
|
-
args: ["/d", "/c", line],
|
|
13
|
-
windowsVerbatimArguments: true
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
function assertSafeWin32CmdArgs(args) {
|
|
17
|
-
for (const arg of args) {
|
|
18
|
-
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
19
|
-
throw new Error(
|
|
20
|
-
'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)
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function quoteWin32CmdArg(arg) {
|
|
26
|
-
return `"${arg}"`;
|
|
27
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
buildWin32CmdShimInvocation
|
|
8
|
+
} from "@wrongstack/core/utils";
|
|
28
9
|
|
|
29
10
|
// src/agent/stdio-transport.ts
|
|
30
11
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
31
12
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
13
|
+
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
32
14
|
function positiveLimit(value, fallback) {
|
|
33
15
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
34
16
|
}
|
|
@@ -36,12 +18,15 @@ var ClientTransport = class {
|
|
|
36
18
|
child = null;
|
|
37
19
|
buffer = "";
|
|
38
20
|
handlers = /* @__PURE__ */ new Set();
|
|
21
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
39
22
|
closed = false;
|
|
40
23
|
resolveRead = null;
|
|
41
24
|
messageQueue = [];
|
|
25
|
+
queuedChars = 0;
|
|
42
26
|
opts;
|
|
43
27
|
maxFrameChars;
|
|
44
28
|
maxQueuedMessages;
|
|
29
|
+
maxQueuedChars;
|
|
45
30
|
constructor(options) {
|
|
46
31
|
this.opts = {
|
|
47
32
|
handshakeTimeoutMs: 3e4,
|
|
@@ -49,6 +34,7 @@ var ClientTransport = class {
|
|
|
49
34
|
};
|
|
50
35
|
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
51
36
|
this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
37
|
+
this.maxQueuedChars = positiveLimit(options.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
52
38
|
}
|
|
53
39
|
async start() {
|
|
54
40
|
if (this.child) return;
|
|
@@ -59,6 +45,14 @@ var ClientTransport = class {
|
|
|
59
45
|
]);
|
|
60
46
|
return new Promise((resolve3, reject) => {
|
|
61
47
|
const timeout = setTimeout(() => {
|
|
48
|
+
const child2 = this.child;
|
|
49
|
+
this.child = null;
|
|
50
|
+
if (child2) {
|
|
51
|
+
try {
|
|
52
|
+
treeKill(child2);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
62
56
|
reject(
|
|
63
57
|
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
64
58
|
);
|
|
@@ -141,8 +135,11 @@ var ClientTransport = class {
|
|
|
141
135
|
});
|
|
142
136
|
}
|
|
143
137
|
read() {
|
|
144
|
-
if (this.messageQueue.length > 0)
|
|
145
|
-
|
|
138
|
+
if (this.messageQueue.length > 0) {
|
|
139
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
140
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
141
|
+
return Promise.resolve(queued.message);
|
|
142
|
+
}
|
|
146
143
|
if (this.closed) return Promise.resolve(null);
|
|
147
144
|
return new Promise((resolve3) => {
|
|
148
145
|
this.resolveRead = resolve3;
|
|
@@ -152,13 +149,29 @@ var ClientTransport = class {
|
|
|
152
149
|
this.handlers.add(handler);
|
|
153
150
|
return () => this.handlers.delete(handler);
|
|
154
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
154
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
155
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
156
|
+
* server transport prevents the correlation handler (which always
|
|
157
|
+
* fires on every message but only actually correlates responses)
|
|
158
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
159
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
160
|
+
* the gating logic.
|
|
161
|
+
*/
|
|
162
|
+
onMessageClaim(handler) {
|
|
163
|
+
this.claimHandlers.add(handler);
|
|
164
|
+
return () => this.claimHandlers.delete(handler);
|
|
165
|
+
}
|
|
155
166
|
stop() {
|
|
156
167
|
this.closed = true;
|
|
157
168
|
this.resolveRead?.(null);
|
|
158
169
|
this.resolveRead = null;
|
|
159
170
|
this.buffer = "";
|
|
160
171
|
this.messageQueue.length = 0;
|
|
172
|
+
this.queuedChars = 0;
|
|
161
173
|
this.handlers.clear();
|
|
174
|
+
this.claimHandlers.clear();
|
|
162
175
|
const child = this.child;
|
|
163
176
|
if (!child) return;
|
|
164
177
|
treeKill(child);
|
|
@@ -183,7 +196,7 @@ var ClientTransport = class {
|
|
|
183
196
|
return;
|
|
184
197
|
}
|
|
185
198
|
try {
|
|
186
|
-
this.dispatch(JSON.parse(raw));
|
|
199
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
187
200
|
} catch {
|
|
188
201
|
}
|
|
189
202
|
}
|
|
@@ -193,29 +206,34 @@ var ClientTransport = class {
|
|
|
193
206
|
}
|
|
194
207
|
onChildClose(code) {
|
|
195
208
|
this.closed = true;
|
|
209
|
+
this.child = null;
|
|
196
210
|
this.resolveRead?.(null);
|
|
197
211
|
this.resolveRead = null;
|
|
198
212
|
this.buffer = "";
|
|
199
213
|
this.messageQueue.length = 0;
|
|
214
|
+
this.queuedChars = 0;
|
|
200
215
|
this.handlers.clear();
|
|
201
216
|
if (code !== 0 && code !== null) {
|
|
202
217
|
writeErr(`[acp-child exited with code ${code}]
|
|
203
218
|
`);
|
|
204
219
|
}
|
|
205
220
|
}
|
|
206
|
-
dispatch(msg) {
|
|
221
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
207
222
|
if (this.resolveRead) {
|
|
208
223
|
const resolve3 = this.resolveRead;
|
|
209
224
|
this.resolveRead = null;
|
|
210
225
|
resolve3(msg);
|
|
211
226
|
} else if (this.handlers.size === 0) {
|
|
212
|
-
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
213
|
-
writeErr(
|
|
214
|
-
`
|
|
227
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
228
|
+
writeErr(
|
|
229
|
+
`[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
|
|
230
|
+
`
|
|
231
|
+
);
|
|
215
232
|
this.stop();
|
|
216
233
|
return;
|
|
217
234
|
}
|
|
218
|
-
this.messageQueue.push(msg);
|
|
235
|
+
this.messageQueue.push({ message: msg, chars });
|
|
236
|
+
this.queuedChars += chars;
|
|
219
237
|
}
|
|
220
238
|
for (const handler of this.handlers) {
|
|
221
239
|
try {
|
|
@@ -500,6 +518,7 @@ import { spawn } from "node:child_process";
|
|
|
500
518
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
501
519
|
import * as path2 from "node:path";
|
|
502
520
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
521
|
+
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
503
522
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
504
523
|
var TerminalServer = class {
|
|
505
524
|
terminals = /* @__PURE__ */ new Map();
|
|
@@ -619,13 +638,11 @@ var TerminalServer = class {
|
|
|
619
638
|
state.outputHead = 0;
|
|
620
639
|
}
|
|
621
640
|
};
|
|
641
|
+
state.onData = onData;
|
|
622
642
|
proc.stdout?.on("data", onData);
|
|
623
643
|
proc.stderr?.on("data", onData);
|
|
624
644
|
state.timeoutHandle = setTimeout(() => {
|
|
625
|
-
|
|
626
|
-
proc.kill("SIGTERM");
|
|
627
|
-
} catch {
|
|
628
|
-
}
|
|
645
|
+
treeKill2(proc);
|
|
629
646
|
}, this.commandTimeoutMs);
|
|
630
647
|
this.terminals.set(id, state);
|
|
631
648
|
return { terminalId: id };
|
|
@@ -649,14 +666,15 @@ var TerminalServer = class {
|
|
|
649
666
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
650
667
|
return state.exitPromise;
|
|
651
668
|
}
|
|
652
|
-
/**
|
|
669
|
+
/**
|
|
670
|
+
* Kill the process but keep the terminal record (agent can still read output).
|
|
671
|
+
* On POSIX this signals only the direct child; descendants may survive because
|
|
672
|
+
* terminal processes are not spawned as process-group leaders.
|
|
673
|
+
*/
|
|
653
674
|
kill(terminalId) {
|
|
654
675
|
const state = this.terminals.get(terminalId);
|
|
655
676
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
656
|
-
|
|
657
|
-
state.proc.kill("SIGTERM");
|
|
658
|
-
} catch {
|
|
659
|
-
}
|
|
677
|
+
treeKill2(state.proc);
|
|
660
678
|
}
|
|
661
679
|
/** Kill the process if alive and remove the record. */
|
|
662
680
|
release(terminalId) {
|
|
@@ -666,10 +684,17 @@ var TerminalServer = class {
|
|
|
666
684
|
clearTimeout(state.timeoutHandle);
|
|
667
685
|
state.timeoutHandle = null;
|
|
668
686
|
}
|
|
669
|
-
|
|
670
|
-
state.proc.
|
|
671
|
-
|
|
672
|
-
|
|
687
|
+
if (state.onData) {
|
|
688
|
+
state.proc.stdout?.off("data", state.onData);
|
|
689
|
+
state.proc.stderr?.off("data", state.onData);
|
|
690
|
+
state.onData = void 0;
|
|
691
|
+
}
|
|
692
|
+
state.proc.stdout?.destroy?.();
|
|
693
|
+
state.proc.stderr?.destroy?.();
|
|
694
|
+
state.outputChunks.length = 0;
|
|
695
|
+
state.outputHead = 0;
|
|
696
|
+
state.retainedBytes = 0;
|
|
697
|
+
treeKill2(state.proc, { force: true });
|
|
673
698
|
this.terminals.delete(terminalId);
|
|
674
699
|
}
|
|
675
700
|
/** Kill all active terminals. Used on session close. */
|
|
@@ -832,7 +857,12 @@ var WebSocketClientTransport = class {
|
|
|
832
857
|
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
833
858
|
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
834
859
|
}
|
|
860
|
+
/** Pending start() promise resolve/reject — settled in stop() to avoid leaking. */
|
|
861
|
+
pendingStart = null;
|
|
835
862
|
start() {
|
|
863
|
+
if (this.closed || this.ws !== null || this.pendingStart !== null) {
|
|
864
|
+
return Promise.reject(new Error("WebSocket transport has already been started or stopped"));
|
|
865
|
+
}
|
|
836
866
|
const WS = globalThis.WebSocket;
|
|
837
867
|
if (!WS) {
|
|
838
868
|
return Promise.reject(
|
|
@@ -843,35 +873,53 @@ var WebSocketClientTransport = class {
|
|
|
843
873
|
}
|
|
844
874
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
845
875
|
return new Promise((resolve3, reject) => {
|
|
846
|
-
let settled = false;
|
|
847
876
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
848
877
|
this.ws = ws;
|
|
849
878
|
const timer = setTimeout(() => {
|
|
850
|
-
|
|
879
|
+
const pending = this.pendingStart;
|
|
880
|
+
if (pending === null) return;
|
|
881
|
+
this.pendingStart = null;
|
|
882
|
+
this.closed = true;
|
|
883
|
+
if (this.ws === ws) this.ws = null;
|
|
884
|
+
this.handlers.clear();
|
|
851
885
|
try {
|
|
852
886
|
ws.close();
|
|
853
887
|
} catch {
|
|
854
888
|
}
|
|
855
|
-
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
889
|
+
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
856
890
|
}, timeoutMs);
|
|
891
|
+
this.pendingStart = { resolve: resolve3, reject, timer };
|
|
857
892
|
ws.addEventListener("open", () => {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
893
|
+
const pending = this.pendingStart;
|
|
894
|
+
if (pending === null) return;
|
|
895
|
+
this.pendingStart = null;
|
|
896
|
+
clearTimeout(pending.timer);
|
|
897
|
+
pending.resolve();
|
|
862
898
|
});
|
|
863
899
|
ws.addEventListener("error", (ev) => {
|
|
864
|
-
|
|
865
|
-
|
|
900
|
+
const pending = this.pendingStart;
|
|
901
|
+
if (pending === null) {
|
|
902
|
+
this.stop();
|
|
866
903
|
return;
|
|
867
904
|
}
|
|
868
|
-
|
|
869
|
-
|
|
905
|
+
this.pendingStart = null;
|
|
906
|
+
this.closed = true;
|
|
907
|
+
if (this.ws === ws) this.ws = null;
|
|
908
|
+
this.handlers.clear();
|
|
909
|
+
clearTimeout(pending.timer);
|
|
870
910
|
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
871
|
-
reject(new Error(message));
|
|
911
|
+
pending.reject(new Error(message));
|
|
872
912
|
});
|
|
873
913
|
ws.addEventListener("close", () => {
|
|
874
914
|
this.closed = true;
|
|
915
|
+
if (this.ws === ws) this.ws = null;
|
|
916
|
+
this.handlers.clear();
|
|
917
|
+
const pending = this.pendingStart;
|
|
918
|
+
if (pending !== null) {
|
|
919
|
+
this.pendingStart = null;
|
|
920
|
+
clearTimeout(pending.timer);
|
|
921
|
+
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
922
|
+
}
|
|
875
923
|
});
|
|
876
924
|
ws.addEventListener("message", (ev) => {
|
|
877
925
|
this.onData(ev.data);
|
|
@@ -901,6 +949,16 @@ var WebSocketClientTransport = class {
|
|
|
901
949
|
}
|
|
902
950
|
stop() {
|
|
903
951
|
this.closed = true;
|
|
952
|
+
this.handlers.clear();
|
|
953
|
+
if (this.pendingStart !== null) {
|
|
954
|
+
const pending = this.pendingStart;
|
|
955
|
+
this.pendingStart = null;
|
|
956
|
+
clearTimeout(pending.timer);
|
|
957
|
+
try {
|
|
958
|
+
pending.reject(new Error("WebSocket transport stopped while connecting"));
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
904
962
|
if (this.ws) {
|
|
905
963
|
try {
|
|
906
964
|
this.ws.close();
|
|
@@ -1313,7 +1371,15 @@ var ACPSession = class _ACPSession {
|
|
|
1313
1371
|
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
1314
1372
|
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
1315
1373
|
const transport = new ClientTransport(transportOpts);
|
|
1316
|
-
|
|
1374
|
+
try {
|
|
1375
|
+
return await _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
try {
|
|
1378
|
+
transport.stop();
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
throw err;
|
|
1382
|
+
}
|
|
1317
1383
|
}
|
|
1318
1384
|
/**
|
|
1319
1385
|
* Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
|