@wrongstack/acp 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/protocol-contract.d.ts +22 -2
- package/dist/agent/protocol-handler.d.ts +1 -1
- package/dist/agent/protocol-session-ops.d.ts +12 -1
- package/dist/agent/server-agent-turn.d.ts +6 -2
- package/dist/agent.js +122 -16
- package/dist/client/acp-session-errors.d.ts +6 -0
- package/dist/client/acp-session.d.ts +12 -0
- package/dist/client.js +90 -4
- package/dist/index.js +221 -35
- package/dist/integration/acp-subagent-runner.d.ts +8 -2
- package/dist/registry/acp-registry-fetch.d.ts +2 -0
- package/dist/types/acp-v1.d.ts +9 -1
- package/dist/wrongstack-acp-agent.js +116 -15
- package/package.json +3 -2
package/dist/agent/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ClientCapabilities, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
|
|
1
|
+
export type { ClientCapabilities, McpServer, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
|
|
2
2
|
export { ACPProtocolHandler } from './protocol-handler.js';
|
|
3
3
|
export type { ACPServerAgentTurnOptions } from './server-agent-turn.js';
|
|
4
4
|
export { makeACPServerAgentTurn } from './server-agent-turn.js';
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Keeping these declarations separate leaves the handler focused on request dispatch.
|
|
4
4
|
*/
|
|
5
5
|
import type { ACPMessage } from '../types/acp-messages.js';
|
|
6
|
-
import { ACP_PROTOCOL_VERSION, type ContentBlock, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
|
|
6
|
+
import { ACP_PROTOCOL_VERSION, type ContentBlock, type McpServer, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
|
|
7
7
|
import type { AgentServerTransport } from './stdio-transport.js';
|
|
8
|
-
export type { ACPMessage, ContentBlock, RequestPermissionOutcome };
|
|
8
|
+
export type { ACPMessage, ContentBlock, McpServer, RequestPermissionOutcome };
|
|
9
9
|
export { ACP_PROTOCOL_VERSION };
|
|
10
10
|
type WireMessage = {
|
|
11
11
|
jsonrpc?: '2.0';
|
|
@@ -33,6 +33,19 @@ export interface RunTurnInput {
|
|
|
33
33
|
prompt: readonly ContentBlock[];
|
|
34
34
|
/** Cancelled when the client sends `session/cancel` for this session. */
|
|
35
35
|
signal: AbortSignal;
|
|
36
|
+
/** Session working directory from `session/new` (absolute). */
|
|
37
|
+
cwd?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* MCP servers the CLIENT asked this session to connect to, as supplied in
|
|
40
|
+
* `session/new` / `session/load` / `session/fork`. Validated at the wire
|
|
41
|
+
* boundary (see `parseMcpServers`), so entries here are well-formed.
|
|
42
|
+
*
|
|
43
|
+
* The runTurn implementation owns the connection: it is what holds the tool
|
|
44
|
+
* registry these servers' tools must land in. Nothing here is optional
|
|
45
|
+
* decoration — an editor that passes a server and gets no tools has been
|
|
46
|
+
* told a successful `session/new` about work that never happened.
|
|
47
|
+
*/
|
|
48
|
+
mcpServers?: readonly McpServer[] | undefined;
|
|
36
49
|
}
|
|
37
50
|
export interface RunTurnResult {
|
|
38
51
|
stopReason: StopReason;
|
|
@@ -127,6 +140,13 @@ export interface SessionState {
|
|
|
127
140
|
updatedAt: string;
|
|
128
141
|
/** Optional human title. */
|
|
129
142
|
title?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The client's MCP servers for this session, captured at `session/new`
|
|
145
|
+
* (or `load`/`fork`) and replayed into every turn. Held on the session
|
|
146
|
+
* because the connection is session-scoped: two ACP sessions may ask for
|
|
147
|
+
* different servers, and closing one must not tear down the other's.
|
|
148
|
+
*/
|
|
149
|
+
mcpServers?: readonly McpServer[] | undefined;
|
|
130
150
|
}
|
|
131
151
|
/** MCP-style session mode advertised in current_mode_update. */
|
|
132
152
|
export interface SessionMode {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ProtocolHandlerOptions, WRONGSTACK_VERSION } from './protocol-contract.js';
|
|
2
2
|
import { errorToJsonRpc } from './protocol-session-ops.js';
|
|
3
|
-
export type { AgentCapabilities, ClientCapabilities, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
|
|
3
|
+
export type { AgentCapabilities, ClientCapabilities, McpServer, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
|
|
4
4
|
export { WRONGSTACK_VERSION };
|
|
5
5
|
export declare class ACPProtocolHandler {
|
|
6
6
|
private readonly transport;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { type ClientCapabilities, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
|
|
1
|
+
import { type ClientCapabilities, type McpServer, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
|
|
2
2
|
/** Single global mode id, sufficient for v1. */
|
|
3
3
|
export declare const DEFAULT_MODE_ID = "code";
|
|
4
4
|
export declare const DEFAULT_MAX_SESSIONS = 64;
|
|
5
5
|
export declare const DEFAULT_MODES: readonly SessionMode[];
|
|
6
|
+
/**
|
|
7
|
+
* Validate the `mcpServers` array a client sends with `session/new`,
|
|
8
|
+
* `session/load` or `session/fork`.
|
|
9
|
+
*
|
|
10
|
+
* Malformed entries are dropped rather than rejecting the whole request: an
|
|
11
|
+
* editor that ships one bad server config should still get a working session.
|
|
12
|
+
* A dropped entry is reported through `onSkipped` so the caller can tell the
|
|
13
|
+
* client instead of silently discarding what it asked for — silent discard is
|
|
14
|
+
* exactly how this array came to be ignored in the first place.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseMcpServers(raw: unknown, onSkipped?: (reason: string) => void): McpServer[];
|
|
6
17
|
export declare function resolveSessionCwd(requested: string): Promise<string | null>;
|
|
7
18
|
export declare function errorToJsonRpc(err: unknown): {
|
|
8
19
|
code: number;
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `{stopReason: 'cancelled'}`.
|
|
40
40
|
*/
|
|
41
41
|
import type { Agent, AgentInput } from '@wrongstack/core/agent';
|
|
42
|
-
import type { ContentBlock, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
|
|
42
|
+
import type { ContentBlock, McpServer, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
|
|
43
43
|
import type { RunTurn, RunTurnApi, RunTurnResult } from './protocol-handler.js';
|
|
44
44
|
export interface ACPServerAgentTurnOptions {
|
|
45
45
|
/**
|
|
@@ -53,8 +53,12 @@ export interface ACPServerAgentTurnOptions {
|
|
|
53
53
|
* advertises those capabilities. A factory that wires it builds a
|
|
54
54
|
* client-backed permission policy and ACP-backed fs/terminal tools
|
|
55
55
|
* instead of silently auto-approving against the local disk.
|
|
56
|
+
*
|
|
57
|
+
* `mcpServers` is the client's per-session MCP server list from
|
|
58
|
+
* `session/new`. The factory owns the session's tool registry, so it is the
|
|
59
|
+
* only place those servers can be connected and their tools registered.
|
|
56
60
|
*/
|
|
57
|
-
agentFor: (sessionId: string, cwd: string, api?: RunTurnApi) => Promise<Agent> | Agent;
|
|
61
|
+
agentFor: (sessionId: string, cwd: string, api?: RunTurnApi, mcpServers?: readonly McpServer[]) => Promise<Agent> | Agent;
|
|
58
62
|
/**
|
|
59
63
|
* Hard wall-clock cap for one turn. The agent's own provider
|
|
60
64
|
* timeout is layered under this; this cap is a safety belt.
|
package/dist/agent.js
CHANGED
|
@@ -46,6 +46,64 @@ var DEFAULT_MODES = [
|
|
|
46
46
|
description: "Default agent mode for code-generation tasks."
|
|
47
47
|
}
|
|
48
48
|
];
|
|
49
|
+
function parseMcpServers(raw, onSkipped) {
|
|
50
|
+
if (!Array.isArray(raw)) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const entry of raw) {
|
|
53
|
+
if (typeof entry !== "object" || entry === null) {
|
|
54
|
+
onSkipped?.("entry is not an object");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const e = entry;
|
|
58
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
59
|
+
if (name === "") {
|
|
60
|
+
onSkipped?.("entry has no name");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
64
|
+
if (type === "http" || type === "sse") {
|
|
65
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
66
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const headers = parseNameValuePairs(e.headers);
|
|
70
|
+
const url = e.url;
|
|
71
|
+
out.push(
|
|
72
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (type !== "stdio") {
|
|
77
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
81
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
85
|
+
const env = parseNameValuePairs(e.env);
|
|
86
|
+
out.push({
|
|
87
|
+
name,
|
|
88
|
+
command: e.command,
|
|
89
|
+
...args && args.length > 0 ? { args } : {},
|
|
90
|
+
...env ? { env } : {}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function parseNameValuePairs(raw) {
|
|
96
|
+
if (!Array.isArray(raw)) return void 0;
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const pair of raw) {
|
|
99
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
100
|
+
const p = pair;
|
|
101
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
102
|
+
out.push({ name: p.name, value: p.value });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out.length > 0 ? out : void 0;
|
|
106
|
+
}
|
|
49
107
|
async function resolveSessionCwd(requested) {
|
|
50
108
|
if (!path.isAbsolute(requested)) return null;
|
|
51
109
|
const resolved = path.resolve(requested);
|
|
@@ -128,9 +186,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
128
186
|
audio: false,
|
|
129
187
|
embeddedContext: true
|
|
130
188
|
},
|
|
189
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
190
|
+
// and cannot be declined; http and sse are declared here because the
|
|
191
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
192
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
193
|
+
// wiring existed the array was destructured and thrown away at every
|
|
194
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
195
|
+
// flip these back to false if that connection path is ever removed.
|
|
131
196
|
mcpCapabilities: {
|
|
132
|
-
http:
|
|
133
|
-
sse:
|
|
197
|
+
http: true,
|
|
198
|
+
sse: true
|
|
134
199
|
},
|
|
135
200
|
sessionCapabilities: {
|
|
136
201
|
close: {},
|
|
@@ -170,6 +235,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
170
235
|
}
|
|
171
236
|
cwd = resolved;
|
|
172
237
|
}
|
|
238
|
+
const skipped = [];
|
|
239
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
173
240
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
174
241
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
175
242
|
const state = {
|
|
@@ -178,7 +245,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
178
245
|
abort: new AbortController(),
|
|
179
246
|
modeId: DEFAULT_MODE_ID,
|
|
180
247
|
createdAt: now,
|
|
181
|
-
updatedAt: now
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
182
250
|
};
|
|
183
251
|
ctx.sessions.set(sessionId, state);
|
|
184
252
|
ctx.onSessionNew(state);
|
|
@@ -199,6 +267,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
199
267
|
}
|
|
200
268
|
});
|
|
201
269
|
}
|
|
270
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
202
271
|
await ctx.sendResult(id, {
|
|
203
272
|
sessionId,
|
|
204
273
|
modes: ctx.modes,
|
|
@@ -210,6 +279,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
210
279
|
const p = params ?? {};
|
|
211
280
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
212
281
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
282
|
+
const loadSkipped = [];
|
|
283
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
213
284
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
214
285
|
if (!existing && sessionId && ctx.store) {
|
|
215
286
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -231,7 +302,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
231
302
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
232
303
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
233
304
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
234
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
305
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
306
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
235
307
|
};
|
|
236
308
|
ctx.sessions.set(sessionId, restored);
|
|
237
309
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -242,6 +314,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
242
314
|
sessionId,
|
|
243
315
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
244
316
|
});
|
|
317
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
245
318
|
await ctx.sendResult(id, {
|
|
246
319
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
247
320
|
});
|
|
@@ -250,6 +323,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
250
323
|
}
|
|
251
324
|
if (existing) {
|
|
252
325
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
326
|
+
if (loadMcpServers.length > 0) {
|
|
327
|
+
existing.mcpServers = loadMcpServers;
|
|
328
|
+
}
|
|
253
329
|
const replay = ctx.replayFor?.(sessionId);
|
|
254
330
|
if (replay) {
|
|
255
331
|
for (const update of replay) {
|
|
@@ -270,6 +346,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
270
346
|
modeId: existing.modeId
|
|
271
347
|
}
|
|
272
348
|
});
|
|
349
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
273
350
|
await ctx.sendResult(id, {
|
|
274
351
|
initialMode: {
|
|
275
352
|
currentModeId: existing.modeId,
|
|
@@ -302,6 +379,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
302
379
|
}
|
|
303
380
|
forkCwd = resolved;
|
|
304
381
|
}
|
|
382
|
+
const forkSkipped = [];
|
|
383
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
384
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
305
385
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
306
386
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
307
387
|
const forked = {
|
|
@@ -311,7 +391,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
311
391
|
modeId: source.modeId,
|
|
312
392
|
createdAt: now,
|
|
313
393
|
updatedAt: now,
|
|
314
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
394
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
395
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
315
396
|
};
|
|
316
397
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
317
398
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -325,6 +406,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
325
406
|
sessionId,
|
|
326
407
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
327
408
|
});
|
|
409
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
328
410
|
await ctx.sendResult(id, {
|
|
329
411
|
sessionId,
|
|
330
412
|
modes: ctx.modes,
|
|
@@ -364,7 +446,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
364
446
|
};
|
|
365
447
|
try {
|
|
366
448
|
result = await ctx.runTurn(
|
|
367
|
-
{
|
|
449
|
+
{
|
|
450
|
+
sessionId,
|
|
451
|
+
prompt: p.prompt,
|
|
452
|
+
signal: turnSignal.signal,
|
|
453
|
+
cwd: session.cwd,
|
|
454
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
455
|
+
},
|
|
368
456
|
emit,
|
|
369
457
|
api
|
|
370
458
|
);
|
|
@@ -422,6 +510,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
422
510
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
423
511
|
return false;
|
|
424
512
|
}
|
|
513
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
514
|
+
if (skipped.length === 0) return;
|
|
515
|
+
try {
|
|
516
|
+
await ctx.sendNotification({
|
|
517
|
+
sessionId,
|
|
518
|
+
update: {
|
|
519
|
+
sessionUpdate: "agent_message_chunk",
|
|
520
|
+
content: {
|
|
521
|
+
type: "text",
|
|
522
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
425
529
|
|
|
426
530
|
// src/agent/protocol-handler.ts
|
|
427
531
|
var ACPProtocolHandler = class {
|
|
@@ -620,7 +724,7 @@ var ACPProtocolHandler = class {
|
|
|
620
724
|
return false;
|
|
621
725
|
}
|
|
622
726
|
async handleAuthenticate(id, _params) {
|
|
623
|
-
await this.sendResult(id, {
|
|
727
|
+
await this.sendResult(id, {});
|
|
624
728
|
return false;
|
|
625
729
|
}
|
|
626
730
|
async handleLogout(id, _params) {
|
|
@@ -633,6 +737,10 @@ var ACPProtocolHandler = class {
|
|
|
633
737
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
634
738
|
if (existing) {
|
|
635
739
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
740
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
741
|
+
if (resumeServers.length > 0) {
|
|
742
|
+
existing.mcpServers = resumeServers;
|
|
743
|
+
}
|
|
636
744
|
await this.sendResult(id, {
|
|
637
745
|
initialMode: {
|
|
638
746
|
currentModeId: existing.modeId,
|
|
@@ -775,7 +883,12 @@ function makeACPServerAgentTurn(opts) {
|
|
|
775
883
|
const turn = async (input, emit, api) => {
|
|
776
884
|
let agent = agents.get(input.sessionId);
|
|
777
885
|
if (!agent) {
|
|
778
|
-
agent = await opts.agentFor(
|
|
886
|
+
agent = await opts.agentFor(
|
|
887
|
+
input.sessionId,
|
|
888
|
+
input.cwd ?? process.cwd(),
|
|
889
|
+
api,
|
|
890
|
+
input.mcpServers
|
|
891
|
+
);
|
|
779
892
|
agents.set(input.sessionId, agent);
|
|
780
893
|
if (pendingSeed.has(input.sessionId)) {
|
|
781
894
|
pendingSeed.delete(input.sessionId);
|
|
@@ -1618,11 +1731,11 @@ function toolToPriority(tool) {
|
|
|
1618
1731
|
}
|
|
1619
1732
|
|
|
1620
1733
|
// src/agent/wrongstack-acp-agent.ts
|
|
1621
|
-
import { timingSafeEqual } from "node:crypto";
|
|
1622
1734
|
import { createServer } from "node:http";
|
|
1623
1735
|
import { isIP } from "node:net";
|
|
1624
1736
|
import { fileURLToPath } from "node:url";
|
|
1625
1737
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1738
|
+
import { timingSafeTokenEqual } from "@wrongstack/primitives";
|
|
1626
1739
|
var LISTEN_RETRY_LIMIT = 5;
|
|
1627
1740
|
var LISTEN_RETRY_BASE_MS = 25;
|
|
1628
1741
|
var WrongStackACPServer = class {
|
|
@@ -1856,13 +1969,6 @@ var WrongStackACPServer = class {
|
|
|
1856
1969
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1857
1970
|
return { stopReason: "end_turn" };
|
|
1858
1971
|
};
|
|
1859
|
-
function timingSafeTokenEqual(supplied, expected) {
|
|
1860
|
-
if (!supplied || !expected) return false;
|
|
1861
|
-
const a = Buffer.from(supplied);
|
|
1862
|
-
const b = Buffer.from(expected);
|
|
1863
|
-
if (a.length !== b.length) return false;
|
|
1864
|
-
return timingSafeEqual(a, b);
|
|
1865
|
-
}
|
|
1866
1972
|
function isLoopbackPeer(req) {
|
|
1867
1973
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
1868
1974
|
return address !== void 0 && isLoopbackHost(address);
|
|
@@ -10,5 +10,11 @@ interface JsonRpcError {
|
|
|
10
10
|
data?: unknown;
|
|
11
11
|
}
|
|
12
12
|
export declare function isJsonRpcError(v: unknown): v is JsonRpcError;
|
|
13
|
+
/**
|
|
14
|
+
* True when an agent refused `session/new` (or similar) because the user
|
|
15
|
+
* must authenticate first. Official registry agents often return this
|
|
16
|
+
* (~19/31 in the 2026-09 protocol matrix) instead of creating a session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isAuthRequiredError(err: unknown): boolean;
|
|
13
19
|
export {};
|
|
14
20
|
//# sourceMappingURL=acp-session-errors.d.ts.map
|
|
@@ -107,6 +107,18 @@ export declare class ACPSession {
|
|
|
107
107
|
close(): Promise<void>;
|
|
108
108
|
private allocId;
|
|
109
109
|
private sendRequest;
|
|
110
|
+
/**
|
|
111
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
112
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
113
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
114
|
+
*/
|
|
115
|
+
private createSessionWithAuth;
|
|
116
|
+
/**
|
|
117
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
118
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
119
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
120
|
+
*/
|
|
121
|
+
private ensureAuthenticated;
|
|
110
122
|
private sendResult;
|
|
111
123
|
private sendErrorResponse;
|
|
112
124
|
private responseSender;
|
package/dist/client.js
CHANGED
|
@@ -254,6 +254,21 @@ function verbatimOptions(invocation) {
|
|
|
254
254
|
// src/types/acp-v1.ts
|
|
255
255
|
var ACP_PROTOCOL_VERSION = 1;
|
|
256
256
|
|
|
257
|
+
// src/version.ts
|
|
258
|
+
import { createRequire } from "node:module";
|
|
259
|
+
var require2 = createRequire(import.meta.url);
|
|
260
|
+
function readPackageVersion(load = () => require2("../package.json")) {
|
|
261
|
+
try {
|
|
262
|
+
const packageJson = load();
|
|
263
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
264
|
+
return packageJson.version;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return "dev";
|
|
269
|
+
}
|
|
270
|
+
var ACP_PACKAGE_VERSION = readPackageVersion();
|
|
271
|
+
|
|
257
272
|
// src/client/acp-message-routing.ts
|
|
258
273
|
function isBestEffortAckMethod(method) {
|
|
259
274
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
@@ -728,6 +743,20 @@ var ACPSessionError = class extends Error {
|
|
|
728
743
|
function isJsonRpcError(v) {
|
|
729
744
|
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
730
745
|
}
|
|
746
|
+
function isAuthRequiredError(err) {
|
|
747
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
748
|
+
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
749
|
+
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
750
|
+
if (!cause || typeof cause !== "object") return false;
|
|
751
|
+
const data = cause.data;
|
|
752
|
+
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
753
|
+
if (data && typeof data === "object") {
|
|
754
|
+
const d = data;
|
|
755
|
+
if (d.authRequired === true) return true;
|
|
756
|
+
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
731
760
|
|
|
732
761
|
// src/client/acp-session-ops.ts
|
|
733
762
|
function filterMcpServers(agentCapabilities, servers) {
|
|
@@ -1772,7 +1801,7 @@ var ACPSession = class _ACPSession {
|
|
|
1772
1801
|
fs: { readTextFile: true, writeTextFile: true },
|
|
1773
1802
|
terminal: true
|
|
1774
1803
|
},
|
|
1775
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version:
|
|
1804
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
|
|
1776
1805
|
});
|
|
1777
1806
|
if (isJsonRpcError(result)) {
|
|
1778
1807
|
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
@@ -1804,12 +1833,13 @@ var ACPSession = class _ACPSession {
|
|
|
1804
1833
|
if (this.state === "closed") {
|
|
1805
1834
|
throw new ACPSessionError("closed", "session is closed");
|
|
1806
1835
|
}
|
|
1807
|
-
if (this.state !== "ready") {
|
|
1836
|
+
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
1808
1837
|
throw new ACPSessionError(
|
|
1809
1838
|
"protocol_error",
|
|
1810
1839
|
`authenticate called in state=${this.state} (expected 'ready')`
|
|
1811
1840
|
);
|
|
1812
1841
|
}
|
|
1842
|
+
if (this.state === "authenticated") return;
|
|
1813
1843
|
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
1814
1844
|
throw new ACPSessionError(
|
|
1815
1845
|
"auth_failed",
|
|
@@ -1912,7 +1942,7 @@ var ACPSession = class _ACPSession {
|
|
|
1912
1942
|
return emptyRunResult("cancelled");
|
|
1913
1943
|
}
|
|
1914
1944
|
if (!this.sessionId) {
|
|
1915
|
-
this.sessionId = await
|
|
1945
|
+
this.sessionId = await this.createSessionWithAuth();
|
|
1916
1946
|
}
|
|
1917
1947
|
if (signal.aborted) {
|
|
1918
1948
|
return emptyRunResult("cancelled");
|
|
@@ -2048,6 +2078,54 @@ var ACPSession = class _ACPSession {
|
|
|
2048
2078
|
});
|
|
2049
2079
|
});
|
|
2050
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
2083
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
2084
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
2085
|
+
*/
|
|
2086
|
+
async createSessionWithAuth() {
|
|
2087
|
+
try {
|
|
2088
|
+
return await executeCreateSession(this.opContext());
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
2091
|
+
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
2092
|
+
"session_create_failed",
|
|
2093
|
+
err instanceof Error ? err.message : String(err),
|
|
2094
|
+
err
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
await this.ensureAuthenticated();
|
|
2098
|
+
return executeCreateSession(this.opContext());
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
2103
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
2104
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
2105
|
+
*/
|
|
2106
|
+
async ensureAuthenticated() {
|
|
2107
|
+
if (this.state === "authenticated") return;
|
|
2108
|
+
if (this.authMethods.length === 0) {
|
|
2109
|
+
throw new ACPSessionError(
|
|
2110
|
+
"auth_failed",
|
|
2111
|
+
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
const inProcess = this.authMethods.find(
|
|
2115
|
+
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
2116
|
+
);
|
|
2117
|
+
if (inProcess) {
|
|
2118
|
+
await this.authenticate(inProcess.id);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
2122
|
+
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
2123
|
+
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
2124
|
+
throw new ACPSessionError(
|
|
2125
|
+
"auth_failed",
|
|
2126
|
+
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2051
2129
|
sendResult(id, result) {
|
|
2052
2130
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
2053
2131
|
}
|
|
@@ -2076,7 +2154,11 @@ var ACPSession = class _ACPSession {
|
|
|
2076
2154
|
clearTimeout(pending.timeoutHandle);
|
|
2077
2155
|
this.pending.delete(msg.id);
|
|
2078
2156
|
if (msg.error !== void 0) {
|
|
2079
|
-
|
|
2157
|
+
const method = pending.method;
|
|
2158
|
+
const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
|
|
2159
|
+
pending.reject(
|
|
2160
|
+
new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
|
|
2161
|
+
);
|
|
2080
2162
|
} else {
|
|
2081
2163
|
pending.resolve(msg.result);
|
|
2082
2164
|
}
|
|
@@ -2198,6 +2280,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2198
2280
|
} catch {
|
|
2199
2281
|
}
|
|
2200
2282
|
options.onProgress?.(event);
|
|
2283
|
+
try {
|
|
2284
|
+
options.publishLive?.(ctx, task, event);
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2201
2287
|
};
|
|
2202
2288
|
try {
|
|
2203
2289
|
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|