@wrongstack/acp 1.0.24 → 1.0.27
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.js +12 -3
- package/dist/client/acp-session-auth.d.ts +20 -0
- package/dist/client/acp-session.d.ts +1 -0
- package/dist/client.js +474 -406
- package/dist/index.js +475 -408
- package/dist/wrongstack-acp-agent.js +12 -3
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -11,6 +11,16 @@ import {
|
|
|
11
11
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
12
12
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
13
13
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
14
|
+
function takeLines(pending, chunk) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
let start = 0;
|
|
17
|
+
for (let nl = chunk.indexOf("\n"); nl !== -1; nl = chunk.indexOf("\n", start)) {
|
|
18
|
+
lines.push(pending + chunk.slice(start, nl));
|
|
19
|
+
pending = "";
|
|
20
|
+
start = nl + 1;
|
|
21
|
+
}
|
|
22
|
+
return { lines, pending: pending + chunk.slice(start) };
|
|
23
|
+
}
|
|
14
24
|
function positiveLimit(value, fallback) {
|
|
15
25
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
16
26
|
}
|
|
@@ -178,9 +188,8 @@ var ClientTransport = class {
|
|
|
178
188
|
this.child = null;
|
|
179
189
|
}
|
|
180
190
|
onChildData(chunk) {
|
|
181
|
-
this.buffer
|
|
182
|
-
|
|
183
|
-
this.buffer = lines.pop() ?? "";
|
|
191
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
192
|
+
this.buffer = pending;
|
|
184
193
|
if (this.buffer.length > this.maxFrameChars) {
|
|
185
194
|
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
186
195
|
`);
|
|
@@ -335,6 +344,324 @@ function isBestEffortAckMethod(method) {
|
|
|
335
344
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
336
345
|
}
|
|
337
346
|
|
|
347
|
+
// src/client/acp-session-errors.ts
|
|
348
|
+
var ACPSessionError = class extends Error {
|
|
349
|
+
kind;
|
|
350
|
+
cause;
|
|
351
|
+
constructor(kind, message, cause) {
|
|
352
|
+
super(message);
|
|
353
|
+
this.name = "ACPSessionError";
|
|
354
|
+
this.kind = kind;
|
|
355
|
+
this.cause = cause;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function isJsonRpcError(v) {
|
|
359
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
360
|
+
}
|
|
361
|
+
function isAuthRequiredError(err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
364
|
+
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
365
|
+
if (!cause || typeof cause !== "object") return false;
|
|
366
|
+
const data = cause.data;
|
|
367
|
+
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
368
|
+
if (data && typeof data === "object") {
|
|
369
|
+
const d = data;
|
|
370
|
+
if (d.authRequired === true) return true;
|
|
371
|
+
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/client/acp-session-ops.ts
|
|
377
|
+
function filterMcpServers(agentCapabilities, servers) {
|
|
378
|
+
if (!servers || servers.length === 0) return [];
|
|
379
|
+
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
380
|
+
return servers.filter((s) => {
|
|
381
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
382
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
387
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
388
|
+
if (!ctx.agentCapabilities.loadSession) {
|
|
389
|
+
throw new ACPSessionError(
|
|
390
|
+
"unsupported_capability",
|
|
391
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
395
|
+
await ctx.closeSession();
|
|
396
|
+
}
|
|
397
|
+
ctx.resetScratch();
|
|
398
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
399
|
+
const id = ctx.allocId();
|
|
400
|
+
const result = await ctx.sendRequest(id, "session/load", {
|
|
401
|
+
sessionId,
|
|
402
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
403
|
+
mcpServers: servers
|
|
404
|
+
});
|
|
405
|
+
if (isJsonRpcError(result)) {
|
|
406
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
407
|
+
}
|
|
408
|
+
ctx.setSessionId(sessionId);
|
|
409
|
+
}
|
|
410
|
+
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
411
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
412
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
413
|
+
throw new ACPSessionError(
|
|
414
|
+
"unsupported_capability",
|
|
415
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
419
|
+
await ctx.closeSession();
|
|
420
|
+
}
|
|
421
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
422
|
+
const id = ctx.allocId();
|
|
423
|
+
const result = await ctx.sendRequest(id, "session/resume", {
|
|
424
|
+
sessionId,
|
|
425
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
426
|
+
mcpServers: servers
|
|
427
|
+
});
|
|
428
|
+
if (isJsonRpcError(result)) {
|
|
429
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
430
|
+
}
|
|
431
|
+
ctx.setSessionId(sessionId);
|
|
432
|
+
}
|
|
433
|
+
async function executeListSessions(ctx, cursor, cwd) {
|
|
434
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
435
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
436
|
+
throw new ACPSessionError(
|
|
437
|
+
"unsupported_capability",
|
|
438
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const id = ctx.allocId();
|
|
442
|
+
const params = {};
|
|
443
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
444
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
445
|
+
const result = await ctx.sendRequest(id, "session/list", params);
|
|
446
|
+
if (isJsonRpcError(result)) {
|
|
447
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
448
|
+
}
|
|
449
|
+
const r = result;
|
|
450
|
+
return {
|
|
451
|
+
sessions: r.sessions ?? [],
|
|
452
|
+
nextCursor: r.nextCursor
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function executeDeleteSession(ctx, sessionId) {
|
|
456
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
457
|
+
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
458
|
+
throw new ACPSessionError(
|
|
459
|
+
"unsupported_capability",
|
|
460
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const id = ctx.allocId();
|
|
464
|
+
const result = await ctx.sendRequest(id, "session/delete", { sessionId });
|
|
465
|
+
if (isJsonRpcError(result)) {
|
|
466
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
467
|
+
}
|
|
468
|
+
if (ctx.sessionId === sessionId) {
|
|
469
|
+
ctx.setSessionId(null);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
|
|
473
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
474
|
+
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
475
|
+
const id = ctx.allocId();
|
|
476
|
+
const result = await ctx.sendRequest(id, "session/fork", {
|
|
477
|
+
sessionId: sourceSessionId,
|
|
478
|
+
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
479
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
480
|
+
});
|
|
481
|
+
if (isJsonRpcError(result)) {
|
|
482
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
483
|
+
}
|
|
484
|
+
const newId = result.sessionId;
|
|
485
|
+
if (typeof newId !== "string" || !newId) {
|
|
486
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
487
|
+
}
|
|
488
|
+
return newId;
|
|
489
|
+
}
|
|
490
|
+
async function executeSetMode(ctx, sessionId, modeId) {
|
|
491
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
492
|
+
const id = ctx.allocId();
|
|
493
|
+
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
494
|
+
if (isJsonRpcError(result)) {
|
|
495
|
+
throw new ACPSessionError(
|
|
496
|
+
"prompt_failed",
|
|
497
|
+
`session/set_mode failed: ${result.message}`,
|
|
498
|
+
result
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function executeSetConfigOption(ctx, sessionId, configId, value) {
|
|
503
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
504
|
+
const id = ctx.allocId();
|
|
505
|
+
const result = await ctx.sendRequest(id, "session/set_config_option", {
|
|
506
|
+
sessionId,
|
|
507
|
+
configId,
|
|
508
|
+
value
|
|
509
|
+
});
|
|
510
|
+
if (isJsonRpcError(result)) {
|
|
511
|
+
throw new ACPSessionError(
|
|
512
|
+
"prompt_failed",
|
|
513
|
+
`session/set_config_option failed: ${result.message}`,
|
|
514
|
+
result
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function executeListProviders(ctx) {
|
|
519
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
520
|
+
const id = ctx.allocId();
|
|
521
|
+
const result = await ctx.sendRequest(id, "providers/list", {});
|
|
522
|
+
if (isJsonRpcError(result)) {
|
|
523
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
524
|
+
}
|
|
525
|
+
const r = result;
|
|
526
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
527
|
+
}
|
|
528
|
+
async function executeSetProvider(ctx, providerId, config) {
|
|
529
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
530
|
+
const id = ctx.allocId();
|
|
531
|
+
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
532
|
+
if (isJsonRpcError(result)) {
|
|
533
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function executeDisableProvider(ctx) {
|
|
537
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
538
|
+
const id = ctx.allocId();
|
|
539
|
+
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
540
|
+
if (isJsonRpcError(result)) {
|
|
541
|
+
throw new ACPSessionError(
|
|
542
|
+
"prompt_failed",
|
|
543
|
+
`providers/disable failed: ${result.message}`,
|
|
544
|
+
result
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function executeMcpMessage(ctx, connectionId, message) {
|
|
549
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
550
|
+
const id = ctx.allocId();
|
|
551
|
+
const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
|
|
552
|
+
if (isJsonRpcError(result)) {
|
|
553
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
554
|
+
}
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
async function executeCreateSession(ctx) {
|
|
558
|
+
const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
|
|
559
|
+
const id = ctx.allocId();
|
|
560
|
+
const result = await ctx.sendRequest(id, "session/new", {
|
|
561
|
+
cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
562
|
+
mcpServers: servers
|
|
563
|
+
});
|
|
564
|
+
if (isJsonRpcError(result)) {
|
|
565
|
+
throw new ACPSessionError(
|
|
566
|
+
"session_create_failed",
|
|
567
|
+
`session/new failed: ${result.message}`,
|
|
568
|
+
result
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const sessionId = result.sessionId;
|
|
572
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
573
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
574
|
+
}
|
|
575
|
+
return sessionId;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/client/acp-session-auth.ts
|
|
579
|
+
async function authenticate(host, methodId) {
|
|
580
|
+
if (host.state === "closed") {
|
|
581
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
582
|
+
}
|
|
583
|
+
if (host.state !== "ready" && host.state !== "authenticated") {
|
|
584
|
+
throw new ACPSessionError(
|
|
585
|
+
"protocol_error",
|
|
586
|
+
`authenticate called in state=${host.state} (expected 'ready')`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (host.state === "authenticated") return;
|
|
590
|
+
if (!host.authMethods.some((m) => m.id === methodId)) {
|
|
591
|
+
throw new ACPSessionError(
|
|
592
|
+
"auth_failed",
|
|
593
|
+
`auth method "${methodId}" not in advertised methods: ${host.authMethods.map((m) => m.id).join(", ")}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (host.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
|
|
597
|
+
throw new ACPSessionError(
|
|
598
|
+
"auth_failed",
|
|
599
|
+
"Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const id = host.allocId();
|
|
603
|
+
const result = await host.sendRequest(id, "authenticate", { methodId });
|
|
604
|
+
if (isJsonRpcError(result)) {
|
|
605
|
+
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
606
|
+
}
|
|
607
|
+
host.state = "authenticated";
|
|
608
|
+
}
|
|
609
|
+
async function logout(host) {
|
|
610
|
+
if (host.state === "closed") {
|
|
611
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
612
|
+
}
|
|
613
|
+
if (!host.agentCapabilities.auth?.logout) {
|
|
614
|
+
throw new ACPSessionError(
|
|
615
|
+
"unsupported_capability",
|
|
616
|
+
"agent does not support logout (auth.logout capability not advertised)"
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
const id = host.allocId();
|
|
620
|
+
const result = await host.sendRequest(id, "logout", {});
|
|
621
|
+
if (isJsonRpcError(result)) {
|
|
622
|
+
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
623
|
+
}
|
|
624
|
+
host.state = "ready";
|
|
625
|
+
}
|
|
626
|
+
async function createSessionWithAuth(host) {
|
|
627
|
+
try {
|
|
628
|
+
return await executeCreateSession(host.opContext());
|
|
629
|
+
} catch (err) {
|
|
630
|
+
if (host.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
631
|
+
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
632
|
+
"session_create_failed",
|
|
633
|
+
err instanceof Error ? err.message : String(err),
|
|
634
|
+
err
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
await host.ensureAuthenticated();
|
|
638
|
+
return executeCreateSession(host.opContext());
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function ensureAuthenticated(host) {
|
|
642
|
+
if (host.state === "authenticated") return;
|
|
643
|
+
if (host.authMethods.length === 0) {
|
|
644
|
+
throw new ACPSessionError(
|
|
645
|
+
"auth_failed",
|
|
646
|
+
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const inProcess = host.authMethods.find(
|
|
650
|
+
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
651
|
+
);
|
|
652
|
+
if (inProcess) {
|
|
653
|
+
await host.authenticate(inProcess.id);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const terminal = host.authMethods.find((m) => m.type === "terminal");
|
|
657
|
+
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
658
|
+
const setup = setupArgs !== void 0 ? `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""} ${setupArgs}` : `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""}`;
|
|
659
|
+
throw new ACPSessionError(
|
|
660
|
+
"auth_failed",
|
|
661
|
+
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
338
665
|
// src/client/file-server.ts
|
|
339
666
|
import { randomBytes } from "node:crypto";
|
|
340
667
|
import { realpathSync } from "node:fs";
|
|
@@ -698,345 +1025,114 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
|
|
|
698
1025
|
default:
|
|
699
1026
|
await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
700
1027
|
}
|
|
701
|
-
} catch (err) {
|
|
702
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
703
|
-
await sender.sendErrorResponse(id, -32603, message);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
|
|
707
|
-
try {
|
|
708
|
-
const outcome = await runPermissionWithDeadline(
|
|
709
|
-
permissionPolicy,
|
|
710
|
-
{
|
|
711
|
-
toolCall: {
|
|
712
|
-
sessionUpdate: "tool_call_update",
|
|
713
|
-
toolCallId: partial.toolCallId,
|
|
714
|
-
title: partial.title,
|
|
715
|
-
kind: partial.kind,
|
|
716
|
-
status: "pending",
|
|
717
|
-
rawInput: partial.rawInput
|
|
718
|
-
},
|
|
719
|
-
options: [
|
|
720
|
-
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
721
|
-
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
722
|
-
]
|
|
723
|
-
},
|
|
724
|
-
callbackOptions
|
|
725
|
-
);
|
|
726
|
-
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
|
|
727
|
-
} catch (err) {
|
|
728
|
-
return isAbortLikeError(err) ? "cancelled" : "denied";
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
|
|
732
|
-
const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
|
|
733
|
-
const controller = new AbortController();
|
|
734
|
-
const abort = () => controller.abort();
|
|
735
|
-
const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
|
|
736
|
-
let removeAbort;
|
|
737
|
-
if (callbackOptions.signal) {
|
|
738
|
-
if (callbackOptions.signal.aborted) {
|
|
739
|
-
abort();
|
|
740
|
-
} else {
|
|
741
|
-
callbackOptions.signal.addEventListener("abort", abort, { once: true });
|
|
742
|
-
removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
return await Promise.race([
|
|
747
|
-
permissionPolicy({
|
|
748
|
-
toolCall: call.toolCall,
|
|
749
|
-
options: call.options,
|
|
750
|
-
signal: controller.signal
|
|
751
|
-
}),
|
|
752
|
-
rejectOnAbort(controller.signal)
|
|
753
|
-
]);
|
|
754
|
-
} finally {
|
|
755
|
-
if (timer !== null) clearTimeout(timer);
|
|
756
|
-
removeAbort?.();
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function rejectOnAbort(signal) {
|
|
760
|
-
return new Promise((_, reject) => {
|
|
761
|
-
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
762
|
-
if (signal.aborted) {
|
|
763
|
-
rejectAbort();
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
function isAbortLikeError(err) {
|
|
770
|
-
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
771
|
-
}
|
|
772
|
-
function resolvePermissionDeadline(value) {
|
|
773
|
-
if (value === Number.POSITIVE_INFINITY) return null;
|
|
774
|
-
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
775
|
-
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// src/client/acp-session-content.ts
|
|
779
|
-
function textContent(text) {
|
|
780
|
-
return { type: "text", text };
|
|
781
|
-
}
|
|
782
|
-
function imageContent(mimeType, data) {
|
|
783
|
-
return { type: "image", mimeType, data };
|
|
784
|
-
}
|
|
785
|
-
function audioContent(mimeType, data) {
|
|
786
|
-
return { type: "audio", mimeType, data };
|
|
787
|
-
}
|
|
788
|
-
function extractText(block) {
|
|
789
|
-
if (typeof block !== "object" || block === null) return "";
|
|
790
|
-
const b = block;
|
|
791
|
-
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
792
|
-
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
793
|
-
return b.resource.text;
|
|
794
|
-
}
|
|
795
|
-
return "";
|
|
796
|
-
}
|
|
797
|
-
function isRecord(v) {
|
|
798
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
799
|
-
}
|
|
800
|
-
function emptyRunResult(stopReason) {
|
|
801
|
-
return {
|
|
802
|
-
text: "",
|
|
803
|
-
stopReason,
|
|
804
|
-
hasText: false,
|
|
805
|
-
toolCalls: [],
|
|
806
|
-
diffs: [],
|
|
807
|
-
thoughts: ""
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// src/client/acp-session-errors.ts
|
|
812
|
-
var ACPSessionError = class extends Error {
|
|
813
|
-
kind;
|
|
814
|
-
cause;
|
|
815
|
-
constructor(kind, message, cause) {
|
|
816
|
-
super(message);
|
|
817
|
-
this.name = "ACPSessionError";
|
|
818
|
-
this.kind = kind;
|
|
819
|
-
this.cause = cause;
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
function isJsonRpcError(v) {
|
|
823
|
-
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
824
|
-
}
|
|
825
|
-
function isAuthRequiredError(err) {
|
|
826
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
827
|
-
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
828
|
-
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
829
|
-
if (!cause || typeof cause !== "object") return false;
|
|
830
|
-
const data = cause.data;
|
|
831
|
-
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
832
|
-
if (data && typeof data === "object") {
|
|
833
|
-
const d = data;
|
|
834
|
-
if (d.authRequired === true) return true;
|
|
835
|
-
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
836
|
-
}
|
|
837
|
-
return false;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/client/acp-session-ops.ts
|
|
841
|
-
function filterMcpServers(agentCapabilities, servers) {
|
|
842
|
-
if (!servers || servers.length === 0) return [];
|
|
843
|
-
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
844
|
-
return servers.filter((s) => {
|
|
845
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
846
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
847
|
-
return true;
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
851
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
852
|
-
if (!ctx.agentCapabilities.loadSession) {
|
|
853
|
-
throw new ACPSessionError(
|
|
854
|
-
"unsupported_capability",
|
|
855
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
859
|
-
await ctx.closeSession();
|
|
860
|
-
}
|
|
861
|
-
ctx.resetScratch();
|
|
862
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
863
|
-
const id = ctx.allocId();
|
|
864
|
-
const result = await ctx.sendRequest(id, "session/load", {
|
|
865
|
-
sessionId,
|
|
866
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
867
|
-
mcpServers: servers
|
|
868
|
-
});
|
|
869
|
-
if (isJsonRpcError(result)) {
|
|
870
|
-
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
871
|
-
}
|
|
872
|
-
ctx.setSessionId(sessionId);
|
|
873
|
-
}
|
|
874
|
-
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
875
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
876
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
877
|
-
throw new ACPSessionError(
|
|
878
|
-
"unsupported_capability",
|
|
879
|
-
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
883
|
-
await ctx.closeSession();
|
|
884
|
-
}
|
|
885
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
886
|
-
const id = ctx.allocId();
|
|
887
|
-
const result = await ctx.sendRequest(id, "session/resume", {
|
|
888
|
-
sessionId,
|
|
889
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
890
|
-
mcpServers: servers
|
|
891
|
-
});
|
|
892
|
-
if (isJsonRpcError(result)) {
|
|
893
|
-
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
894
|
-
}
|
|
895
|
-
ctx.setSessionId(sessionId);
|
|
896
|
-
}
|
|
897
|
-
async function executeListSessions(ctx, cursor, cwd) {
|
|
898
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
899
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
900
|
-
throw new ACPSessionError(
|
|
901
|
-
"unsupported_capability",
|
|
902
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
const id = ctx.allocId();
|
|
906
|
-
const params = {};
|
|
907
|
-
if (cursor !== void 0) params.cursor = cursor;
|
|
908
|
-
if (cwd !== void 0) params.cwd = cwd;
|
|
909
|
-
const result = await ctx.sendRequest(id, "session/list", params);
|
|
910
|
-
if (isJsonRpcError(result)) {
|
|
911
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1030
|
+
await sender.sendErrorResponse(id, -32603, message);
|
|
912
1031
|
}
|
|
913
|
-
const r = result;
|
|
914
|
-
return {
|
|
915
|
-
sessions: r.sessions ?? [],
|
|
916
|
-
nextCursor: r.nextCursor
|
|
917
|
-
};
|
|
918
1032
|
}
|
|
919
|
-
async function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1033
|
+
async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
|
|
1034
|
+
try {
|
|
1035
|
+
const outcome = await runPermissionWithDeadline(
|
|
1036
|
+
permissionPolicy,
|
|
1037
|
+
{
|
|
1038
|
+
toolCall: {
|
|
1039
|
+
sessionUpdate: "tool_call_update",
|
|
1040
|
+
toolCallId: partial.toolCallId,
|
|
1041
|
+
title: partial.title,
|
|
1042
|
+
kind: partial.kind,
|
|
1043
|
+
status: "pending",
|
|
1044
|
+
rawInput: partial.rawInput
|
|
1045
|
+
},
|
|
1046
|
+
options: [
|
|
1047
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
1048
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
1049
|
+
]
|
|
1050
|
+
},
|
|
1051
|
+
callbackOptions
|
|
925
1052
|
);
|
|
1053
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
return isAbortLikeError(err) ? "cancelled" : "denied";
|
|
926
1056
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1057
|
+
}
|
|
1058
|
+
async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
|
|
1059
|
+
const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
|
|
1060
|
+
const controller = new AbortController();
|
|
1061
|
+
const abort = () => controller.abort();
|
|
1062
|
+
const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
|
|
1063
|
+
let removeAbort;
|
|
1064
|
+
if (callbackOptions.signal) {
|
|
1065
|
+
if (callbackOptions.signal.aborted) {
|
|
1066
|
+
abort();
|
|
1067
|
+
} else {
|
|
1068
|
+
callbackOptions.signal.addEventListener("abort", abort, { once: true });
|
|
1069
|
+
removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
|
|
1070
|
+
}
|
|
931
1071
|
}
|
|
932
|
-
|
|
933
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
return await Promise.race([
|
|
1074
|
+
permissionPolicy({
|
|
1075
|
+
toolCall: call.toolCall,
|
|
1076
|
+
options: call.options,
|
|
1077
|
+
signal: controller.signal
|
|
1078
|
+
}),
|
|
1079
|
+
rejectOnAbort(controller.signal)
|
|
1080
|
+
]);
|
|
1081
|
+
} finally {
|
|
1082
|
+
if (timer !== null) clearTimeout(timer);
|
|
1083
|
+
removeAbort?.();
|
|
934
1084
|
}
|
|
935
1085
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1086
|
+
function rejectOnAbort(signal) {
|
|
1087
|
+
return new Promise((_, reject) => {
|
|
1088
|
+
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
1089
|
+
if (signal.aborted) {
|
|
1090
|
+
rejectAbort();
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
944
1094
|
});
|
|
945
|
-
if (isJsonRpcError(result)) {
|
|
946
|
-
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
947
|
-
}
|
|
948
|
-
const newId = result.sessionId;
|
|
949
|
-
if (typeof newId !== "string" || !newId) {
|
|
950
|
-
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
951
|
-
}
|
|
952
|
-
return newId;
|
|
953
1095
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const id = ctx.allocId();
|
|
957
|
-
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
958
|
-
if (isJsonRpcError(result)) {
|
|
959
|
-
throw new ACPSessionError(
|
|
960
|
-
"prompt_failed",
|
|
961
|
-
`session/set_mode failed: ${result.message}`,
|
|
962
|
-
result
|
|
963
|
-
);
|
|
964
|
-
}
|
|
1096
|
+
function isAbortLikeError(err) {
|
|
1097
|
+
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
965
1098
|
}
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
sessionId,
|
|
971
|
-
configId,
|
|
972
|
-
value
|
|
973
|
-
});
|
|
974
|
-
if (isJsonRpcError(result)) {
|
|
975
|
-
throw new ACPSessionError(
|
|
976
|
-
"prompt_failed",
|
|
977
|
-
`session/set_config_option failed: ${result.message}`,
|
|
978
|
-
result
|
|
979
|
-
);
|
|
980
|
-
}
|
|
1099
|
+
function resolvePermissionDeadline(value) {
|
|
1100
|
+
if (value === Number.POSITIVE_INFINITY) return null;
|
|
1101
|
+
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
1102
|
+
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
981
1103
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (isJsonRpcError(result)) {
|
|
987
|
-
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
988
|
-
}
|
|
989
|
-
const r = result;
|
|
990
|
-
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
1104
|
+
|
|
1105
|
+
// src/client/acp-session-content.ts
|
|
1106
|
+
function textContent(text) {
|
|
1107
|
+
return { type: "text", text };
|
|
991
1108
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const id = ctx.allocId();
|
|
995
|
-
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
996
|
-
if (isJsonRpcError(result)) {
|
|
997
|
-
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
998
|
-
}
|
|
1109
|
+
function imageContent(mimeType, data) {
|
|
1110
|
+
return { type: "image", mimeType, data };
|
|
999
1111
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const id = ctx.allocId();
|
|
1003
|
-
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
1004
|
-
if (isJsonRpcError(result)) {
|
|
1005
|
-
throw new ACPSessionError(
|
|
1006
|
-
"prompt_failed",
|
|
1007
|
-
`providers/disable failed: ${result.message}`,
|
|
1008
|
-
result
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1112
|
+
function audioContent(mimeType, data) {
|
|
1113
|
+
return { type: "audio", mimeType, data };
|
|
1011
1114
|
}
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1115
|
+
function extractText(block) {
|
|
1116
|
+
if (typeof block !== "object" || block === null) return "";
|
|
1117
|
+
const b = block;
|
|
1118
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1119
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1120
|
+
return b.resource.text;
|
|
1018
1121
|
}
|
|
1019
|
-
return
|
|
1122
|
+
return "";
|
|
1020
1123
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
const sessionId = result.sessionId;
|
|
1036
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1037
|
-
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
1038
|
-
}
|
|
1039
|
-
return sessionId;
|
|
1124
|
+
function isRecord(v) {
|
|
1125
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1126
|
+
}
|
|
1127
|
+
function emptyRunResult(stopReason) {
|
|
1128
|
+
return {
|
|
1129
|
+
text: "",
|
|
1130
|
+
stopReason,
|
|
1131
|
+
hasText: false,
|
|
1132
|
+
toolCalls: [],
|
|
1133
|
+
diffs: [],
|
|
1134
|
+
thoughts: ""
|
|
1135
|
+
};
|
|
1040
1136
|
}
|
|
1041
1137
|
|
|
1042
1138
|
// src/client/acp-session-updates.ts
|
|
@@ -1176,11 +1272,30 @@ function makePermissionPolicy(decide) {
|
|
|
1176
1272
|
// src/client/terminal-server.ts
|
|
1177
1273
|
import { spawn } from "node:child_process";
|
|
1178
1274
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1179
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1275
|
+
import { accessSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1180
1276
|
import * as path2 from "node:path";
|
|
1181
1277
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1182
1278
|
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1183
1279
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1280
|
+
function win32BatchTarget(command) {
|
|
1281
|
+
if (/\.(?:cmd|bat)$/i.test(command)) return command;
|
|
1282
|
+
if (/[\\/]/.test(command) || path2.extname(command)) return void 0;
|
|
1283
|
+
const exts = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").toLowerCase().split(";");
|
|
1284
|
+
for (const dir of (process.env["PATH"] ?? "").split(path2.delimiter)) {
|
|
1285
|
+
if (!dir) continue;
|
|
1286
|
+
for (const ext of exts) {
|
|
1287
|
+
if (!ext) continue;
|
|
1288
|
+
const full = path2.join(dir, `${command}${ext}`);
|
|
1289
|
+
try {
|
|
1290
|
+
accessSync(full);
|
|
1291
|
+
} catch {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
return ext === ".cmd" || ext === ".bat" ? full : void 0;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return void 0;
|
|
1298
|
+
}
|
|
1184
1299
|
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
1185
1300
|
var TerminalServer = class {
|
|
1186
1301
|
terminals = /* @__PURE__ */ new Map();
|
|
@@ -1231,11 +1346,14 @@ var TerminalServer = class {
|
|
|
1231
1346
|
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1232
1347
|
this.maxOutputByteLimit
|
|
1233
1348
|
);
|
|
1234
|
-
const
|
|
1349
|
+
const batch = process.platform === "win32" ? win32BatchTarget(params.command) : void 0;
|
|
1350
|
+
const shim = batch ? buildWin32CmdShimInvocation(batch, params.args ?? []) : void 0;
|
|
1351
|
+
const proc = spawn(shim?.command ?? params.command, shim?.args ?? params.args ?? [], {
|
|
1235
1352
|
cwd,
|
|
1236
1353
|
env: this.buildEnv(params.env),
|
|
1237
1354
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1238
|
-
windowsHide: true
|
|
1355
|
+
windowsHide: true,
|
|
1356
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
1239
1357
|
// shell: false on purpose. The terminal server is invoked with
|
|
1240
1358
|
// the agent's explicit argv; turning on shell-mode would make
|
|
1241
1359
|
// the command a single shell-parsed string, which breaks
|
|
@@ -1912,55 +2030,14 @@ var ACPSession = class _ACPSession {
|
|
|
1912
2030
|
* Call this AFTER start() and BEFORE any session/new call.
|
|
1913
2031
|
*/
|
|
1914
2032
|
async authenticate(methodId) {
|
|
1915
|
-
|
|
1916
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1917
|
-
}
|
|
1918
|
-
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
1919
|
-
throw new ACPSessionError(
|
|
1920
|
-
"protocol_error",
|
|
1921
|
-
`authenticate called in state=${this.state} (expected 'ready')`
|
|
1922
|
-
);
|
|
1923
|
-
}
|
|
1924
|
-
if (this.state === "authenticated") return;
|
|
1925
|
-
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
1926
|
-
throw new ACPSessionError(
|
|
1927
|
-
"auth_failed",
|
|
1928
|
-
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
if (this.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
|
|
1932
|
-
throw new ACPSessionError(
|
|
1933
|
-
"auth_failed",
|
|
1934
|
-
"Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
const id = this.allocId();
|
|
1938
|
-
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
1939
|
-
if (isJsonRpcError(result)) {
|
|
1940
|
-
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
1941
|
-
}
|
|
1942
|
-
this.state = "authenticated";
|
|
2033
|
+
return authenticate(this.acpSessionAuthHost(), methodId);
|
|
1943
2034
|
}
|
|
1944
2035
|
/**
|
|
1945
2036
|
* Log out from the current authenticated session.
|
|
1946
2037
|
* Only callable if the agent advertises `auth.logout` capability.
|
|
1947
2038
|
*/
|
|
1948
2039
|
async logout() {
|
|
1949
|
-
|
|
1950
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1951
|
-
}
|
|
1952
|
-
if (!this.agentCapabilities.auth?.logout) {
|
|
1953
|
-
throw new ACPSessionError(
|
|
1954
|
-
"unsupported_capability",
|
|
1955
|
-
"agent does not support logout (auth.logout capability not advertised)"
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
const id = this.allocId();
|
|
1959
|
-
const result = await this.sendRequest(id, "logout", {});
|
|
1960
|
-
if (isJsonRpcError(result)) {
|
|
1961
|
-
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
1962
|
-
}
|
|
1963
|
-
this.state = "ready";
|
|
2040
|
+
return logout(this.acpSessionAuthHost());
|
|
1964
2041
|
}
|
|
1965
2042
|
// ──────────────────────────────────────────────────────────────────────
|
|
1966
2043
|
// Session management delegation
|
|
@@ -2258,19 +2335,7 @@ var ACPSession = class _ACPSession {
|
|
|
2258
2335
|
* `authMethods`; we do not pop OAuth on every spawn.
|
|
2259
2336
|
*/
|
|
2260
2337
|
async createSessionWithAuth() {
|
|
2261
|
-
|
|
2262
|
-
return await executeCreateSession(this.opContext());
|
|
2263
|
-
} catch (err) {
|
|
2264
|
-
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
2265
|
-
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
2266
|
-
"session_create_failed",
|
|
2267
|
-
err instanceof Error ? err.message : String(err),
|
|
2268
|
-
err
|
|
2269
|
-
);
|
|
2270
|
-
}
|
|
2271
|
-
await this.ensureAuthenticated();
|
|
2272
|
-
return executeCreateSession(this.opContext());
|
|
2273
|
-
}
|
|
2338
|
+
return createSessionWithAuth(this.acpSessionAuthHost());
|
|
2274
2339
|
}
|
|
2275
2340
|
/**
|
|
2276
2341
|
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
@@ -2278,27 +2343,7 @@ var ACPSession = class _ACPSession {
|
|
|
2278
2343
|
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
2279
2344
|
*/
|
|
2280
2345
|
async ensureAuthenticated() {
|
|
2281
|
-
|
|
2282
|
-
if (this.authMethods.length === 0) {
|
|
2283
|
-
throw new ACPSessionError(
|
|
2284
|
-
"auth_failed",
|
|
2285
|
-
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
2286
|
-
);
|
|
2287
|
-
}
|
|
2288
|
-
const inProcess = this.authMethods.find(
|
|
2289
|
-
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
2290
|
-
);
|
|
2291
|
-
if (inProcess) {
|
|
2292
|
-
await this.authenticate(inProcess.id);
|
|
2293
|
-
return;
|
|
2294
|
-
}
|
|
2295
|
-
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
2296
|
-
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
2297
|
-
const setup = setupArgs !== void 0 ? `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
2298
|
-
throw new ACPSessionError(
|
|
2299
|
-
"auth_failed",
|
|
2300
|
-
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
2301
|
-
);
|
|
2346
|
+
return ensureAuthenticated(this.acpSessionAuthHost());
|
|
2302
2347
|
}
|
|
2303
2348
|
sendResult(id, result) {
|
|
2304
2349
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
@@ -2414,6 +2459,29 @@ var ACPSession = class _ACPSession {
|
|
|
2414
2459
|
resetScratch() {
|
|
2415
2460
|
this.scratch = createSessionScratch();
|
|
2416
2461
|
}
|
|
2462
|
+
acpSessionAuthHost() {
|
|
2463
|
+
const self = this;
|
|
2464
|
+
return {
|
|
2465
|
+
get state() {
|
|
2466
|
+
return self.state;
|
|
2467
|
+
},
|
|
2468
|
+
set state(value) {
|
|
2469
|
+
self.state = value;
|
|
2470
|
+
},
|
|
2471
|
+
get authMethods() {
|
|
2472
|
+
return self.authMethods;
|
|
2473
|
+
},
|
|
2474
|
+
allocId: (...args) => this.allocId(...args),
|
|
2475
|
+
sendRequest: (...args) => this.sendRequest(...args),
|
|
2476
|
+
get agentCapabilities() {
|
|
2477
|
+
return self.agentCapabilities;
|
|
2478
|
+
},
|
|
2479
|
+
opContext: (...args) => this.opContext(...args),
|
|
2480
|
+
ensureAuthenticated: (...args) => this.ensureAuthenticated(...args),
|
|
2481
|
+
authenticate: (...args) => this.authenticate(...args),
|
|
2482
|
+
opts: this.opts
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2417
2485
|
};
|
|
2418
2486
|
|
|
2419
2487
|
// src/integration/acp-subagent-runner.ts
|