@wrongstack/acp 0.297.0 → 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.
Files changed (55) hide show
  1. package/dist/agent/protocol-handler.d.ts +39 -46
  2. package/dist/agent/session-store.d.ts +55 -0
  3. package/dist/agent/stdio-transport.d.ts +74 -1
  4. package/dist/agent.js +472 -210
  5. package/dist/client/terminal-server.d.ts +5 -1
  6. package/dist/client.js +70 -44
  7. package/dist/index.js +433 -253
  8. package/dist/win32-cmd.d.ts +9 -6
  9. package/dist/wrongstack-acp-agent.js +312 -157
  10. package/package.json +4 -3
  11. package/dist/agent/index.d.ts.map +0 -1
  12. package/dist/agent/protocol-contract.d.ts.map +0 -1
  13. package/dist/agent/protocol-handler.d.ts.map +0 -1
  14. package/dist/agent/server-agent-turn.d.ts.map +0 -1
  15. package/dist/agent/session-store.d.ts.map +0 -1
  16. package/dist/agent/stdio-transport.d.ts.map +0 -1
  17. package/dist/agent/tools-registry.d.ts.map +0 -1
  18. package/dist/agent/wrongstack-acp-agent.d.ts.map +0 -1
  19. package/dist/agent/ws-bridge-transport.d.ts.map +0 -1
  20. package/dist/agent.js.map +0 -7
  21. package/dist/client/acp-message-routing.d.ts.map +0 -1
  22. package/dist/client/acp-session-callbacks.d.ts.map +0 -1
  23. package/dist/client/acp-session-content.d.ts.map +0 -1
  24. package/dist/client/acp-session-errors.d.ts.map +0 -1
  25. package/dist/client/acp-session-types.d.ts.map +0 -1
  26. package/dist/client/acp-session-updates.d.ts.map +0 -1
  27. package/dist/client/acp-session.d.ts.map +0 -1
  28. package/dist/client/file-server.d.ts.map +0 -1
  29. package/dist/client/index.d.ts.map +0 -1
  30. package/dist/client/permission.d.ts.map +0 -1
  31. package/dist/client/terminal-server.d.ts.map +0 -1
  32. package/dist/client/tool-translator.d.ts.map +0 -1
  33. package/dist/client/trust-boundary-permission.d.ts.map +0 -1
  34. package/dist/client/websocket-transport.d.ts.map +0 -1
  35. package/dist/client.js.map +0 -7
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js.map +0 -7
  38. package/dist/integration/acp-bench.d.ts.map +0 -1
  39. package/dist/integration/acp-subagent-runner.d.ts.map +0 -1
  40. package/dist/integration/ensemble-runner.d.ts.map +0 -1
  41. package/dist/integration/run-one-acp-task.d.ts.map +0 -1
  42. package/dist/legacy.d.ts.map +0 -1
  43. package/dist/legacy.js.map +0 -7
  44. package/dist/registry/acp-registry-fetch.d.ts.map +0 -1
  45. package/dist/registry/agents.catalog.d.ts.map +0 -1
  46. package/dist/registry/ensemble-registry.d.ts.map +0 -1
  47. package/dist/sdk.d.ts.map +0 -1
  48. package/dist/sdk.js.map +0 -7
  49. package/dist/types/acp-messages.d.ts.map +0 -1
  50. package/dist/types/acp-v1.d.ts.map +0 -1
  51. package/dist/v1.d.ts.map +0 -1
  52. package/dist/v1.js.map +0 -7
  53. package/dist/version.d.ts.map +0 -1
  54. package/dist/win32-cmd.d.ts.map +0 -1
  55. package/dist/wrongstack-acp-agent.js.map +0 -7
