@wrongstack/acp 0.297.0 → 0.298.1
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 +5 -4
- 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
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/agent/protocol-handler.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fsp from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
1
6
|
// src/types/acp-v1.ts
|
|
2
7
|
var ACP_PROTOCOL_VERSION = 1;
|
|
3
8
|
function assertNeverSessionUpdate(x) {
|
|
@@ -92,12 +97,12 @@ var ACPProtocolHandler = class {
|
|
|
92
97
|
*/
|
|
93
98
|
request(method, params, timeoutMs = 6e4) {
|
|
94
99
|
const id = `srv_${this.nextOutId++}`;
|
|
95
|
-
return new Promise((
|
|
100
|
+
return new Promise((resolve4, reject) => {
|
|
96
101
|
const timer = setTimeout(() => {
|
|
97
102
|
this.pendingOut.delete(id);
|
|
98
103
|
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
99
104
|
}, timeoutMs);
|
|
100
|
-
this.pendingOut.set(id, { resolve:
|
|
105
|
+
this.pendingOut.set(id, { resolve: resolve4, reject, timer });
|
|
101
106
|
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
102
107
|
clearTimeout(timer);
|
|
103
108
|
this.pendingOut.delete(id);
|
|
@@ -214,62 +219,68 @@ var ACPProtocolHandler = class {
|
|
|
214
219
|
this.clientCapabilities = p.clientCapabilities;
|
|
215
220
|
}
|
|
216
221
|
this.initialized = true;
|
|
217
|
-
await this.transport.send(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
222
|
+
await this.transport.send(
|
|
223
|
+
toWire({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id,
|
|
226
|
+
result: {
|
|
227
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
228
|
+
agentCapabilities: {
|
|
229
|
+
loadSession: true,
|
|
230
|
+
promptCapabilities: {
|
|
231
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
232
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
233
|
+
// model can see them is the configured provider's concern.
|
|
234
|
+
image: true,
|
|
235
|
+
audio: false,
|
|
236
|
+
embeddedContext: true
|
|
237
|
+
},
|
|
238
|
+
mcpCapabilities: {
|
|
239
|
+
http: false,
|
|
240
|
+
sse: false
|
|
241
|
+
},
|
|
242
|
+
sessionCapabilities: {
|
|
243
|
+
close: {},
|
|
244
|
+
list: {},
|
|
245
|
+
delete: {},
|
|
246
|
+
resume: {},
|
|
247
|
+
fork: {}
|
|
248
|
+
},
|
|
249
|
+
auth: {
|
|
250
|
+
logout: {}
|
|
251
|
+
}
|
|
235
252
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
resume: {},
|
|
241
|
-
fork: {}
|
|
253
|
+
agentInfo: {
|
|
254
|
+
name: this.agentName,
|
|
255
|
+
title: "WrongStack",
|
|
256
|
+
version: WRONGSTACK_VERSION
|
|
242
257
|
},
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
title: "WrongStack",
|
|
250
|
-
version: WRONGSTACK_VERSION
|
|
251
|
-
},
|
|
252
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
253
|
-
modes: this.modes,
|
|
254
|
-
configOptions: this.configOptions
|
|
255
|
-
}
|
|
256
|
-
}));
|
|
258
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
259
|
+
modes: this.modes,
|
|
260
|
+
configOptions: this.configOptions
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
);
|
|
257
264
|
return false;
|
|
258
265
|
}
|
|
259
266
|
async handleAuthenticate(id, _params) {
|
|
260
|
-
await this.transport.send(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
await this.transport.send(
|
|
268
|
+
toWire({
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
result: { outcome: "unauthenticated" }
|
|
272
|
+
})
|
|
273
|
+
);
|
|
265
274
|
return false;
|
|
266
275
|
}
|
|
267
276
|
async handleLogout(id, _params) {
|
|
268
|
-
await this.transport.send(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
await this.transport.send(
|
|
278
|
+
toWire({
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
id,
|
|
281
|
+
result: {}
|
|
282
|
+
})
|
|
283
|
+
);
|
|
273
284
|
return false;
|
|
274
285
|
}
|
|
275
286
|
async handleSessionNew(id, params) {
|
|
@@ -278,7 +289,15 @@ var ACPProtocolHandler = class {
|
|
|
278
289
|
return false;
|
|
279
290
|
}
|
|
280
291
|
const p = params ?? {};
|
|
281
|
-
|
|
292
|
+
let cwd = this.defaultCwd;
|
|
293
|
+
if (typeof p.cwd === "string") {
|
|
294
|
+
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
295
|
+
if (resolved === null) {
|
|
296
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
cwd = resolved;
|
|
300
|
+
}
|
|
282
301
|
const sessionId = `sess_${this.allocId()}`;
|
|
283
302
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
303
|
const state = {
|
|
@@ -308,15 +327,17 @@ var ACPProtocolHandler = class {
|
|
|
308
327
|
}
|
|
309
328
|
});
|
|
310
329
|
}
|
|
311
|
-
await this.transport.send(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
330
|
+
await this.transport.send(
|
|
331
|
+
toWire({
|
|
332
|
+
jsonrpc: "2.0",
|
|
333
|
+
id,
|
|
334
|
+
result: {
|
|
335
|
+
sessionId,
|
|
336
|
+
modes: this.modes,
|
|
337
|
+
configOptions: this.configOptions
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
);
|
|
320
341
|
return false;
|
|
321
342
|
}
|
|
322
343
|
async handleSessionLoad(id, params) {
|
|
@@ -331,9 +352,15 @@ var ACPProtocolHandler = class {
|
|
|
331
352
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
332
353
|
return false;
|
|
333
354
|
}
|
|
355
|
+
if (loadCwd !== void 0 && await this.resolveSessionCwd(loadCwd) === null) {
|
|
356
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const candidateCwd = persisted.cwd ?? loadCwd ?? this.defaultCwd;
|
|
360
|
+
const restoredCwd = await this.resolveSessionCwd(candidateCwd) ?? this.defaultCwd;
|
|
334
361
|
const restored = {
|
|
335
362
|
id: sessionId,
|
|
336
|
-
cwd:
|
|
363
|
+
cwd: restoredCwd,
|
|
337
364
|
abort: new AbortController(),
|
|
338
365
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
339
366
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -349,13 +376,15 @@ var ACPProtocolHandler = class {
|
|
|
349
376
|
sessionId,
|
|
350
377
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
351
378
|
});
|
|
352
|
-
await this.transport.send(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
await this.transport.send(
|
|
380
|
+
toWire({
|
|
381
|
+
jsonrpc: "2.0",
|
|
382
|
+
id,
|
|
383
|
+
result: {
|
|
384
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
);
|
|
359
388
|
return false;
|
|
360
389
|
}
|
|
361
390
|
}
|
|
@@ -381,16 +410,18 @@ var ACPProtocolHandler = class {
|
|
|
381
410
|
modeId: existing.modeId
|
|
382
411
|
}
|
|
383
412
|
});
|
|
384
|
-
await this.transport.send(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
await this.transport.send(
|
|
414
|
+
toWire({
|
|
415
|
+
jsonrpc: "2.0",
|
|
416
|
+
id,
|
|
417
|
+
result: {
|
|
418
|
+
initialMode: {
|
|
419
|
+
currentModeId: existing.modeId,
|
|
420
|
+
availableModes: this.modes
|
|
421
|
+
}
|
|
391
422
|
}
|
|
392
|
-
}
|
|
393
|
-
|
|
423
|
+
})
|
|
424
|
+
);
|
|
394
425
|
return false;
|
|
395
426
|
}
|
|
396
427
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -402,16 +433,18 @@ var ACPProtocolHandler = class {
|
|
|
402
433
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
403
434
|
if (existing) {
|
|
404
435
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
405
|
-
await this.transport.send(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
436
|
+
await this.transport.send(
|
|
437
|
+
toWire({
|
|
438
|
+
jsonrpc: "2.0",
|
|
439
|
+
id,
|
|
440
|
+
result: {
|
|
441
|
+
initialMode: {
|
|
442
|
+
currentModeId: existing.modeId,
|
|
443
|
+
availableModes: this.modes
|
|
444
|
+
}
|
|
412
445
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
446
|
+
})
|
|
447
|
+
);
|
|
415
448
|
return false;
|
|
416
449
|
}
|
|
417
450
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -428,11 +461,13 @@ var ACPProtocolHandler = class {
|
|
|
428
461
|
session.abort.abort();
|
|
429
462
|
this.sessions.delete(sessionId);
|
|
430
463
|
this.disposeSession(sessionId);
|
|
431
|
-
await this.transport.send(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
464
|
+
await this.transport.send(
|
|
465
|
+
toWire({
|
|
466
|
+
jsonrpc: "2.0",
|
|
467
|
+
id,
|
|
468
|
+
result: {}
|
|
469
|
+
})
|
|
470
|
+
);
|
|
436
471
|
return false;
|
|
437
472
|
}
|
|
438
473
|
async handleSessionDelete(id, params) {
|
|
@@ -443,18 +478,22 @@ var ACPProtocolHandler = class {
|
|
|
443
478
|
return false;
|
|
444
479
|
}
|
|
445
480
|
if (!this.sessions.has(sessionId)) {
|
|
446
|
-
await this.transport.send(
|
|
481
|
+
await this.transport.send(
|
|
482
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
483
|
+
);
|
|
447
484
|
return false;
|
|
448
485
|
}
|
|
449
486
|
const session = this.sessions.get(sessionId);
|
|
450
487
|
session.abort.abort();
|
|
451
488
|
this.sessions.delete(sessionId);
|
|
452
489
|
this.disposeSession(sessionId);
|
|
453
|
-
await this.transport.send(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
490
|
+
await this.transport.send(
|
|
491
|
+
toWire({
|
|
492
|
+
jsonrpc: "2.0",
|
|
493
|
+
id,
|
|
494
|
+
result: {}
|
|
495
|
+
})
|
|
496
|
+
);
|
|
458
497
|
return false;
|
|
459
498
|
}
|
|
460
499
|
async handleSessionFork(id, params) {
|
|
@@ -469,11 +508,20 @@ var ACPProtocolHandler = class {
|
|
|
469
508
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
470
509
|
return false;
|
|
471
510
|
}
|
|
511
|
+
let forkCwd = source.cwd;
|
|
512
|
+
if (typeof p.cwd === "string") {
|
|
513
|
+
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
514
|
+
if (resolved === null) {
|
|
515
|
+
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
forkCwd = resolved;
|
|
519
|
+
}
|
|
472
520
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
473
521
|
const sessionId = `sess_${this.allocId()}`;
|
|
474
522
|
const forked = {
|
|
475
523
|
id: sessionId,
|
|
476
|
-
cwd:
|
|
524
|
+
cwd: forkCwd,
|
|
477
525
|
abort: new AbortController(),
|
|
478
526
|
modeId: source.modeId,
|
|
479
527
|
createdAt: now,
|
|
@@ -492,38 +540,48 @@ var ACPProtocolHandler = class {
|
|
|
492
540
|
sessionId,
|
|
493
541
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
494
542
|
});
|
|
495
|
-
await this.transport.send(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
543
|
+
await this.transport.send(
|
|
544
|
+
toWire({
|
|
545
|
+
jsonrpc: "2.0",
|
|
546
|
+
id,
|
|
547
|
+
result: {
|
|
548
|
+
sessionId,
|
|
549
|
+
modes: this.modes,
|
|
550
|
+
configOptions: this.configOptions
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
);
|
|
504
554
|
return false;
|
|
505
555
|
}
|
|
506
556
|
async handleProvidersList(id, _params) {
|
|
507
|
-
await this.transport.send(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
557
|
+
await this.transport.send(
|
|
558
|
+
toWire({
|
|
559
|
+
jsonrpc: "2.0",
|
|
560
|
+
id,
|
|
561
|
+
result: {
|
|
562
|
+
providers: [],
|
|
563
|
+
currentProviderId: null
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
);
|
|
515
567
|
return false;
|
|
516
568
|
}
|
|
517
569
|
async handleProvidersSet(id, _params) {
|
|
518
|
-
await this.sendError(
|
|
570
|
+
await this.sendError(
|
|
571
|
+
id,
|
|
572
|
+
-32e3,
|
|
573
|
+
"provider configuration not available through ACP; use wstack auth"
|
|
574
|
+
);
|
|
519
575
|
return false;
|
|
520
576
|
}
|
|
521
577
|
async handleProvidersDisable(id, _params) {
|
|
522
|
-
await this.transport.send(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
578
|
+
await this.transport.send(
|
|
579
|
+
toWire({
|
|
580
|
+
jsonrpc: "2.0",
|
|
581
|
+
id,
|
|
582
|
+
result: {}
|
|
583
|
+
})
|
|
584
|
+
);
|
|
527
585
|
return false;
|
|
528
586
|
}
|
|
529
587
|
async handleMcpMessage(id, _params) {
|
|
@@ -576,7 +634,10 @@ var ACPProtocolHandler = class {
|
|
|
576
634
|
const terminalId = created?.terminalId;
|
|
577
635
|
if (!terminalId) return { output: "", exitCode: null };
|
|
578
636
|
try {
|
|
579
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
637
|
+
const exit = await this.request("terminal/wait_for_exit", {
|
|
638
|
+
sessionId,
|
|
639
|
+
terminalId
|
|
640
|
+
});
|
|
580
641
|
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
581
642
|
return {
|
|
582
643
|
output: String(out?.output ?? ""),
|
|
@@ -606,11 +667,13 @@ var ACPProtocolHandler = class {
|
|
|
606
667
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
607
668
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
608
669
|
await this.persist(session);
|
|
609
|
-
await this.transport.send(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
670
|
+
await this.transport.send(
|
|
671
|
+
toWire({
|
|
672
|
+
jsonrpc: "2.0",
|
|
673
|
+
id,
|
|
674
|
+
result: { stopReason: result.stopReason }
|
|
675
|
+
})
|
|
676
|
+
);
|
|
614
677
|
return false;
|
|
615
678
|
}
|
|
616
679
|
async handleSetMode(id, params) {
|
|
@@ -651,7 +714,9 @@ var ACPProtocolHandler = class {
|
|
|
651
714
|
configOptions: [...this.configOptions]
|
|
652
715
|
}
|
|
653
716
|
});
|
|
654
|
-
await this.transport.send(
|
|
717
|
+
await this.transport.send(
|
|
718
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
719
|
+
);
|
|
655
720
|
return false;
|
|
656
721
|
}
|
|
657
722
|
async handleSessionList(id) {
|
|
@@ -664,11 +729,13 @@ var ACPProtocolHandler = class {
|
|
|
664
729
|
if (s.title !== void 0) out.title = s.title;
|
|
665
730
|
return out;
|
|
666
731
|
});
|
|
667
|
-
await this.transport.send(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
732
|
+
await this.transport.send(
|
|
733
|
+
toWire({
|
|
734
|
+
jsonrpc: "2.0",
|
|
735
|
+
id,
|
|
736
|
+
result: { sessions }
|
|
737
|
+
})
|
|
738
|
+
);
|
|
672
739
|
return false;
|
|
673
740
|
}
|
|
674
741
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -714,8 +781,54 @@ var ACPProtocolHandler = class {
|
|
|
714
781
|
if (data !== void 0) error.data = data;
|
|
715
782
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
716
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* Allocate a session id (WS-015).
|
|
786
|
+
*
|
|
787
|
+
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
788
|
+
* handler has no per-connection ownership: any caller that names a session
|
|
789
|
+
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
790
|
+
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
791
|
+
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
792
|
+
* authorization story for any local process or page that reaches the port.
|
|
793
|
+
*
|
|
794
|
+
* Random ids do not create ownership — they remove the trivial enumeration
|
|
795
|
+
* that made its absence exploitable. Real per-connection ownership is the
|
|
796
|
+
* larger fix and is noted in the WS-015 test file.
|
|
797
|
+
*
|
|
798
|
+
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
799
|
+
* uniqueness within a process even in the (impossible) event of a UUID
|
|
800
|
+
* collision. The random half is what makes the id unguessable.
|
|
801
|
+
*/
|
|
802
|
+
/**
|
|
803
|
+
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
804
|
+
* usable (WS-015).
|
|
805
|
+
*
|
|
806
|
+
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
807
|
+
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
808
|
+
* the working directory the agent then reads, writes and executes in.
|
|
809
|
+
*
|
|
810
|
+
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
811
|
+
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
812
|
+
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
813
|
+
* break the integration this package exists for. What it enforces is that
|
|
814
|
+
* the directory is absolute and actually exists as a directory: a relative
|
|
815
|
+
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
816
|
+
* running the agent somewhere other than where the client asked is worse
|
|
817
|
+
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
818
|
+
* on top of this, not in place of it.
|
|
819
|
+
*/
|
|
820
|
+
async resolveSessionCwd(requested) {
|
|
821
|
+
if (!path.isAbsolute(requested)) return null;
|
|
822
|
+
const resolved = path.resolve(requested);
|
|
823
|
+
try {
|
|
824
|
+
const stat3 = await fsp.stat(resolved);
|
|
825
|
+
return stat3.isDirectory() ? resolved : null;
|
|
826
|
+
} catch {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
717
830
|
allocId() {
|
|
718
|
-
return this.nextId
|
|
831
|
+
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
719
832
|
}
|
|
720
833
|
};
|
|
721
834
|
function errorToJsonRpc(err) {
|
|
@@ -739,32 +852,14 @@ import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
|
739
852
|
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
740
853
|
|
|
741
854
|
// src/win32-cmd.ts
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
746
|
-
return {
|
|
747
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
748
|
-
args: ["/d", "/c", line],
|
|
749
|
-
windowsVerbatimArguments: true
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
function assertSafeWin32CmdArgs(args) {
|
|
753
|
-
for (const arg of args) {
|
|
754
|
-
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
755
|
-
throw new Error(
|
|
756
|
-
'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)
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
function quoteWin32CmdArg(arg) {
|
|
762
|
-
return `"${arg}"`;
|
|
763
|
-
}
|
|
855
|
+
import {
|
|
856
|
+
buildWin32CmdShimInvocation
|
|
857
|
+
} from "@wrongstack/core/utils";
|
|
764
858
|
|
|
765
859
|
// src/agent/stdio-transport.ts
|
|
766
860
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
767
861
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
862
|
+
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
768
863
|
function positiveLimit(value, fallback) {
|
|
769
864
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
770
865
|
}
|
|
@@ -774,53 +869,82 @@ var StdioTransport = class {
|
|
|
774
869
|
stderr = process.stderr;
|
|
775
870
|
buffer = "";
|
|
776
871
|
handlers = /* @__PURE__ */ new Set();
|
|
872
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
777
873
|
closed = false;
|
|
778
874
|
resolveRead = null;
|
|
779
875
|
messageQueue = [];
|
|
876
|
+
queuedChars = 0;
|
|
780
877
|
maxFrameChars;
|
|
781
878
|
maxQueuedMessages;
|
|
879
|
+
maxQueuedChars;
|
|
880
|
+
onStdinData = (chunk) => this.onData(chunk);
|
|
881
|
+
onStdinEnd = () => this.handleClose();
|
|
882
|
+
onStdinError = (err) => this.failAll(err);
|
|
782
883
|
constructor(opts = {}) {
|
|
783
884
|
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
784
885
|
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
886
|
+
this.maxQueuedChars = positiveLimit(opts.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
785
887
|
this.stdin.resume();
|
|
786
888
|
this.stdin.setEncoding("utf8");
|
|
787
|
-
this.stdin.on("data",
|
|
788
|
-
this.stdin.on("end",
|
|
789
|
-
this.stdin.on("error",
|
|
889
|
+
this.stdin.on("data", this.onStdinData);
|
|
890
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
891
|
+
this.stdin.on("error", this.onStdinError);
|
|
790
892
|
}
|
|
791
893
|
sendStartupMarker() {
|
|
792
894
|
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
793
895
|
}
|
|
794
896
|
send(msg) {
|
|
795
897
|
if (this.closed) return Promise.resolve();
|
|
796
|
-
return new Promise((
|
|
898
|
+
return new Promise((resolve4) => {
|
|
797
899
|
const line = JSON.stringify(msg) + "\n";
|
|
798
|
-
this.stdout.write(line, "utf8", () =>
|
|
900
|
+
this.stdout.write(line, "utf8", () => resolve4());
|
|
799
901
|
});
|
|
800
902
|
}
|
|
801
903
|
sendRaw(chunk) {
|
|
802
904
|
this.stdout.write(chunk, "utf8");
|
|
803
905
|
}
|
|
804
906
|
read() {
|
|
805
|
-
if (this.messageQueue.length > 0)
|
|
806
|
-
|
|
907
|
+
if (this.messageQueue.length > 0) {
|
|
908
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
909
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
910
|
+
return Promise.resolve(queued.message);
|
|
911
|
+
}
|
|
807
912
|
if (this.closed) return Promise.resolve(null);
|
|
808
|
-
return new Promise((
|
|
809
|
-
this.resolveRead =
|
|
913
|
+
return new Promise((resolve4) => {
|
|
914
|
+
this.resolveRead = resolve4;
|
|
810
915
|
});
|
|
811
916
|
}
|
|
812
917
|
onMessage(handler) {
|
|
813
918
|
this.handlers.add(handler);
|
|
814
919
|
return () => this.handlers.delete(handler);
|
|
815
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
923
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
924
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
925
|
+
* server transport prevents the correlation handler (which always
|
|
926
|
+
* fires on every message but only actually correlates responses)
|
|
927
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
928
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
929
|
+
* the gating logic.
|
|
930
|
+
*/
|
|
931
|
+
onMessageClaim(handler) {
|
|
932
|
+
this.claimHandlers.add(handler);
|
|
933
|
+
return () => this.claimHandlers.delete(handler);
|
|
934
|
+
}
|
|
816
935
|
close() {
|
|
817
936
|
this.closed = true;
|
|
937
|
+
this.stdin.off("data", this.onStdinData);
|
|
938
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
939
|
+
this.stdin.off("error", this.onStdinError);
|
|
818
940
|
this.stdin.pause();
|
|
819
941
|
this.resolveRead?.(null);
|
|
820
942
|
this.resolveRead = null;
|
|
821
943
|
this.buffer = "";
|
|
822
944
|
this.messageQueue.length = 0;
|
|
945
|
+
this.queuedChars = 0;
|
|
823
946
|
this.handlers.clear();
|
|
947
|
+
this.claimHandlers.clear();
|
|
824
948
|
}
|
|
825
949
|
onData(chunk) {
|
|
826
950
|
this.buffer += chunk;
|
|
@@ -847,29 +971,41 @@ var StdioTransport = class {
|
|
|
847
971
|
return;
|
|
848
972
|
}
|
|
849
973
|
try {
|
|
850
|
-
this.dispatch(JSON.parse(raw));
|
|
974
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
851
975
|
} catch (err) {
|
|
852
976
|
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
853
977
|
`, "utf8");
|
|
854
978
|
}
|
|
855
979
|
}
|
|
856
980
|
}
|
|
857
|
-
dispatch(msg) {
|
|
981
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
858
982
|
if (this.resolveRead) {
|
|
859
|
-
const
|
|
983
|
+
const resolve4 = this.resolveRead;
|
|
860
984
|
this.resolveRead = null;
|
|
861
|
-
|
|
985
|
+
resolve4(msg);
|
|
862
986
|
} else {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
987
|
+
let claimed = false;
|
|
988
|
+
for (const handler of this.claimHandlers) {
|
|
989
|
+
try {
|
|
990
|
+
if (handler(msg)) claimed = true;
|
|
991
|
+
} catch (err) {
|
|
992
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
993
|
+
`, "utf8");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (!claimed) {
|
|
997
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
998
|
+
this.stderr.write(
|
|
999
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters
|
|
866
1000
|
`,
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1001
|
+
"utf8"
|
|
1002
|
+
);
|
|
1003
|
+
this.close();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
this.messageQueue.push({ message: msg, chars });
|
|
1007
|
+
this.queuedChars += chars;
|
|
871
1008
|
}
|
|
872
|
-
this.messageQueue.push(msg);
|
|
873
1009
|
}
|
|
874
1010
|
for (const handler of this.handlers) {
|
|
875
1011
|
try {
|
|
@@ -893,12 +1029,15 @@ var ClientTransport = class {
|
|
|
893
1029
|
child = null;
|
|
894
1030
|
buffer = "";
|
|
895
1031
|
handlers = /* @__PURE__ */ new Set();
|
|
1032
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
896
1033
|
closed = false;
|
|
897
1034
|
resolveRead = null;
|
|
898
1035
|
messageQueue = [];
|
|
1036
|
+
queuedChars = 0;
|
|
899
1037
|
opts;
|
|
900
1038
|
maxFrameChars;
|
|
901
1039
|
maxQueuedMessages;
|
|
1040
|
+
maxQueuedChars;
|
|
902
1041
|
constructor(options) {
|
|
903
1042
|
this.opts = {
|
|
904
1043
|
handshakeTimeoutMs: 3e4,
|
|
@@ -906,6 +1045,7 @@ var ClientTransport = class {
|
|
|
906
1045
|
};
|
|
907
1046
|
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
908
1047
|
this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
1048
|
+
this.maxQueuedChars = positiveLimit(options.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
909
1049
|
}
|
|
910
1050
|
async start() {
|
|
911
1051
|
if (this.child) return;
|
|
@@ -914,7 +1054,7 @@ var ClientTransport = class {
|
|
|
914
1054
|
import("@wrongstack/core/utils"),
|
|
915
1055
|
import("node:os")
|
|
916
1056
|
]);
|
|
917
|
-
return new Promise((
|
|
1057
|
+
return new Promise((resolve4, reject) => {
|
|
918
1058
|
const timeout = setTimeout(() => {
|
|
919
1059
|
const child2 = this.child;
|
|
920
1060
|
this.child = null;
|
|
@@ -967,7 +1107,7 @@ var ClientTransport = class {
|
|
|
967
1107
|
if (settled) return;
|
|
968
1108
|
settled = true;
|
|
969
1109
|
clearTimeout(timeout);
|
|
970
|
-
|
|
1110
|
+
resolve4();
|
|
971
1111
|
});
|
|
972
1112
|
return;
|
|
973
1113
|
}
|
|
@@ -978,7 +1118,7 @@ var ClientTransport = class {
|
|
|
978
1118
|
child.stderr.on("data", (c) => this.onChildError(c));
|
|
979
1119
|
child.on("close", (code) => this.onChildClose(code));
|
|
980
1120
|
clearTimeout(timeout);
|
|
981
|
-
|
|
1121
|
+
resolve4();
|
|
982
1122
|
};
|
|
983
1123
|
const waitForMarker = (chunk) => {
|
|
984
1124
|
this.buffer += chunk;
|
|
@@ -997,33 +1137,52 @@ var ClientTransport = class {
|
|
|
997
1137
|
}
|
|
998
1138
|
send(msg) {
|
|
999
1139
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
1000
|
-
return new Promise((
|
|
1140
|
+
return new Promise((resolve4, reject) => {
|
|
1001
1141
|
const line = JSON.stringify(msg) + "\n";
|
|
1002
1142
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
1003
1143
|
if (err) reject(err);
|
|
1004
|
-
else
|
|
1144
|
+
else resolve4();
|
|
1005
1145
|
});
|
|
1006
1146
|
});
|
|
1007
1147
|
}
|
|
1008
1148
|
read() {
|
|
1009
|
-
if (this.messageQueue.length > 0)
|
|
1010
|
-
|
|
1149
|
+
if (this.messageQueue.length > 0) {
|
|
1150
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
1151
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
1152
|
+
return Promise.resolve(queued.message);
|
|
1153
|
+
}
|
|
1011
1154
|
if (this.closed) return Promise.resolve(null);
|
|
1012
|
-
return new Promise((
|
|
1013
|
-
this.resolveRead =
|
|
1155
|
+
return new Promise((resolve4) => {
|
|
1156
|
+
this.resolveRead = resolve4;
|
|
1014
1157
|
});
|
|
1015
1158
|
}
|
|
1016
1159
|
onMessage(handler) {
|
|
1017
1160
|
this.handlers.add(handler);
|
|
1018
1161
|
return () => this.handlers.delete(handler);
|
|
1019
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
1165
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
1166
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
1167
|
+
* server transport prevents the correlation handler (which always
|
|
1168
|
+
* fires on every message but only actually correlates responses)
|
|
1169
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
1170
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
1171
|
+
* the gating logic.
|
|
1172
|
+
*/
|
|
1173
|
+
onMessageClaim(handler) {
|
|
1174
|
+
this.claimHandlers.add(handler);
|
|
1175
|
+
return () => this.claimHandlers.delete(handler);
|
|
1176
|
+
}
|
|
1020
1177
|
stop() {
|
|
1021
1178
|
this.closed = true;
|
|
1022
1179
|
this.resolveRead?.(null);
|
|
1023
1180
|
this.resolveRead = null;
|
|
1024
1181
|
this.buffer = "";
|
|
1025
1182
|
this.messageQueue.length = 0;
|
|
1183
|
+
this.queuedChars = 0;
|
|
1026
1184
|
this.handlers.clear();
|
|
1185
|
+
this.claimHandlers.clear();
|
|
1027
1186
|
const child = this.child;
|
|
1028
1187
|
if (!child) return;
|
|
1029
1188
|
treeKill(child);
|
|
@@ -1048,7 +1207,7 @@ var ClientTransport = class {
|
|
|
1048
1207
|
return;
|
|
1049
1208
|
}
|
|
1050
1209
|
try {
|
|
1051
|
-
this.dispatch(JSON.parse(raw));
|
|
1210
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
1052
1211
|
} catch {
|
|
1053
1212
|
}
|
|
1054
1213
|
}
|
|
@@ -1058,29 +1217,34 @@ var ClientTransport = class {
|
|
|
1058
1217
|
}
|
|
1059
1218
|
onChildClose(code) {
|
|
1060
1219
|
this.closed = true;
|
|
1220
|
+
this.child = null;
|
|
1061
1221
|
this.resolveRead?.(null);
|
|
1062
1222
|
this.resolveRead = null;
|
|
1063
1223
|
this.buffer = "";
|
|
1064
1224
|
this.messageQueue.length = 0;
|
|
1225
|
+
this.queuedChars = 0;
|
|
1065
1226
|
this.handlers.clear();
|
|
1066
1227
|
if (code !== 0 && code !== null) {
|
|
1067
1228
|
writeErr(`[acp-child exited with code ${code}]
|
|
1068
1229
|
`);
|
|
1069
1230
|
}
|
|
1070
1231
|
}
|
|
1071
|
-
dispatch(msg) {
|
|
1232
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1072
1233
|
if (this.resolveRead) {
|
|
1073
|
-
const
|
|
1234
|
+
const resolve4 = this.resolveRead;
|
|
1074
1235
|
this.resolveRead = null;
|
|
1075
|
-
|
|
1236
|
+
resolve4(msg);
|
|
1076
1237
|
} else if (this.handlers.size === 0) {
|
|
1077
|
-
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
1078
|
-
writeErr(
|
|
1079
|
-
`
|
|
1238
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
1239
|
+
writeErr(
|
|
1240
|
+
`[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
|
|
1241
|
+
`
|
|
1242
|
+
);
|
|
1080
1243
|
this.stop();
|
|
1081
1244
|
return;
|
|
1082
1245
|
}
|
|
1083
|
-
this.messageQueue.push(msg);
|
|
1246
|
+
this.messageQueue.push({ message: msg, chars });
|
|
1247
|
+
this.queuedChars += chars;
|
|
1084
1248
|
}
|
|
1085
1249
|
for (const handler of this.handlers) {
|
|
1086
1250
|
try {
|
|
@@ -1374,12 +1538,12 @@ var WrongStackACPServer = class {
|
|
|
1374
1538
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1375
1539
|
}
|
|
1376
1540
|
});
|
|
1377
|
-
return new Promise((
|
|
1541
|
+
return new Promise((resolve4) => {
|
|
1378
1542
|
this.httpServer.listen(port, host, () => {
|
|
1379
1543
|
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1380
1544
|
`);
|
|
1381
1545
|
this.running = true;
|
|
1382
|
-
|
|
1546
|
+
resolve4();
|
|
1383
1547
|
});
|
|
1384
1548
|
});
|
|
1385
1549
|
}
|
|
@@ -1462,26 +1626,26 @@ function emptyRunResult(stopReason) {
|
|
|
1462
1626
|
// src/client/file-server.ts
|
|
1463
1627
|
import { randomBytes } from "node:crypto";
|
|
1464
1628
|
import { realpathSync } from "node:fs";
|
|
1465
|
-
import * as
|
|
1466
|
-
import * as
|
|
1629
|
+
import * as fsp2 from "node:fs/promises";
|
|
1630
|
+
import * as path2 from "node:path";
|
|
1467
1631
|
var DEFAULT_FILE_OPERATIONS = {
|
|
1468
|
-
stat:
|
|
1469
|
-
readFile:
|
|
1470
|
-
writeFile:
|
|
1471
|
-
realpath:
|
|
1472
|
-
rename:
|
|
1473
|
-
unlink:
|
|
1632
|
+
stat: fsp2.stat,
|
|
1633
|
+
readFile: fsp2.readFile,
|
|
1634
|
+
writeFile: fsp2.writeFile,
|
|
1635
|
+
realpath: fsp2.realpath,
|
|
1636
|
+
rename: fsp2.rename,
|
|
1637
|
+
unlink: fsp2.unlink
|
|
1474
1638
|
};
|
|
1475
1639
|
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1476
1640
|
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1477
1641
|
var FsError = class extends Error {
|
|
1478
1642
|
code;
|
|
1479
1643
|
path;
|
|
1480
|
-
constructor(code,
|
|
1644
|
+
constructor(code, path5, message) {
|
|
1481
1645
|
super(message);
|
|
1482
1646
|
this.name = "FsError";
|
|
1483
1647
|
this.code = code;
|
|
1484
|
-
this.path =
|
|
1648
|
+
this.path = path5;
|
|
1485
1649
|
}
|
|
1486
1650
|
};
|
|
1487
1651
|
var FileServer = class {
|
|
@@ -1492,7 +1656,7 @@ var FileServer = class {
|
|
|
1492
1656
|
maxWriteBytes;
|
|
1493
1657
|
operations;
|
|
1494
1658
|
constructor(opts) {
|
|
1495
|
-
this.root =
|
|
1659
|
+
this.root = path2.resolve(opts.projectRoot);
|
|
1496
1660
|
this.realRoot = safeRealpathSync(this.root);
|
|
1497
1661
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1498
1662
|
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
@@ -1505,14 +1669,14 @@ var FileServer = class {
|
|
|
1505
1669
|
const controller = new AbortController();
|
|
1506
1670
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1507
1671
|
try {
|
|
1508
|
-
const
|
|
1672
|
+
const stat3 = await this.operations.stat(safe).catch((err) => {
|
|
1509
1673
|
throw mapFsError(err, safe);
|
|
1510
1674
|
});
|
|
1511
|
-
if (
|
|
1675
|
+
if (stat3.size > this.maxReadBytes) {
|
|
1512
1676
|
throw new FsError(
|
|
1513
1677
|
"TOO_LARGE",
|
|
1514
1678
|
safe,
|
|
1515
|
-
`file is ${
|
|
1679
|
+
`file is ${stat3.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1516
1680
|
);
|
|
1517
1681
|
}
|
|
1518
1682
|
const content = await this.operations.readFile(safe, {
|
|
@@ -1550,7 +1714,7 @@ var FileServer = class {
|
|
|
1550
1714
|
signal: controller.signal
|
|
1551
1715
|
});
|
|
1552
1716
|
await this.assertRealInside(tmp);
|
|
1553
|
-
await this.assertRealInside(
|
|
1717
|
+
await this.assertRealInside(path2.dirname(safe));
|
|
1554
1718
|
await this.operations.rename(tmp, safe);
|
|
1555
1719
|
} catch (err) {
|
|
1556
1720
|
if (err instanceof FsError) {
|
|
@@ -1581,11 +1745,11 @@ var FileServer = class {
|
|
|
1581
1745
|
if (typeof p !== "string" || p.length === 0) {
|
|
1582
1746
|
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
1583
1747
|
}
|
|
1584
|
-
if (!
|
|
1748
|
+
if (!path2.isAbsolute(p)) {
|
|
1585
1749
|
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
1586
1750
|
}
|
|
1587
|
-
const resolved =
|
|
1588
|
-
const rootWithSep = this.root.endsWith(
|
|
1751
|
+
const resolved = path2.resolve(p);
|
|
1752
|
+
const rootWithSep = this.root.endsWith(path2.sep) ? this.root : this.root + path2.sep;
|
|
1589
1753
|
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
1590
1754
|
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
1591
1755
|
}
|
|
@@ -1606,7 +1770,7 @@ var FileServer = class {
|
|
|
1606
1770
|
} catch (err) {
|
|
1607
1771
|
const code = err.code;
|
|
1608
1772
|
if (code === "ENOENT") {
|
|
1609
|
-
const parent =
|
|
1773
|
+
const parent = path2.dirname(probe);
|
|
1610
1774
|
if (parent === probe) {
|
|
1611
1775
|
throw new FsError("ENOENT", resolvedPath, `no existing ancestor: ${resolvedPath}`);
|
|
1612
1776
|
}
|
|
@@ -1615,7 +1779,7 @@ var FileServer = class {
|
|
|
1615
1779
|
}
|
|
1616
1780
|
throw mapFsError(err, resolvedPath);
|
|
1617
1781
|
}
|
|
1618
|
-
if (real === this.realRoot || real.startsWith(this.realRoot +
|
|
1782
|
+
if (real === this.realRoot || real.startsWith(this.realRoot + path2.sep)) return;
|
|
1619
1783
|
throw new FsError(
|
|
1620
1784
|
"OUTSIDE_ROOT",
|
|
1621
1785
|
resolvedPath,
|
|
@@ -1688,8 +1852,9 @@ function makePermissionPolicy(decide) {
|
|
|
1688
1852
|
// src/client/terminal-server.ts
|
|
1689
1853
|
import { spawn } from "node:child_process";
|
|
1690
1854
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1691
|
-
import * as
|
|
1855
|
+
import * as path3 from "node:path";
|
|
1692
1856
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1857
|
+
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1693
1858
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1694
1859
|
var TerminalServer = class {
|
|
1695
1860
|
terminals = /* @__PURE__ */ new Map();
|
|
@@ -1702,7 +1867,7 @@ var TerminalServer = class {
|
|
|
1702
1867
|
abortHandler = () => this.releaseAll();
|
|
1703
1868
|
nextId = 1;
|
|
1704
1869
|
constructor(opts) {
|
|
1705
|
-
this.projectRoot =
|
|
1870
|
+
this.projectRoot = path3.resolve(opts.projectRoot);
|
|
1706
1871
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1707
1872
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1708
1873
|
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
@@ -1749,7 +1914,7 @@ var TerminalServer = class {
|
|
|
1749
1914
|
truncated: false,
|
|
1750
1915
|
exitStatus: void 0,
|
|
1751
1916
|
timeoutHandle: null,
|
|
1752
|
-
exitPromise: new Promise((
|
|
1917
|
+
exitPromise: new Promise((resolve4) => {
|
|
1753
1918
|
proc.on("close", (code, signalName) => {
|
|
1754
1919
|
if (state.timeoutHandle) {
|
|
1755
1920
|
clearTimeout(state.timeoutHandle);
|
|
@@ -1760,7 +1925,7 @@ var TerminalServer = class {
|
|
|
1760
1925
|
signal: typeof signalName === "string" ? signalName : null
|
|
1761
1926
|
};
|
|
1762
1927
|
state.exitStatus = exitStatus;
|
|
1763
|
-
|
|
1928
|
+
resolve4(exitStatus);
|
|
1764
1929
|
});
|
|
1765
1930
|
proc.on("error", (err) => {
|
|
1766
1931
|
if (state.timeoutHandle) {
|
|
@@ -1779,7 +1944,7 @@ var TerminalServer = class {
|
|
|
1779
1944
|
}
|
|
1780
1945
|
state.outputChunks.push(errorOutput);
|
|
1781
1946
|
state.retainedBytes = errorOutput.length;
|
|
1782
|
-
|
|
1947
|
+
resolve4(exitStatus);
|
|
1783
1948
|
});
|
|
1784
1949
|
})
|
|
1785
1950
|
};
|
|
@@ -1809,13 +1974,11 @@ var TerminalServer = class {
|
|
|
1809
1974
|
state.outputHead = 0;
|
|
1810
1975
|
}
|
|
1811
1976
|
};
|
|
1977
|
+
state.onData = onData;
|
|
1812
1978
|
proc.stdout?.on("data", onData);
|
|
1813
1979
|
proc.stderr?.on("data", onData);
|
|
1814
1980
|
state.timeoutHandle = setTimeout(() => {
|
|
1815
|
-
|
|
1816
|
-
proc.kill("SIGTERM");
|
|
1817
|
-
} catch {
|
|
1818
|
-
}
|
|
1981
|
+
treeKill2(proc);
|
|
1819
1982
|
}, this.commandTimeoutMs);
|
|
1820
1983
|
this.terminals.set(id, state);
|
|
1821
1984
|
return { terminalId: id };
|
|
@@ -1839,14 +2002,15 @@ var TerminalServer = class {
|
|
|
1839
2002
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1840
2003
|
return state.exitPromise;
|
|
1841
2004
|
}
|
|
1842
|
-
/**
|
|
2005
|
+
/**
|
|
2006
|
+
* Kill the process but keep the terminal record (agent can still read output).
|
|
2007
|
+
* On POSIX this signals only the direct child; descendants may survive because
|
|
2008
|
+
* terminal processes are not spawned as process-group leaders.
|
|
2009
|
+
*/
|
|
1843
2010
|
kill(terminalId) {
|
|
1844
2011
|
const state = this.terminals.get(terminalId);
|
|
1845
2012
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1846
|
-
|
|
1847
|
-
state.proc.kill("SIGTERM");
|
|
1848
|
-
} catch {
|
|
1849
|
-
}
|
|
2013
|
+
treeKill2(state.proc);
|
|
1850
2014
|
}
|
|
1851
2015
|
/** Kill the process if alive and remove the record. */
|
|
1852
2016
|
release(terminalId) {
|
|
@@ -1856,10 +2020,17 @@ var TerminalServer = class {
|
|
|
1856
2020
|
clearTimeout(state.timeoutHandle);
|
|
1857
2021
|
state.timeoutHandle = null;
|
|
1858
2022
|
}
|
|
1859
|
-
|
|
1860
|
-
state.proc.
|
|
1861
|
-
|
|
1862
|
-
|
|
2023
|
+
if (state.onData) {
|
|
2024
|
+
state.proc.stdout?.off("data", state.onData);
|
|
2025
|
+
state.proc.stderr?.off("data", state.onData);
|
|
2026
|
+
state.onData = void 0;
|
|
2027
|
+
}
|
|
2028
|
+
state.proc.stdout?.destroy?.();
|
|
2029
|
+
state.proc.stderr?.destroy?.();
|
|
2030
|
+
state.outputChunks.length = 0;
|
|
2031
|
+
state.outputHead = 0;
|
|
2032
|
+
state.retainedBytes = 0;
|
|
2033
|
+
treeKill2(state.proc, { force: true });
|
|
1863
2034
|
this.terminals.delete(terminalId);
|
|
1864
2035
|
}
|
|
1865
2036
|
/** Kill all active terminals. Used on session close. */
|
|
@@ -1871,15 +2042,15 @@ var TerminalServer = class {
|
|
|
1871
2042
|
}
|
|
1872
2043
|
resolveCwd(cwd) {
|
|
1873
2044
|
if (!cwd) return this.projectRoot;
|
|
1874
|
-
const resolved =
|
|
1875
|
-
const rootWithSep = this.projectRoot.endsWith(
|
|
2045
|
+
const resolved = path3.resolve(cwd);
|
|
2046
|
+
const rootWithSep = this.projectRoot.endsWith(path3.sep) ? this.projectRoot : this.projectRoot + path3.sep;
|
|
1876
2047
|
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1877
2048
|
return this.projectRoot;
|
|
1878
2049
|
}
|
|
1879
2050
|
try {
|
|
1880
2051
|
const realRoot = realpathSync2(this.projectRoot);
|
|
1881
2052
|
const realCwd = realpathSync2(resolved);
|
|
1882
|
-
const realRootWithSep = realRoot.endsWith(
|
|
2053
|
+
const realRootWithSep = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
|
|
1883
2054
|
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
1884
2055
|
return realRoot;
|
|
1885
2056
|
}
|
|
@@ -2037,20 +2208,23 @@ var WebSocketClientTransport = class {
|
|
|
2037
2208
|
);
|
|
2038
2209
|
}
|
|
2039
2210
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
2040
|
-
return new Promise((
|
|
2211
|
+
return new Promise((resolve4, reject) => {
|
|
2041
2212
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
2042
2213
|
this.ws = ws;
|
|
2043
2214
|
const timer = setTimeout(() => {
|
|
2044
2215
|
const pending = this.pendingStart;
|
|
2045
2216
|
if (pending === null) return;
|
|
2046
2217
|
this.pendingStart = null;
|
|
2218
|
+
this.closed = true;
|
|
2219
|
+
if (this.ws === ws) this.ws = null;
|
|
2220
|
+
this.handlers.clear();
|
|
2047
2221
|
try {
|
|
2048
2222
|
ws.close();
|
|
2049
2223
|
} catch {
|
|
2050
2224
|
}
|
|
2051
2225
|
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
2052
2226
|
}, timeoutMs);
|
|
2053
|
-
this.pendingStart = { resolve:
|
|
2227
|
+
this.pendingStart = { resolve: resolve4, reject, timer };
|
|
2054
2228
|
ws.addEventListener("open", () => {
|
|
2055
2229
|
const pending = this.pendingStart;
|
|
2056
2230
|
if (pending === null) return;
|
|
@@ -2061,16 +2235,21 @@ var WebSocketClientTransport = class {
|
|
|
2061
2235
|
ws.addEventListener("error", (ev) => {
|
|
2062
2236
|
const pending = this.pendingStart;
|
|
2063
2237
|
if (pending === null) {
|
|
2064
|
-
this.
|
|
2238
|
+
this.stop();
|
|
2065
2239
|
return;
|
|
2066
2240
|
}
|
|
2067
2241
|
this.pendingStart = null;
|
|
2242
|
+
this.closed = true;
|
|
2243
|
+
if (this.ws === ws) this.ws = null;
|
|
2244
|
+
this.handlers.clear();
|
|
2068
2245
|
clearTimeout(pending.timer);
|
|
2069
2246
|
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
2070
2247
|
pending.reject(new Error(message));
|
|
2071
2248
|
});
|
|
2072
2249
|
ws.addEventListener("close", () => {
|
|
2073
2250
|
this.closed = true;
|
|
2251
|
+
if (this.ws === ws) this.ws = null;
|
|
2252
|
+
this.handlers.clear();
|
|
2074
2253
|
const pending = this.pendingStart;
|
|
2075
2254
|
if (pending !== null) {
|
|
2076
2255
|
this.pendingStart = null;
|
|
@@ -2106,6 +2285,7 @@ var WebSocketClientTransport = class {
|
|
|
2106
2285
|
}
|
|
2107
2286
|
stop() {
|
|
2108
2287
|
this.closed = true;
|
|
2288
|
+
this.handlers.clear();
|
|
2109
2289
|
if (this.pendingStart !== null) {
|
|
2110
2290
|
const pending = this.pendingStart;
|
|
2111
2291
|
this.pendingStart = null;
|
|
@@ -3086,7 +3266,7 @@ var ACPSession = class _ACPSession {
|
|
|
3086
3266
|
return this.nextId++;
|
|
3087
3267
|
}
|
|
3088
3268
|
async sendRequest(id, method, params, timeoutMs) {
|
|
3089
|
-
return new Promise((
|
|
3269
|
+
return new Promise((resolve4, reject) => {
|
|
3090
3270
|
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
3091
3271
|
const handle = setTimeout(() => {
|
|
3092
3272
|
this.pending.delete(id);
|
|
@@ -3096,7 +3276,7 @@ var ACPSession = class _ACPSession {
|
|
|
3096
3276
|
}, effectiveTimeout);
|
|
3097
3277
|
this.pending.set(id, {
|
|
3098
3278
|
method,
|
|
3099
|
-
resolve:
|
|
3279
|
+
resolve: resolve4,
|
|
3100
3280
|
reject,
|
|
3101
3281
|
timeoutMs: effectiveTimeout,
|
|
3102
3282
|
timeoutHandle: handle
|
|
@@ -3262,12 +3442,12 @@ var ToolTranslator = class {
|
|
|
3262
3442
|
id: callId,
|
|
3263
3443
|
params: { name, arguments: args }
|
|
3264
3444
|
});
|
|
3265
|
-
return new Promise((
|
|
3445
|
+
return new Promise((resolve4, reject) => {
|
|
3266
3446
|
const timeout = setTimeout(() => {
|
|
3267
3447
|
this.pending.delete(callId);
|
|
3268
3448
|
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
3269
3449
|
}, this.opts.totalTimeoutMs);
|
|
3270
|
-
this.pending.set(callId, { resolve:
|
|
3450
|
+
this.pending.set(callId, { resolve: resolve4, reject, timeout });
|
|
3271
3451
|
});
|
|
3272
3452
|
}
|
|
3273
3453
|
cancelAll() {
|
|
@@ -3279,8 +3459,8 @@ var ToolTranslator = class {
|
|
|
3279
3459
|
};
|
|
3280
3460
|
|
|
3281
3461
|
// src/integration/acp-bench.ts
|
|
3282
|
-
import * as
|
|
3283
|
-
import * as
|
|
3462
|
+
import * as fsp3 from "node:fs/promises";
|
|
3463
|
+
import * as path4 from "node:path";
|
|
3284
3464
|
function firstLine(s) {
|
|
3285
3465
|
const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
3286
3466
|
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
@@ -3347,11 +3527,11 @@ async function benchOne(agentId, cmd, opts) {
|
|
|
3347
3527
|
if (opts.checkFs) {
|
|
3348
3528
|
const fileToken = `FILE_${opts.marker}`;
|
|
3349
3529
|
const fileName = `acp-bench-${opts.marker}.txt`;
|
|
3350
|
-
const filePath =
|
|
3530
|
+
const filePath = path4.join(opts.projectRoot, fileName);
|
|
3351
3531
|
let fsOk = false;
|
|
3352
3532
|
let fsDetail;
|
|
3353
3533
|
try {
|
|
3354
|
-
await
|
|
3534
|
+
await fsp3.writeFile(filePath, fileToken, "utf8");
|
|
3355
3535
|
const fsRes = await session.prompt(
|
|
3356
3536
|
[
|
|
3357
3537
|
textContent(
|
|
@@ -3487,7 +3667,7 @@ function renderAcpBenchText(result) {
|
|
|
3487
3667
|
);
|
|
3488
3668
|
return lines.join("\n");
|
|
3489
3669
|
}
|
|
3490
|
-
async function removeBenchFile(filePath, remove =
|
|
3670
|
+
async function removeBenchFile(filePath, remove = fsp3.rm) {
|
|
3491
3671
|
await remove(filePath, { force: true }).catch(() => void 0);
|
|
3492
3672
|
}
|
|
3493
3673
|
|
|
@@ -4027,7 +4207,7 @@ import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
|
4027
4207
|
|
|
4028
4208
|
// src/registry/ensemble-registry.ts
|
|
4029
4209
|
import { spawn as spawn2 } from "node:child_process";
|
|
4030
|
-
import { treeKill as
|
|
4210
|
+
import { treeKill as treeKill3 } from "@wrongstack/core/utils/tree-kill";
|
|
4031
4211
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
4032
4212
|
var PROBE_CACHE_MS = 5e3;
|
|
4033
4213
|
var MAX_PARALLEL_PROBES = 4;
|
|
@@ -4053,7 +4233,7 @@ async function probeWithBound(items, worker, limit) {
|
|
|
4053
4233
|
}
|
|
4054
4234
|
async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = process.platform) {
|
|
4055
4235
|
const start = Date.now();
|
|
4056
|
-
return new Promise((
|
|
4236
|
+
return new Promise((resolve4) => {
|
|
4057
4237
|
let settled = false;
|
|
4058
4238
|
let stdout = "";
|
|
4059
4239
|
let stderr = "";
|
|
@@ -4062,11 +4242,11 @@ async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = p
|
|
|
4062
4242
|
settled = true;
|
|
4063
4243
|
try {
|
|
4064
4244
|
if (child.exitCode === null && child.signalCode === null) {
|
|
4065
|
-
|
|
4245
|
+
treeKill3(child);
|
|
4066
4246
|
}
|
|
4067
4247
|
} catch {
|
|
4068
4248
|
}
|
|
4069
|
-
|
|
4249
|
+
resolve4(result);
|
|
4070
4250
|
};
|
|
4071
4251
|
let child;
|
|
4072
4252
|
try {
|