@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.
- 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.js +70 -44
- package/dist/index.js +433 -253
- 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
|
@@ -4,6 +4,11 @@ import { isIP } from "node:net";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
6
6
|
|
|
7
|
+
// src/agent/protocol-handler.ts
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import * as fsp from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
7
12
|
// src/types/acp-v1.ts
|
|
8
13
|
var ACP_PROTOCOL_VERSION = 1;
|
|
9
14
|
|
|
@@ -93,12 +98,12 @@ var ACPProtocolHandler = class {
|
|
|
93
98
|
*/
|
|
94
99
|
request(method, params, timeoutMs = 6e4) {
|
|
95
100
|
const id = `srv_${this.nextOutId++}`;
|
|
96
|
-
return new Promise((
|
|
101
|
+
return new Promise((resolve2, reject) => {
|
|
97
102
|
const timer = setTimeout(() => {
|
|
98
103
|
this.pendingOut.delete(id);
|
|
99
104
|
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
100
105
|
}, timeoutMs);
|
|
101
|
-
this.pendingOut.set(id, { resolve, reject, timer });
|
|
106
|
+
this.pendingOut.set(id, { resolve: resolve2, reject, timer });
|
|
102
107
|
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
103
108
|
clearTimeout(timer);
|
|
104
109
|
this.pendingOut.delete(id);
|
|
@@ -215,62 +220,68 @@ var ACPProtocolHandler = class {
|
|
|
215
220
|
this.clientCapabilities = p.clientCapabilities;
|
|
216
221
|
}
|
|
217
222
|
this.initialized = true;
|
|
218
|
-
await this.transport.send(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
223
|
+
await this.transport.send(
|
|
224
|
+
toWire({
|
|
225
|
+
jsonrpc: "2.0",
|
|
226
|
+
id,
|
|
227
|
+
result: {
|
|
228
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
229
|
+
agentCapabilities: {
|
|
230
|
+
loadSession: true,
|
|
231
|
+
promptCapabilities: {
|
|
232
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
233
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
234
|
+
// model can see them is the configured provider's concern.
|
|
235
|
+
image: true,
|
|
236
|
+
audio: false,
|
|
237
|
+
embeddedContext: true
|
|
238
|
+
},
|
|
239
|
+
mcpCapabilities: {
|
|
240
|
+
http: false,
|
|
241
|
+
sse: false
|
|
242
|
+
},
|
|
243
|
+
sessionCapabilities: {
|
|
244
|
+
close: {},
|
|
245
|
+
list: {},
|
|
246
|
+
delete: {},
|
|
247
|
+
resume: {},
|
|
248
|
+
fork: {}
|
|
249
|
+
},
|
|
250
|
+
auth: {
|
|
251
|
+
logout: {}
|
|
252
|
+
}
|
|
236
253
|
},
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
resume: {},
|
|
242
|
-
fork: {}
|
|
254
|
+
agentInfo: {
|
|
255
|
+
name: this.agentName,
|
|
256
|
+
title: "WrongStack",
|
|
257
|
+
version: WRONGSTACK_VERSION
|
|
243
258
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
title: "WrongStack",
|
|
251
|
-
version: WRONGSTACK_VERSION
|
|
252
|
-
},
|
|
253
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
254
|
-
modes: this.modes,
|
|
255
|
-
configOptions: this.configOptions
|
|
256
|
-
}
|
|
257
|
-
}));
|
|
259
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
260
|
+
modes: this.modes,
|
|
261
|
+
configOptions: this.configOptions
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
);
|
|
258
265
|
return false;
|
|
259
266
|
}
|
|
260
267
|
async handleAuthenticate(id, _params) {
|
|
261
|
-
await this.transport.send(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
await this.transport.send(
|
|
269
|
+
toWire({
|
|
270
|
+
jsonrpc: "2.0",
|
|
271
|
+
id,
|
|
272
|
+
result: { outcome: "unauthenticated" }
|
|
273
|
+
})
|
|
274
|
+
);
|
|
266
275
|
return false;
|
|
267
276
|
}
|
|
268
277
|
async handleLogout(id, _params) {
|
|
269
|
-
await this.transport.send(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
await this.transport.send(
|
|
279
|
+
toWire({
|
|
280
|
+
jsonrpc: "2.0",
|
|
281
|
+
id,
|
|
282
|
+
result: {}
|
|
283
|
+
})
|
|
284
|
+
);
|
|
274
285
|
return false;
|
|
275
286
|
}
|
|
276
287
|
async handleSessionNew(id, params) {
|
|
@@ -279,7 +290,15 @@ var ACPProtocolHandler = class {
|
|
|
279
290
|
return false;
|
|
280
291
|
}
|
|
281
292
|
const p = params ?? {};
|
|
282
|
-
|
|
293
|
+
let cwd = this.defaultCwd;
|
|
294
|
+
if (typeof p.cwd === "string") {
|
|
295
|
+
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
296
|
+
if (resolved === null) {
|
|
297
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
cwd = resolved;
|
|
301
|
+
}
|
|
283
302
|
const sessionId = `sess_${this.allocId()}`;
|
|
284
303
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
285
304
|
const state = {
|
|
@@ -309,15 +328,17 @@ var ACPProtocolHandler = class {
|
|
|
309
328
|
}
|
|
310
329
|
});
|
|
311
330
|
}
|
|
312
|
-
await this.transport.send(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
await this.transport.send(
|
|
332
|
+
toWire({
|
|
333
|
+
jsonrpc: "2.0",
|
|
334
|
+
id,
|
|
335
|
+
result: {
|
|
336
|
+
sessionId,
|
|
337
|
+
modes: this.modes,
|
|
338
|
+
configOptions: this.configOptions
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
);
|
|
321
342
|
return false;
|
|
322
343
|
}
|
|
323
344
|
async handleSessionLoad(id, params) {
|
|
@@ -332,9 +353,15 @@ var ACPProtocolHandler = class {
|
|
|
332
353
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
333
354
|
return false;
|
|
334
355
|
}
|
|
356
|
+
if (loadCwd !== void 0 && await this.resolveSessionCwd(loadCwd) === null) {
|
|
357
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const candidateCwd = persisted.cwd ?? loadCwd ?? this.defaultCwd;
|
|
361
|
+
const restoredCwd = await this.resolveSessionCwd(candidateCwd) ?? this.defaultCwd;
|
|
335
362
|
const restored = {
|
|
336
363
|
id: sessionId,
|
|
337
|
-
cwd:
|
|
364
|
+
cwd: restoredCwd,
|
|
338
365
|
abort: new AbortController(),
|
|
339
366
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
340
367
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -350,13 +377,15 @@ var ACPProtocolHandler = class {
|
|
|
350
377
|
sessionId,
|
|
351
378
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
352
379
|
});
|
|
353
|
-
await this.transport.send(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
380
|
+
await this.transport.send(
|
|
381
|
+
toWire({
|
|
382
|
+
jsonrpc: "2.0",
|
|
383
|
+
id,
|
|
384
|
+
result: {
|
|
385
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
);
|
|
360
389
|
return false;
|
|
361
390
|
}
|
|
362
391
|
}
|
|
@@ -382,16 +411,18 @@ var ACPProtocolHandler = class {
|
|
|
382
411
|
modeId: existing.modeId
|
|
383
412
|
}
|
|
384
413
|
});
|
|
385
|
-
await this.transport.send(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
414
|
+
await this.transport.send(
|
|
415
|
+
toWire({
|
|
416
|
+
jsonrpc: "2.0",
|
|
417
|
+
id,
|
|
418
|
+
result: {
|
|
419
|
+
initialMode: {
|
|
420
|
+
currentModeId: existing.modeId,
|
|
421
|
+
availableModes: this.modes
|
|
422
|
+
}
|
|
392
423
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
424
|
+
})
|
|
425
|
+
);
|
|
395
426
|
return false;
|
|
396
427
|
}
|
|
397
428
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -403,16 +434,18 @@ var ACPProtocolHandler = class {
|
|
|
403
434
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
404
435
|
if (existing) {
|
|
405
436
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
406
|
-
await this.transport.send(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
437
|
+
await this.transport.send(
|
|
438
|
+
toWire({
|
|
439
|
+
jsonrpc: "2.0",
|
|
440
|
+
id,
|
|
441
|
+
result: {
|
|
442
|
+
initialMode: {
|
|
443
|
+
currentModeId: existing.modeId,
|
|
444
|
+
availableModes: this.modes
|
|
445
|
+
}
|
|
413
446
|
}
|
|
414
|
-
}
|
|
415
|
-
|
|
447
|
+
})
|
|
448
|
+
);
|
|
416
449
|
return false;
|
|
417
450
|
}
|
|
418
451
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -429,11 +462,13 @@ var ACPProtocolHandler = class {
|
|
|
429
462
|
session.abort.abort();
|
|
430
463
|
this.sessions.delete(sessionId);
|
|
431
464
|
this.disposeSession(sessionId);
|
|
432
|
-
await this.transport.send(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
465
|
+
await this.transport.send(
|
|
466
|
+
toWire({
|
|
467
|
+
jsonrpc: "2.0",
|
|
468
|
+
id,
|
|
469
|
+
result: {}
|
|
470
|
+
})
|
|
471
|
+
);
|
|
437
472
|
return false;
|
|
438
473
|
}
|
|
439
474
|
async handleSessionDelete(id, params) {
|
|
@@ -444,18 +479,22 @@ var ACPProtocolHandler = class {
|
|
|
444
479
|
return false;
|
|
445
480
|
}
|
|
446
481
|
if (!this.sessions.has(sessionId)) {
|
|
447
|
-
await this.transport.send(
|
|
482
|
+
await this.transport.send(
|
|
483
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
484
|
+
);
|
|
448
485
|
return false;
|
|
449
486
|
}
|
|
450
487
|
const session = this.sessions.get(sessionId);
|
|
451
488
|
session.abort.abort();
|
|
452
489
|
this.sessions.delete(sessionId);
|
|
453
490
|
this.disposeSession(sessionId);
|
|
454
|
-
await this.transport.send(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
491
|
+
await this.transport.send(
|
|
492
|
+
toWire({
|
|
493
|
+
jsonrpc: "2.0",
|
|
494
|
+
id,
|
|
495
|
+
result: {}
|
|
496
|
+
})
|
|
497
|
+
);
|
|
459
498
|
return false;
|
|
460
499
|
}
|
|
461
500
|
async handleSessionFork(id, params) {
|
|
@@ -470,11 +509,20 @@ var ACPProtocolHandler = class {
|
|
|
470
509
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
471
510
|
return false;
|
|
472
511
|
}
|
|
512
|
+
let forkCwd = source.cwd;
|
|
513
|
+
if (typeof p.cwd === "string") {
|
|
514
|
+
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
515
|
+
if (resolved === null) {
|
|
516
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
forkCwd = resolved;
|
|
520
|
+
}
|
|
473
521
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
474
522
|
const sessionId = `sess_${this.allocId()}`;
|
|
475
523
|
const forked = {
|
|
476
524
|
id: sessionId,
|
|
477
|
-
cwd:
|
|
525
|
+
cwd: forkCwd,
|
|
478
526
|
abort: new AbortController(),
|
|
479
527
|
modeId: source.modeId,
|
|
480
528
|
createdAt: now,
|
|
@@ -493,38 +541,48 @@ var ACPProtocolHandler = class {
|
|
|
493
541
|
sessionId,
|
|
494
542
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
495
543
|
});
|
|
496
|
-
await this.transport.send(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
544
|
+
await this.transport.send(
|
|
545
|
+
toWire({
|
|
546
|
+
jsonrpc: "2.0",
|
|
547
|
+
id,
|
|
548
|
+
result: {
|
|
549
|
+
sessionId,
|
|
550
|
+
modes: this.modes,
|
|
551
|
+
configOptions: this.configOptions
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
);
|
|
505
555
|
return false;
|
|
506
556
|
}
|
|
507
557
|
async handleProvidersList(id, _params) {
|
|
508
|
-
await this.transport.send(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
558
|
+
await this.transport.send(
|
|
559
|
+
toWire({
|
|
560
|
+
jsonrpc: "2.0",
|
|
561
|
+
id,
|
|
562
|
+
result: {
|
|
563
|
+
providers: [],
|
|
564
|
+
currentProviderId: null
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
);
|
|
516
568
|
return false;
|
|
517
569
|
}
|
|
518
570
|
async handleProvidersSet(id, _params) {
|
|
519
|
-
await this.sendError(
|
|
571
|
+
await this.sendError(
|
|
572
|
+
id,
|
|
573
|
+
-32e3,
|
|
574
|
+
"provider configuration not available through ACP; use wstack auth"
|
|
575
|
+
);
|
|
520
576
|
return false;
|
|
521
577
|
}
|
|
522
578
|
async handleProvidersDisable(id, _params) {
|
|
523
|
-
await this.transport.send(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
579
|
+
await this.transport.send(
|
|
580
|
+
toWire({
|
|
581
|
+
jsonrpc: "2.0",
|
|
582
|
+
id,
|
|
583
|
+
result: {}
|
|
584
|
+
})
|
|
585
|
+
);
|
|
528
586
|
return false;
|
|
529
587
|
}
|
|
530
588
|
async handleMcpMessage(id, _params) {
|
|
@@ -577,7 +635,10 @@ var ACPProtocolHandler = class {
|
|
|
577
635
|
const terminalId = created?.terminalId;
|
|
578
636
|
if (!terminalId) return { output: "", exitCode: null };
|
|
579
637
|
try {
|
|
580
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
638
|
+
const exit = await this.request("terminal/wait_for_exit", {
|
|
639
|
+
sessionId,
|
|
640
|
+
terminalId
|
|
641
|
+
});
|
|
581
642
|
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
582
643
|
return {
|
|
583
644
|
output: String(out?.output ?? ""),
|
|
@@ -607,11 +668,13 @@ var ACPProtocolHandler = class {
|
|
|
607
668
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
608
669
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
670
|
await this.persist(session);
|
|
610
|
-
await this.transport.send(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
671
|
+
await this.transport.send(
|
|
672
|
+
toWire({
|
|
673
|
+
jsonrpc: "2.0",
|
|
674
|
+
id,
|
|
675
|
+
result: { stopReason: result.stopReason }
|
|
676
|
+
})
|
|
677
|
+
);
|
|
615
678
|
return false;
|
|
616
679
|
}
|
|
617
680
|
async handleSetMode(id, params) {
|
|
@@ -652,7 +715,9 @@ var ACPProtocolHandler = class {
|
|
|
652
715
|
configOptions: [...this.configOptions]
|
|
653
716
|
}
|
|
654
717
|
});
|
|
655
|
-
await this.transport.send(
|
|
718
|
+
await this.transport.send(
|
|
719
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
720
|
+
);
|
|
656
721
|
return false;
|
|
657
722
|
}
|
|
658
723
|
async handleSessionList(id) {
|
|
@@ -665,11 +730,13 @@ var ACPProtocolHandler = class {
|
|
|
665
730
|
if (s.title !== void 0) out.title = s.title;
|
|
666
731
|
return out;
|
|
667
732
|
});
|
|
668
|
-
await this.transport.send(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
733
|
+
await this.transport.send(
|
|
734
|
+
toWire({
|
|
735
|
+
jsonrpc: "2.0",
|
|
736
|
+
id,
|
|
737
|
+
result: { sessions }
|
|
738
|
+
})
|
|
739
|
+
);
|
|
673
740
|
return false;
|
|
674
741
|
}
|
|
675
742
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -715,8 +782,54 @@ var ACPProtocolHandler = class {
|
|
|
715
782
|
if (data !== void 0) error.data = data;
|
|
716
783
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
717
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Allocate a session id (WS-015).
|
|
787
|
+
*
|
|
788
|
+
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
789
|
+
* handler has no per-connection ownership: any caller that names a session
|
|
790
|
+
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
791
|
+
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
792
|
+
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
793
|
+
* authorization story for any local process or page that reaches the port.
|
|
794
|
+
*
|
|
795
|
+
* Random ids do not create ownership — they remove the trivial enumeration
|
|
796
|
+
* that made its absence exploitable. Real per-connection ownership is the
|
|
797
|
+
* larger fix and is noted in the WS-015 test file.
|
|
798
|
+
*
|
|
799
|
+
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
800
|
+
* uniqueness within a process even in the (impossible) event of a UUID
|
|
801
|
+
* collision. The random half is what makes the id unguessable.
|
|
802
|
+
*/
|
|
803
|
+
/**
|
|
804
|
+
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
805
|
+
* usable (WS-015).
|
|
806
|
+
*
|
|
807
|
+
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
808
|
+
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
809
|
+
* the working directory the agent then reads, writes and executes in.
|
|
810
|
+
*
|
|
811
|
+
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
812
|
+
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
813
|
+
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
814
|
+
* break the integration this package exists for. What it enforces is that
|
|
815
|
+
* the directory is absolute and actually exists as a directory: a relative
|
|
816
|
+
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
817
|
+
* running the agent somewhere other than where the client asked is worse
|
|
818
|
+
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
819
|
+
* on top of this, not in place of it.
|
|
820
|
+
*/
|
|
821
|
+
async resolveSessionCwd(requested) {
|
|
822
|
+
if (!path.isAbsolute(requested)) return null;
|
|
823
|
+
const resolved = path.resolve(requested);
|
|
824
|
+
try {
|
|
825
|
+
const stat2 = await fsp.stat(resolved);
|
|
826
|
+
return stat2.isDirectory() ? resolved : null;
|
|
827
|
+
} catch {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
718
831
|
allocId() {
|
|
719
|
-
return this.nextId
|
|
832
|
+
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
720
833
|
}
|
|
721
834
|
};
|
|
722
835
|
function errorToJsonRpc(err) {
|
|
@@ -740,6 +853,7 @@ import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
|
740
853
|
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
741
854
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
742
855
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
856
|
+
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
743
857
|
function positiveLimit(value, fallback) {
|
|
744
858
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
745
859
|
}
|
|
@@ -749,53 +863,82 @@ var StdioTransport = class {
|
|
|
749
863
|
stderr = process.stderr;
|
|
750
864
|
buffer = "";
|
|
751
865
|
handlers = /* @__PURE__ */ new Set();
|
|
866
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
752
867
|
closed = false;
|
|
753
868
|
resolveRead = null;
|
|
754
869
|
messageQueue = [];
|
|
870
|
+
queuedChars = 0;
|
|
755
871
|
maxFrameChars;
|
|
756
872
|
maxQueuedMessages;
|
|
873
|
+
maxQueuedChars;
|
|
874
|
+
onStdinData = (chunk) => this.onData(chunk);
|
|
875
|
+
onStdinEnd = () => this.handleClose();
|
|
876
|
+
onStdinError = (err) => this.failAll(err);
|
|
757
877
|
constructor(opts = {}) {
|
|
758
878
|
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
759
879
|
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
880
|
+
this.maxQueuedChars = positiveLimit(opts.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
760
881
|
this.stdin.resume();
|
|
761
882
|
this.stdin.setEncoding("utf8");
|
|
762
|
-
this.stdin.on("data",
|
|
763
|
-
this.stdin.on("end",
|
|
764
|
-
this.stdin.on("error",
|
|
883
|
+
this.stdin.on("data", this.onStdinData);
|
|
884
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
885
|
+
this.stdin.on("error", this.onStdinError);
|
|
765
886
|
}
|
|
766
887
|
sendStartupMarker() {
|
|
767
888
|
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
768
889
|
}
|
|
769
890
|
send(msg) {
|
|
770
891
|
if (this.closed) return Promise.resolve();
|
|
771
|
-
return new Promise((
|
|
892
|
+
return new Promise((resolve2) => {
|
|
772
893
|
const line = JSON.stringify(msg) + "\n";
|
|
773
|
-
this.stdout.write(line, "utf8", () =>
|
|
894
|
+
this.stdout.write(line, "utf8", () => resolve2());
|
|
774
895
|
});
|
|
775
896
|
}
|
|
776
897
|
sendRaw(chunk) {
|
|
777
898
|
this.stdout.write(chunk, "utf8");
|
|
778
899
|
}
|
|
779
900
|
read() {
|
|
780
|
-
if (this.messageQueue.length > 0)
|
|
781
|
-
|
|
901
|
+
if (this.messageQueue.length > 0) {
|
|
902
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
903
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
904
|
+
return Promise.resolve(queued.message);
|
|
905
|
+
}
|
|
782
906
|
if (this.closed) return Promise.resolve(null);
|
|
783
|
-
return new Promise((
|
|
784
|
-
this.resolveRead =
|
|
907
|
+
return new Promise((resolve2) => {
|
|
908
|
+
this.resolveRead = resolve2;
|
|
785
909
|
});
|
|
786
910
|
}
|
|
787
911
|
onMessage(handler) {
|
|
788
912
|
this.handlers.add(handler);
|
|
789
913
|
return () => this.handlers.delete(handler);
|
|
790
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
917
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
918
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
919
|
+
* server transport prevents the correlation handler (which always
|
|
920
|
+
* fires on every message but only actually correlates responses)
|
|
921
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
922
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
923
|
+
* the gating logic.
|
|
924
|
+
*/
|
|
925
|
+
onMessageClaim(handler) {
|
|
926
|
+
this.claimHandlers.add(handler);
|
|
927
|
+
return () => this.claimHandlers.delete(handler);
|
|
928
|
+
}
|
|
791
929
|
close() {
|
|
792
930
|
this.closed = true;
|
|
931
|
+
this.stdin.off("data", this.onStdinData);
|
|
932
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
933
|
+
this.stdin.off("error", this.onStdinError);
|
|
793
934
|
this.stdin.pause();
|
|
794
935
|
this.resolveRead?.(null);
|
|
795
936
|
this.resolveRead = null;
|
|
796
937
|
this.buffer = "";
|
|
797
938
|
this.messageQueue.length = 0;
|
|
939
|
+
this.queuedChars = 0;
|
|
798
940
|
this.handlers.clear();
|
|
941
|
+
this.claimHandlers.clear();
|
|
799
942
|
}
|
|
800
943
|
onData(chunk) {
|
|
801
944
|
this.buffer += chunk;
|
|
@@ -822,29 +965,41 @@ var StdioTransport = class {
|
|
|
822
965
|
return;
|
|
823
966
|
}
|
|
824
967
|
try {
|
|
825
|
-
this.dispatch(JSON.parse(raw));
|
|
968
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
826
969
|
} catch (err) {
|
|
827
970
|
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
828
971
|
`, "utf8");
|
|
829
972
|
}
|
|
830
973
|
}
|
|
831
974
|
}
|
|
832
|
-
dispatch(msg) {
|
|
975
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
833
976
|
if (this.resolveRead) {
|
|
834
|
-
const
|
|
977
|
+
const resolve2 = this.resolveRead;
|
|
835
978
|
this.resolveRead = null;
|
|
836
|
-
|
|
979
|
+
resolve2(msg);
|
|
837
980
|
} else {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
981
|
+
let claimed = false;
|
|
982
|
+
for (const handler of this.claimHandlers) {
|
|
983
|
+
try {
|
|
984
|
+
if (handler(msg)) claimed = true;
|
|
985
|
+
} catch (err) {
|
|
986
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
987
|
+
`, "utf8");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (!claimed) {
|
|
991
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
992
|
+
this.stderr.write(
|
|
993
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters
|
|
841
994
|
`,
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
995
|
+
"utf8"
|
|
996
|
+
);
|
|
997
|
+
this.close();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this.messageQueue.push({ message: msg, chars });
|
|
1001
|
+
this.queuedChars += chars;
|
|
846
1002
|
}
|
|
847
|
-
this.messageQueue.push(msg);
|
|
848
1003
|
}
|
|
849
1004
|
for (const handler of this.handlers) {
|
|
850
1005
|
try {
|
|
@@ -1023,12 +1178,12 @@ var WrongStackACPServer = class {
|
|
|
1023
1178
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1024
1179
|
}
|
|
1025
1180
|
});
|
|
1026
|
-
return new Promise((
|
|
1181
|
+
return new Promise((resolve2) => {
|
|
1027
1182
|
this.httpServer.listen(port, host, () => {
|
|
1028
1183
|
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1029
1184
|
`);
|
|
1030
1185
|
this.running = true;
|
|
1031
|
-
|
|
1186
|
+
resolve2();
|
|
1032
1187
|
});
|
|
1033
1188
|
});
|
|
1034
1189
|
}
|