@@ -1,50 +1,6 @@
1
- /**
2
- * ACP v1 server-side protocol handler.
3
- *
4
- * Receives JSON-RPC requests from an external ACP client (Zed, JetBrains
5
- * Junie, VS Code ACP extension, etc.) over stdio and answers them per the
6
- * v1 spec. See https://agentclientprotocol.com/protocol/v1/overview.
7
- *
8
- * Supported methods
9
- * ─────────────────
10
- * - initialize — handshake
11
- * - authenticate — optional, no-op when auth isn't required
12
- * - session/new — create a session
13
- * - session/load — restore a session by id
14
- * - session/prompt — run one turn, stream session/update
15
- * notifications, return stopReason
16
- * - session/cancel — notification (no response); cancels the
17
- * in-flight turn on the target session
18
- * - session/set_mode — change the active mode for a session
19
- * - session/set_config_option — change a config option value
20
- * - session/list — list known sessions
21
- *
22
- * Method execution
23
- * ────────────────
24
- * The handler is transport-agnostic; it sends responses via the
25
- * `AgentServerTransport` injected at construction. The actual
26
- * agent-loop work for a `session/prompt` turn is delegated to the
27
- * caller-provided `runTurn` callback, which receives the prompt
28
- * blocks and the per-turn AbortSignal and resolves with the final
29
- * stopReason. Updates are streamed via the `emit` callback passed
30
- * to `runTurn`; the handler wraps each as a `session/update`
31
- * notification.
32
- *
33
- * This separation keeps the handler unit-testable: tests can supply
34
- * a fake `runTurn` that yields a canned sequence of updates, and
35
- * assert on the JSON-RPC traffic the handler produces. A real
36
- * production caller wires `runTurn` to a core `Agent` instance.
37
- *
38
- * Concurrency
39
- * ───────────
40
- * Each session is single-threaded (one active turn at a time). The
41
- * handler keeps a per-session AbortController so a `session/cancel`
42
- * notification can stop the running turn mid-stream without tearing
43
- * down the session. Multiple sessions can be active concurrently.
44
- */
45
- import { WRONGSTACK_VERSION, type ProtocolHandlerOptions } from './protocol-contract.js';
46
- export { WRONGSTACK_VERSION };
1
+ import { type ProtocolHandlerOptions, WRONGSTACK_VERSION } from './protocol-contract.js';
47
2
  export type { AgentCapabilities, ClientCapabilities, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
3
+ export { WRONGSTACK_VERSION };
48
4
  export declare class ACPProtocolHandler {
49
5
  private readonly transport;
50
6
  private readonly defaultCwd;
@@ -104,6 +60,43 @@ export declare class ACPProtocolHandler {
104
60
  /** Best-effort durable persistence of a session + its recorded history. */
105
61
  private persist;
106
62
  private sendError;
63
+ /**
64
+ * Allocate a session id (WS-015).
65
+ *
66
+ * This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
67
+ * handler has no per-connection ownership: any caller that names a session
68
+ * id can `session/load`, `session/prompt`, `session/cancel` or
69
+ * `session/delete` it. Over stdio that is academic (one client per process),
70
+ * but the agent also serves over HTTP, where a guessable id is the whole
71
+ * authorization story for any local process or page that reaches the port.
72
+ *
73
+ * Random ids do not create ownership — they remove the trivial enumeration
74
+ * that made its absence exploitable. Real per-connection ownership is the
75
+ * larger fix and is noted in the WS-015 test file.
76
+ *
77
+ * The counter is retained: it keeps ids ordered for debugging and guarantees
78
+ * uniqueness within a process even in the (impossible) event of a UUID
79
+ * collision. The random half is what makes the id unguessable.
80
+ */
81
+ /**
82
+ * Resolve a client-supplied `cwd` for a session, or `null` when it is not
83
+ * usable (WS-015).
84
+ *
85
+ * `session/new`, `session/load` and `session/fork` all took `params.cwd`
86
+ * with a single `typeof === 'string'` check and nothing else. That value is
87
+ * the working directory the agent then reads, writes and executes in.
88
+ *
89
+ * SCOPE, deliberately stated: this does NOT confine the session to a root.
90
+ * In ACP the client IS the editor and legitimately names its own workspace —
91
+ * Zed and JetBrains pass the project root — so a fixed boundary here would
92
+ * break the integration this package exists for. What it enforces is that
93
+ * the directory is absolute and actually exists as a directory: a relative
94
+ * or missing `cwd` is a bug or an attack under either reading, and silently
95
+ * running the agent somewhere other than where the client asked is worse
96
+ * than refusing. Confinement, if wanted, belongs in an operator-set option
97
+ * on top of this, not in place of it.
98
+ */
99
+ private resolveSessionCwd;
107
100
  private allocId;
108
101
  }
109
102
  declare function errorToJsonRpc(err: unknown): {
@@ -21,7 +21,43 @@ export declare class ACPSessionStore {
21
21
  * Cleared automatically if the directory disappears between calls.
22
22
  */
23
23
  private initialized;
24
+ /**
25
+ * Tail of in-flight sidecar-index mutations. `readIndex`/`writeIndex`/
26
+ * `updateIndex`/delete's index-touching branch all run through the
27
+ * `withIndexLock` gate so a concurrent save/delete/save cannot read the
28
+ * same baseline twice and clobber the other writer's edit (Chimera
29
+ * HIGH — race that could permanently omit or resurrect sessions in
30
+ * `list()`). Mirrors the `writeChains` pattern in
31
+ * `packages/core/src/storage/tool-audit-log.ts`.
32
+ */
33
+ private indexChain;
34
+ /**
35
+ * Monotonic per-store counter. Used to make tmp filenames unique so two
36
+ * concurrent saves started in the same millisecond cannot collide on
37
+ * `<target>.<pid>.<ts>.tmp` and lose one's tmp mid-write (latent bug
38
+ * discovered by the concurrent same-id save/delete stress test).
39
+ */
40
+ private writeSeq;
24
41
  constructor(opts?: SessionStoreOptions);
42
+ /**
43
+ * Path of `<sessionId>.json` inside the store, or `null` when `sessionId`
44
+ * is not usable as a single path segment (WS-015).
45
+ *
46
+ * The session id arrives from the ACP client — `session/load`,
47
+ * `session/close` and `session/delete` all take it verbatim from the wire.
48
+ * It was joined straight onto the store directory, so `../../..` escaped it,
49
+ * and the `.json` suffix was the only thing narrowing the blast radius:
50
+ *
51
+ * - `load` — read any `.json` on the machine, e.g. the provider config
52
+ * holding API keys
53
+ * - `delete` — unlink any `.json` on the machine
54
+ * - `save` — the worst one: a loaded session keeps the traversing id as
55
+ * its `state.id`, so the next persist WRITES to that path
56
+ *
57
+ * All three now route through here, so the escape is closed once rather than
58
+ * three times.
59
+ */
60
+ private sessionFile;
25
61
  /** Ensure the store directory exists. Memoized — only mkdirs once. */
26
62
  init(): Promise<void>;
27
63
  /**
@@ -37,8 +73,27 @@ export declare class ACPSessionStore {
37
73
  id: string;
38
74
  updatedAt: string;
39
75
  }>>;
76
+ /**
77
+ * Walk `<dir>` and parse every session file into `{id, updatedAt}`
78
+ * metadata. Used as the slow-path fallback by `list()` and by
79
+ * `updateIndex()` when the sidecar index is missing. Pure read —
80
+ * does NOT touch `index.json`.
81
+ */
82
+ private scanForSessions;
40
83
  /** Sidecar path that stores `{id, updatedAt}` for every saved session. */
41
84
  private indexPath;
85
+ /**
86
+ * Serialize sidecar-index mutations. Each call appends `fn` to the tail
87
+ * of `indexChain` so reads and writes see a linearized order — a second
88
+ * caller cannot start until the first one's index update has finished.
89
+ *
90
+ * The chain swallows errors (`prev.then(fn, fn)`) so one failing writer
91
+ * does not poison every subsequent caller, and the catch keeps an
92
+ * unhandled rejection from leaking out of the stored tail. The cleanup
93
+ * removes the tail entry once settled, so a quiet store does not retain
94
+ * stale promises forever.
95
+ */
96
+ private withIndexLock;
42
97
  /** Read the sidecar index. Returns `null` when missing or unreadable. */
43
98
  private readIndex;
44
99
  /** Atomically replace the sidecar index with the supplied entries. */
@@ -4,8 +4,33 @@ export interface AgentServerTransport {
4
4
  send(msg: ACPMessage): Promise<void>;
5
5
  sendRaw(chunk: string): void;
6
6
  read(): Promise<ACPMessage | null>;
7
- close(): void;
7
+ /**
8
+ * Register an observer that fires for every inbound message — including
9
+ * messages that resolve a parked `read()` call.
10
+ *
11
+ * Dual-fire contract: a message is delivered to BOTH the `read()`
12
+ * promise (if one is parked) AND every handler registered here. The
13
+ * legacy behavior was "no fire while read() is parked"; the new
14
+ * behavior was adopted to fix a deadlock when a JSON-RPC reply
15
+ * arrived before the next read() call (handlers that correlate
16
+ * responses to outbound requests would never see it). Handlers MUST
17
+ * tolerate being called for every inbound message and MUST NOT
18
+ * assume the message has not also been delivered to read().
19
+ *
20
+ * To consume a message exclusively (prevent it from being enqueued for
21
+ * read()), use `onMessageClaim()` instead. Most observers want
22
+ * `onMessage`; correlation handlers and the protocol layer want
23
+ * `onMessageClaim`.
24
+ */
8
25
  onMessage(handler: (msg: ACPMessage) => void): () => void;
26
+ /**
27
+ * Register a handler that MAY claim a message by returning `true`.
28
+ * Claimed messages are NOT enqueued for the read() loop. Required
29
+ * for the server transport so the correlation handler (which always
30
+ * fires) doesn't starve the read loop of pipelined requests and
31
+ * notifications. See `onMessageClaim` on the implementations.
32
+ */
33
+ onMessageClaim?(handler: (msg: ACPMessage) => boolean): () => void;
9
34
  }
10
35
  /**
11
36
  * Minimal client-side transport contract `ACPSession` drives. `ClientTransport`
@@ -15,7 +40,21 @@ export interface AgentServerTransport {
15
40
  export interface ACPClientTransport {
16
41
  start(): Promise<void>;
17
42
  send(msg: ACPMessage): Promise<void>;
43
+ /**
44
+ * See {@link AgentServerTransport.onMessage}. The client contract has the
45
+ * same dual-fire guarantee: handlers fire for every inbound message,
46
+ * regardless of any concurrent reader. Client-side consumers always
47
+ * observe the full message stream via onMessage, so claim semantics
48
+ * are not needed — see {@link onMessageClaim} for the optional
49
+ * claim variant.
50
+ */
18
51
  onMessage(handler: (msg: ACPMessage) => void): () => void;
52
+ /**
53
+ * See {@link AgentServerTransport.onMessageClaim}. Optional on the
54
+ * client contract: client-side consumers always observe the full
55
+ * message stream via onMessage, so claim semantics are not needed.
56
+ */
57
+ onMessageClaim?(handler: (msg: ACPMessage) => boolean): () => void;
19
58
  stop(): void;
20
59
  }
21
60
  export declare class StdioTransport implements AgentServerTransport {
@@ -24,20 +63,38 @@ export declare class StdioTransport implements AgentServerTransport {
24
63
  private readonly stderr;
25
64
  private buffer;
26
65
  private readonly handlers;
66
+ private readonly claimHandlers;
27
67
  private closed;
28
68
  private resolveRead;
29
69
  private messageQueue;
70
+ private queuedChars;
30
71
  private readonly maxFrameChars;
31
72
  private readonly maxQueuedMessages;
73
+ private readonly maxQueuedChars;
74
+ private readonly onStdinData;
75
+ private readonly onStdinEnd;
76
+ private readonly onStdinError;
32
77
  constructor(opts?: {
33
78
  maxFrameChars?: number;
34
79
  maxQueuedMessages?: number;
80
+ maxQueuedChars?: number;
35
81
  });
36
82
  sendStartupMarker(): void;
37
83
  send(msg: ACPMessage): Promise<void>;
38
84
  sendRaw(chunk: string): void;
39
85
  read(): Promise<ACPMessage | null>;
40
86
  onMessage(handler: (msg: ACPMessage) => void): () => void;
87
+ /**
88
+ * Register a handler that MAY claim a message by returning `true`.
89
+ * Claimed messages are NOT enqueued for the read() loop — they are
90
+ * considered fully consumed by the handler. This is how the ACP
91
+ * server transport prevents the correlation handler (which always
92
+ * fires on every message but only actually correlates responses)
93
+ * from starving the read() loop of pipelined requests, notifications,
94
+ * and any message the handler chose to ignore. See dispatch() for
95
+ * the gating logic.
96
+ */
97
+ onMessageClaim(handler: (msg: ACPMessage) => boolean): () => void;
41
98
  close(): void;
42
99
  private onData;
43
100
  private dispatch;
@@ -62,6 +119,8 @@ export interface ClientTransportOptions {
62
119
  maxFrameChars?: number | undefined;
63
120
  /** Maximum messages retained for the optional read() API. Default 1000. */
64
121
  maxQueuedMessages?: number | undefined;
122
+ /** Maximum serialized characters retained for the optional read() API. Default 32 MiB. */
123
+ maxQueuedChars?: number | undefined;
65
124
  }
66
125
  export interface ACPChildProcess extends EventEmitter {
67
126
  stdout: NodeJS.ReadableStream;
@@ -74,17 +133,31 @@ export declare class ClientTransport implements ACPClientTransport {
74
133
  private child;
75
134
  private buffer;
76
135
  private readonly handlers;
136
+ private readonly claimHandlers;
77
137
  private closed;
78
138
  private resolveRead;
79
139
  private messageQueue;
140
+ private queuedChars;
80
141
  private readonly opts;
81
142
  private readonly maxFrameChars;
82
143
  private readonly maxQueuedMessages;
144
+ private readonly maxQueuedChars;
83
145
  constructor(options: ClientTransportOptions);
84
146
  start(): Promise<void>;
85
147
  send(msg: ACPMessage): Promise<void>;
86
148
  read(): Promise<ACPMessage | null>;
87
149
  onMessage(handler: (msg: ACPMessage) => void): () => void;
150
+ /**
151
+ * Register a handler that MAY claim a message by returning `true`.
152
+ * Claimed messages are NOT enqueued for the read() loop — they are
153
+ * considered fully consumed by the handler. This is how the ACP
154
+ * server transport prevents the correlation handler (which always
155
+ * fires on every message but only actually correlates responses)
156
+ * from starving the read() loop of pipelined requests, notifications,
157
+ * and any message the handler chose to ignore. See dispatch() for
158
+ * the gating logic.
159
+ */
160
+ onMessageClaim(handler: (msg: ACPMessage) => boolean): () => void;
88
161
  stop(): void;
89
162
  private onChildData;
90
163
  private onChildError;