@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/agent/wrongstack-acp-agent.ts
|
|
2
|
-
import { timingSafeEqual } from "node:crypto";
|
|
3
2
|
import { createServer } from "node:http";
|
|
4
3
|
import { isIP } from "node:net";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
6
|
+
import { timingSafeTokenEqual } from "@wrongstack/primitives";
|
|
7
7
|
|
|
8
8
|
// src/agent/protocol-handler.ts
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
@@ -53,6 +53,64 @@ var DEFAULT_MODES = [
|
|
|
53
53
|
description: "Default agent mode for code-generation tasks."
|
|
54
54
|
}
|
|
55
55
|
];
|
|
56
|
+
function parseMcpServers(raw, onSkipped) {
|
|
57
|
+
if (!Array.isArray(raw)) return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const entry of raw) {
|
|
60
|
+
if (typeof entry !== "object" || entry === null) {
|
|
61
|
+
onSkipped?.("entry is not an object");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const e = entry;
|
|
65
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
66
|
+
if (name === "") {
|
|
67
|
+
onSkipped?.("entry has no name");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
71
|
+
if (type === "http" || type === "sse") {
|
|
72
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
73
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const headers = parseNameValuePairs(e.headers);
|
|
77
|
+
const url = e.url;
|
|
78
|
+
out.push(
|
|
79
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (type !== "stdio") {
|
|
84
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
88
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
92
|
+
const env = parseNameValuePairs(e.env);
|
|
93
|
+
out.push({
|
|
94
|
+
name,
|
|
95
|
+
command: e.command,
|
|
96
|
+
...args && args.length > 0 ? { args } : {},
|
|
97
|
+
...env ? { env } : {}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function parseNameValuePairs(raw) {
|
|
103
|
+
if (!Array.isArray(raw)) return void 0;
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const pair of raw) {
|
|
106
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
107
|
+
const p = pair;
|
|
108
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
109
|
+
out.push({ name: p.name, value: p.value });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out.length > 0 ? out : void 0;
|
|
113
|
+
}
|
|
56
114
|
async function resolveSessionCwd(requested) {
|
|
57
115
|
if (!path.isAbsolute(requested)) return null;
|
|
58
116
|
const resolved = path.resolve(requested);
|
|
@@ -135,9 +193,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
135
193
|
audio: false,
|
|
136
194
|
embeddedContext: true
|
|
137
195
|
},
|
|
196
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
197
|
+
// and cannot be declined; http and sse are declared here because the
|
|
198
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
199
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
200
|
+
// wiring existed the array was destructured and thrown away at every
|
|
201
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
202
|
+
// flip these back to false if that connection path is ever removed.
|
|
138
203
|
mcpCapabilities: {
|
|
139
|
-
http:
|
|
140
|
-
sse:
|
|
204
|
+
http: true,
|
|
205
|
+
sse: true
|
|
141
206
|
},
|
|
142
207
|
sessionCapabilities: {
|
|
143
208
|
close: {},
|
|
@@ -177,6 +242,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
177
242
|
}
|
|
178
243
|
cwd = resolved;
|
|
179
244
|
}
|
|
245
|
+
const skipped = [];
|
|
246
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
180
247
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
181
248
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
182
249
|
const state = {
|
|
@@ -185,7 +252,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
185
252
|
abort: new AbortController(),
|
|
186
253
|
modeId: DEFAULT_MODE_ID,
|
|
187
254
|
createdAt: now,
|
|
188
|
-
updatedAt: now
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
189
257
|
};
|
|
190
258
|
ctx.sessions.set(sessionId, state);
|
|
191
259
|
ctx.onSessionNew(state);
|
|
@@ -206,6 +274,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
206
274
|
}
|
|
207
275
|
});
|
|
208
276
|
}
|
|
277
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
209
278
|
await ctx.sendResult(id, {
|
|
210
279
|
sessionId,
|
|
211
280
|
modes: ctx.modes,
|
|
@@ -217,6 +286,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
217
286
|
const p = params ?? {};
|
|
218
287
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
219
288
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
289
|
+
const loadSkipped = [];
|
|
290
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
220
291
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
221
292
|
if (!existing && sessionId && ctx.store) {
|
|
222
293
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -238,7 +309,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
238
309
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
239
310
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
311
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
312
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
313
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
242
314
|
};
|
|
243
315
|
ctx.sessions.set(sessionId, restored);
|
|
244
316
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -249,6 +321,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
249
321
|
sessionId,
|
|
250
322
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
251
323
|
});
|
|
324
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
252
325
|
await ctx.sendResult(id, {
|
|
253
326
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
254
327
|
});
|
|
@@ -257,6 +330,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
257
330
|
}
|
|
258
331
|
if (existing) {
|
|
259
332
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
333
|
+
if (loadMcpServers.length > 0) {
|
|
334
|
+
existing.mcpServers = loadMcpServers;
|
|
335
|
+
}
|
|
260
336
|
const replay = ctx.replayFor?.(sessionId);
|
|
261
337
|
if (replay) {
|
|
262
338
|
for (const update of replay) {
|
|
@@ -277,6 +353,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
277
353
|
modeId: existing.modeId
|
|
278
354
|
}
|
|
279
355
|
});
|
|
356
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
280
357
|
await ctx.sendResult(id, {
|
|
281
358
|
initialMode: {
|
|
282
359
|
currentModeId: existing.modeId,
|
|
@@ -309,6 +386,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
309
386
|
}
|
|
310
387
|
forkCwd = resolved;
|
|
311
388
|
}
|
|
389
|
+
const forkSkipped = [];
|
|
390
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
391
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
312
392
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
313
393
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
314
394
|
const forked = {
|
|
@@ -318,7 +398,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
318
398
|
modeId: source.modeId,
|
|
319
399
|
createdAt: now,
|
|
320
400
|
updatedAt: now,
|
|
321
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
401
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
402
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
322
403
|
};
|
|
323
404
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
324
405
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -332,6 +413,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
332
413
|
sessionId,
|
|
333
414
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
334
415
|
});
|
|
416
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
335
417
|
await ctx.sendResult(id, {
|
|
336
418
|
sessionId,
|
|
337
419
|
modes: ctx.modes,
|
|
@@ -371,7 +453,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
371
453
|
};
|
|
372
454
|
try {
|
|
373
455
|
result = await ctx.runTurn(
|
|
374
|
-
{
|
|
456
|
+
{
|
|
457
|
+
sessionId,
|
|
458
|
+
prompt: p.prompt,
|
|
459
|
+
signal: turnSignal.signal,
|
|
460
|
+
cwd: session.cwd,
|
|
461
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
462
|
+
},
|
|
375
463
|
emit,
|
|
376
464
|
api
|
|
377
465
|
);
|
|
@@ -429,6 +517,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
429
517
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
430
518
|
return false;
|
|
431
519
|
}
|
|
520
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
521
|
+
if (skipped.length === 0) return;
|
|
522
|
+
try {
|
|
523
|
+
await ctx.sendNotification({
|
|
524
|
+
sessionId,
|
|
525
|
+
update: {
|
|
526
|
+
sessionUpdate: "agent_message_chunk",
|
|
527
|
+
content: {
|
|
528
|
+
type: "text",
|
|
529
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
432
536
|
|
|
433
537
|
// src/agent/protocol-handler.ts
|
|
434
538
|
var ACPProtocolHandler = class {
|
|
@@ -627,7 +731,7 @@ var ACPProtocolHandler = class {
|
|
|
627
731
|
return false;
|
|
628
732
|
}
|
|
629
733
|
async handleAuthenticate(id, _params) {
|
|
630
|
-
await this.sendResult(id, {
|
|
734
|
+
await this.sendResult(id, {});
|
|
631
735
|
return false;
|
|
632
736
|
}
|
|
633
737
|
async handleLogout(id, _params) {
|
|
@@ -640,6 +744,10 @@ var ACPProtocolHandler = class {
|
|
|
640
744
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
641
745
|
if (existing) {
|
|
642
746
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
747
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
748
|
+
if (resumeServers.length > 0) {
|
|
749
|
+
existing.mcpServers = resumeServers;
|
|
750
|
+
}
|
|
643
751
|
await this.sendResult(id, {
|
|
644
752
|
initialMode: {
|
|
645
753
|
currentModeId: existing.modeId,
|
|
@@ -1175,13 +1283,6 @@ var WrongStackACPServer = class {
|
|
|
1175
1283
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1176
1284
|
return { stopReason: "end_turn" };
|
|
1177
1285
|
};
|
|
1178
|
-
function timingSafeTokenEqual(supplied, expected) {
|
|
1179
|
-
if (!supplied || !expected) return false;
|
|
1180
|
-
const a = Buffer.from(supplied);
|
|
1181
|
-
const b = Buffer.from(expected);
|
|
1182
|
-
if (a.length !== b.length) return false;
|
|
1183
|
-
return timingSafeEqual(a, b);
|
|
1184
|
-
}
|
|
1185
1286
|
function isLoopbackPeer(req) {
|
|
1186
1287
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
1187
1288
|
return address !== void 0 && isLoopbackHost(address);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/acp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.4.0",
|
|
55
|
-
"@wrongstack/core": "1.0.
|
|
55
|
+
"@wrongstack/core": "1.0.5",
|
|
56
|
+
"@wrongstack/primitives": "1.0.5"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/node": "^26.2.0",
|