@agentrq/acp-gateway 0.2.4 → 0.2.6
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/README.md +3 -1
- package/dist/__tests__/acpClient.test.js +635 -106
- package/dist/__tests__/acpClient.test.js.map +1 -1
- package/dist/__tests__/agentInfo.test.js +79 -0
- package/dist/__tests__/agentInfo.test.js.map +1 -0
- package/dist/__tests__/config.test.js +45 -0
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/index.test.js +283 -15
- package/dist/__tests__/index.test.js.map +1 -1
- package/dist/__tests__/mcpClient.test.js +94 -1
- package/dist/__tests__/mcpClient.test.js.map +1 -1
- package/dist/__tests__/telemetry.test.js +151 -0
- package/dist/__tests__/telemetry.test.js.map +1 -0
- package/dist/acpClient.js +380 -59
- package/dist/acpClient.js.map +1 -1
- package/dist/agentInfo.js +69 -0
- package/dist/agentInfo.js.map +1 -0
- package/dist/config.js +50 -16
- package/dist/config.js.map +1 -1
- package/dist/index.js +259 -19
- package/dist/index.js.map +1 -1
- package/dist/mcpClient.js +37 -1
- package/dist/mcpClient.js.map +1 -1
- package/dist/telemetry.js +106 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentInfo.js","sourceRoot":"","sources":["../src/agentInfo.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
/** The MCP transports ACP defines that this gateway knows how to hand over. */
|
|
10
|
+
export const MCP_TRANSPORTS = ["http", "sse", "stdio"];
|
|
9
11
|
/**
|
|
10
12
|
* Find and parse the .mcp.json config file.
|
|
11
13
|
* Searches CWD and then up to 3 parent directories.
|
|
@@ -18,29 +20,61 @@ export function loadMcpConfig(startDir = process.cwd()) {
|
|
|
18
20
|
resolve(startDir, "..", "..", "..", ".mcp.json"),
|
|
19
21
|
];
|
|
20
22
|
for (const candidate of candidates) {
|
|
23
|
+
// Only reading and parsing may fall through to the next candidate. What the
|
|
24
|
+
// file *says* is a separate matter: once a .mcp.json has been found, a
|
|
25
|
+
// mistake inside it is reported rather than hidden behind "no config found".
|
|
26
|
+
let parsed;
|
|
21
27
|
try {
|
|
22
|
-
|
|
23
|
-
const parsed = JSON.parse(raw);
|
|
24
|
-
const servers = Object.entries(parsed.mcpServers ?? {}).map(([name, cfg]) => ({
|
|
25
|
-
name,
|
|
26
|
-
type: cfg.type ?? (cfg.url ? "http" : "stdio"),
|
|
27
|
-
url: cfg.url,
|
|
28
|
-
command: cfg.command,
|
|
29
|
-
args: cfg.args,
|
|
30
|
-
env: cfg.env,
|
|
31
|
-
headers: cfg.headers,
|
|
32
|
-
}));
|
|
33
|
-
if (servers.length > 0) {
|
|
34
|
-
console.error(`[config] Loaded .mcp.json from ${candidate}`);
|
|
35
|
-
return servers;
|
|
36
|
-
}
|
|
28
|
+
parsed = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
37
29
|
}
|
|
38
30
|
catch {
|
|
39
|
-
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const servers = Object.entries(parsed.mcpServers ?? {}).map(([name, cfg]) => ({
|
|
34
|
+
name,
|
|
35
|
+
type: readTransport(name, cfg.type, cfg.url),
|
|
36
|
+
url: cfg.url,
|
|
37
|
+
command: cfg.command,
|
|
38
|
+
args: cfg.args,
|
|
39
|
+
env: cfg.env,
|
|
40
|
+
headers: cfg.headers,
|
|
41
|
+
}));
|
|
42
|
+
servers.forEach(assertUsable);
|
|
43
|
+
if (servers.length > 0) {
|
|
44
|
+
console.error(`[config] Loaded .mcp.json from ${candidate}`);
|
|
45
|
+
return servers;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
throw new Error("Could not find .mcp.json — run acp-gateway from your workspace root");
|
|
43
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Reads an entry's transport, defaulting the way MCP clients conventionally do.
|
|
52
|
+
*
|
|
53
|
+
* An unknown transport is refused here rather than quietly treated as stdio,
|
|
54
|
+
* which produced a server with no command and an agent that could not say why.
|
|
55
|
+
*/
|
|
56
|
+
function readTransport(name, type, url) {
|
|
57
|
+
if (type === undefined)
|
|
58
|
+
return url ? "http" : "stdio";
|
|
59
|
+
if (MCP_TRANSPORTS.includes(type))
|
|
60
|
+
return type;
|
|
61
|
+
throw new Error(`MCP server "${name}" in .mcp.json has transport "${type}", which this gateway ` +
|
|
62
|
+
`cannot hand to an agent. Use one of: ${MCP_TRANSPORTS.join(", ")}.`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Refuses an entry the agent could never connect to.
|
|
66
|
+
*
|
|
67
|
+
* The gateway passes these straight through to the agent, so an entry missing
|
|
68
|
+
* the one field its transport needs fails inside the agent, long after the
|
|
69
|
+
* mistake was made and with nothing to point at.
|
|
70
|
+
*/
|
|
71
|
+
function assertUsable(server) {
|
|
72
|
+
const missing = server.type === "stdio" ? !server.command : !server.url;
|
|
73
|
+
if (!missing)
|
|
74
|
+
return;
|
|
75
|
+
const field = server.type === "stdio" ? "command" : "url";
|
|
76
|
+
throw new Error(`MCP server "${server.name}" in .mcp.json is ${server.type} but has no ${field}.`);
|
|
77
|
+
}
|
|
44
78
|
/**
|
|
45
79
|
* Pick the primary agentrq MCP server from the list.
|
|
46
80
|
* Prefers servers with "agentrq" in the name; falls back to the first HTTP server.
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -11,19 +11,45 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import * as acp from "@agentclientprotocol/sdk";
|
|
13
13
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
14
|
-
import { loadMcpConfig, pickAgentrqServer } from "./config.js";
|
|
14
|
+
import { loadMcpConfig, pickAgentrqServer, } from "./config.js";
|
|
15
15
|
import { MCPBridge } from "./mcpClient.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
/**
|
|
17
|
+
* What the agent said about a transport, if anything.
|
|
18
|
+
*
|
|
19
|
+
* stdio is the one transport every agent must support. For the others the
|
|
20
|
+
* answer is only trustworthy when the agent actually stated it: an agent that
|
|
21
|
+
* advertises no MCP capabilities at all is far more likely to be terse than to
|
|
22
|
+
* be unable to reach an HTTP server, and dropping its servers on that reading
|
|
23
|
+
* would take the workspace's own MCP server away from it.
|
|
24
|
+
*/
|
|
25
|
+
function transportSupport(transport, agentCapabilities) {
|
|
26
|
+
if (transport === "stdio")
|
|
27
|
+
return "required";
|
|
28
|
+
const declared = agentCapabilities?.mcpCapabilities?.[transport];
|
|
29
|
+
if (declared === true)
|
|
30
|
+
return "declared";
|
|
31
|
+
if (declared === false)
|
|
32
|
+
return "refused";
|
|
33
|
+
return "unstated";
|
|
34
|
+
}
|
|
35
|
+
export function mapMcpServers(configs, agentCapabilities) {
|
|
36
|
+
return configs
|
|
37
|
+
.filter((cfg) => {
|
|
38
|
+
const support = transportSupport(cfg.type, agentCapabilities);
|
|
39
|
+
if (support === "refused") {
|
|
40
|
+
console.error(`[acp] ⚠️ Not passing MCP server "${cfg.name}" to the agent: it is ${cfg.type}, ` +
|
|
41
|
+
`and the agent says it does not support that transport. Passing it anyway ` +
|
|
42
|
+
`risks the agent refusing the whole session.`);
|
|
43
|
+
return false;
|
|
25
44
|
}
|
|
26
|
-
|
|
45
|
+
if (support === "unstated") {
|
|
46
|
+
console.error(`[acp] MCP server "${cfg.name}" is ${cfg.type}, which the agent does not ` +
|
|
47
|
+
`advertise either way — passing it and letting the agent decide.`);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
})
|
|
51
|
+
.map((cfg) => {
|
|
52
|
+
if (cfg.type === "stdio") {
|
|
27
53
|
return {
|
|
28
54
|
name: cfg.name,
|
|
29
55
|
command: cfg.command,
|
|
@@ -31,17 +57,61 @@ export function mapMcpServers(configs) {
|
|
|
31
57
|
env: Object.entries(cfg.env || {}).map(([name, value]) => ({ name, value })),
|
|
32
58
|
};
|
|
33
59
|
}
|
|
60
|
+
return {
|
|
61
|
+
type: cfg.type,
|
|
62
|
+
name: cfg.name,
|
|
63
|
+
url: cfg.url,
|
|
64
|
+
headers: Object.entries(cfg.headers || {}).map(([name, value]) => ({ name, value })),
|
|
65
|
+
};
|
|
34
66
|
});
|
|
35
67
|
}
|
|
36
|
-
import { AgentRQACPClient } from "./acpClient.js";
|
|
68
|
+
import { AgentRQACPClient, DEFAULT_PERMISSION_TIMEOUT_MS } from "./acpClient.js";
|
|
37
69
|
import { describeAuthMethods, isAuthRequiredError, login, logout, supportsLogout, } from "./auth.js";
|
|
38
70
|
import { resolveAgentLaunch } from "./agentInstall.js";
|
|
71
|
+
import { describeAgentInfo } from "./agentInfo.js";
|
|
39
72
|
import { describeAgents, fetchRegistry, hostPlatformTarget, } from "./registry.js";
|
|
40
73
|
import { extractTaskIdFromMeta, extractTaskIdFromText, } from "./taskIdentity.js";
|
|
41
74
|
const lastTaskContent = new Map();
|
|
75
|
+
// Cancellations are remembered by *where they fall in the stream of events*
|
|
76
|
+
// rather than as a one-shot flag. agentrq reuses a chat's id as the task id, so
|
|
77
|
+
// a flag that outlived the cancellation it describes would swallow the user's
|
|
78
|
+
// next message on that chat; ordered this way, a task is skipped only when the
|
|
79
|
+
// cancel arrived after it was queued. Wall-clock time is too coarse to order
|
|
80
|
+
// two events in the same millisecond, so this is a plain counter.
|
|
81
|
+
let taskSeq = 0;
|
|
82
|
+
export const nextTaskSeq = () => ++taskSeq;
|
|
83
|
+
export const cancelledTaskSeq = new Map();
|
|
84
|
+
// A cancel for a task that never runs (a stale id, a task that already
|
|
85
|
+
// finished) leaves an entry behind, so keep the map from growing without bound.
|
|
86
|
+
const MAX_REMEMBERED_CANCELLATIONS = 200;
|
|
87
|
+
/** Records that `taskId` has just been cancelled. */
|
|
88
|
+
export function markTaskCancelled(taskId) {
|
|
89
|
+
// Re-inserting moves the id to the end, so eviction stays oldest-first.
|
|
90
|
+
cancelledTaskSeq.delete(taskId);
|
|
91
|
+
cancelledTaskSeq.set(taskId, nextTaskSeq());
|
|
92
|
+
while (cancelledTaskSeq.size > MAX_REMEMBERED_CANCELLATIONS) {
|
|
93
|
+
const oldest = cancelledTaskSeq.keys().next().value;
|
|
94
|
+
cancelledTaskSeq.delete(oldest);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether a task queued at `queuedSeq` has since been cancelled.
|
|
99
|
+
*
|
|
100
|
+
* Two notifications for the same task id can be queued at once, so the check is
|
|
101
|
+
* against the point each was queued: cancelling the older one must not stop the
|
|
102
|
+
* newer one from running.
|
|
103
|
+
*/
|
|
104
|
+
export function isTaskCancelled(taskId, queuedSeq) {
|
|
105
|
+
if (!taskId)
|
|
106
|
+
return false;
|
|
107
|
+
const seq = cancelledTaskSeq.get(taskId);
|
|
108
|
+
return seq !== undefined && seq > queuedSeq;
|
|
109
|
+
}
|
|
42
110
|
export const activeSessions = new Map();
|
|
43
111
|
/** Login preferences taken from the CLI, consulted whenever an agent demands auth. */
|
|
44
112
|
export const authConfig = {};
|
|
113
|
+
/** How long tool calls wait for a human, taken from the CLI at startup. */
|
|
114
|
+
export const permissionConfig = {};
|
|
45
115
|
/**
|
|
46
116
|
* Whether a human is sitting in front of this process.
|
|
47
117
|
*
|
|
@@ -64,15 +134,22 @@ export async function openAgentConnection({ acpCmdArgs, mcpBridge, env, label, t
|
|
|
64
134
|
stdio: ["pipe", "pipe", "inherit"],
|
|
65
135
|
env: { ...process.env, ...env },
|
|
66
136
|
});
|
|
137
|
+
const acpClient = new AgentRQACPClient(mcpBridge, () => taskId, {
|
|
138
|
+
permissionTimeoutMs: permissionConfig.timeoutMs,
|
|
139
|
+
});
|
|
67
140
|
// Guard against unhandled child-process failures. Without these listeners a
|
|
68
141
|
// crashed agent (e.g. on network loss) leaves a broken stdin pipe; the next
|
|
69
142
|
// write raises EPIPE as an uncaught error and takes the gateway down with it.
|
|
70
143
|
agentProcess.on("error", (err) => {
|
|
71
144
|
console.error(`[acp] Agent process error for ${label}:`, err.message);
|
|
145
|
+
acpClient.cancelPendingPermissions(`agent process for ${label} failed`);
|
|
72
146
|
onExit?.();
|
|
73
147
|
});
|
|
74
148
|
agentProcess.on("exit", (code, signal) => {
|
|
75
149
|
console.error(`[acp] Agent process for ${label} exited (code=${code}, signal=${signal})`);
|
|
150
|
+
// Nothing will act on these answers now, but the tool calls waiting on them
|
|
151
|
+
// are holding task-queue slots that would never be given back.
|
|
152
|
+
acpClient.cancelPendingPermissions(`agent process for ${label} exited`);
|
|
76
153
|
onExit?.();
|
|
77
154
|
});
|
|
78
155
|
// stdin can emit EPIPE when the child dies mid-write; swallow it so it
|
|
@@ -82,9 +159,11 @@ export async function openAgentConnection({ acpCmdArgs, mcpBridge, env, label, t
|
|
|
82
159
|
});
|
|
83
160
|
const input = Writable.toWeb(agentProcess.stdin);
|
|
84
161
|
const output = Readable.toWeb(agentProcess.stdout);
|
|
85
|
-
const acpClient = new AgentRQACPClient(mcpBridge, () => taskId);
|
|
86
162
|
const stream = acp.ndJsonStream(input, output);
|
|
87
163
|
const connection = new acp.ClientSideConnection((_agent) => acpClient, stream);
|
|
164
|
+
// Stopping a turn is only possible once the connection exists, and the
|
|
165
|
+
// connection is built around the client — so it is handed over afterwards.
|
|
166
|
+
acpClient.setSessionCanceller((sessionId) => connection.cancel({ sessionId }));
|
|
88
167
|
const initResult = await connection.initialize({
|
|
89
168
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
90
169
|
clientCapabilities: {
|
|
@@ -144,7 +223,7 @@ export async function getOrCreateSession(taskId, acpCmdArgs, configs, agentrqCon
|
|
|
144
223
|
});
|
|
145
224
|
const newSessionParams = {
|
|
146
225
|
cwd: process.cwd(),
|
|
147
|
-
mcpServers: mapMcpServers(configs),
|
|
226
|
+
mcpServers: mapMcpServers(configs, initResult.agentCapabilities),
|
|
148
227
|
};
|
|
149
228
|
const sessionResult = await createSessionWithAuth(connection, newSessionParams, {
|
|
150
229
|
methods: initResult.authMethods,
|
|
@@ -154,6 +233,9 @@ export async function getOrCreateSession(taskId, acpCmdArgs, configs, agentrqCon
|
|
|
154
233
|
});
|
|
155
234
|
console.error(`[acp] Created session ${sessionResult.sessionId} for task ${key}`);
|
|
156
235
|
await enforceHumanApprovalMode(connection, sessionResult);
|
|
236
|
+
// The mode is pinned once here, but agents may move themselves back out of
|
|
237
|
+
// it, so keep watching for the rest of the session's life.
|
|
238
|
+
acpClient.setModeChangeHandler((changedSessionId, modeId) => handleAgentModeChange(connection, changedSessionId, modeId, sessionResult.modes));
|
|
157
239
|
const sessionInfo = {
|
|
158
240
|
process: agentProcess,
|
|
159
241
|
connection,
|
|
@@ -164,6 +246,61 @@ export async function getOrCreateSession(taskId, acpCmdArgs, configs, agentrqCon
|
|
|
164
246
|
activeSessions.set(key, sessionInfo);
|
|
165
247
|
return sessionInfo;
|
|
166
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Finds the active agent session for a given task ID (or returns the single active session if none specified).
|
|
251
|
+
*/
|
|
252
|
+
export function findActiveSession(taskId) {
|
|
253
|
+
if (taskId) {
|
|
254
|
+
const direct = activeSessions.get(taskId);
|
|
255
|
+
if (direct)
|
|
256
|
+
return direct;
|
|
257
|
+
for (const session of activeSessions.values()) {
|
|
258
|
+
if (session.sessionId === taskId)
|
|
259
|
+
return session;
|
|
260
|
+
}
|
|
261
|
+
// A session opened from a notification that carried no task id is keyed
|
|
262
|
+
// "default"; when it is the only one running, an id-carrying cancel can
|
|
263
|
+
// only have meant it. Never fall back to a session keyed under a
|
|
264
|
+
// *different* task id — that would abort an unrelated task.
|
|
265
|
+
const untracked = activeSessions.get("default");
|
|
266
|
+
if (untracked && activeSessions.size === 1) {
|
|
267
|
+
return untracked;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (activeSessions.size === 1) {
|
|
271
|
+
return activeSessions.values().next().value;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Handles task cancellation events from the MCP server.
|
|
277
|
+
* Cancels the ACP session/turn and immediately cancels any pending permissions.
|
|
278
|
+
*/
|
|
279
|
+
export async function handleTaskCancellation(taskId, reason) {
|
|
280
|
+
if (taskId) {
|
|
281
|
+
markTaskCancelled(taskId);
|
|
282
|
+
const session = findActiveSession(taskId);
|
|
283
|
+
if (!session) {
|
|
284
|
+
console.error(`[bridge] Received cancellation for task ${taskId}, but no active session found (queued tasks will be skipped)`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
console.error(`[bridge] Cancelling session ${session.sessionId} for task ${taskId}${reason ? ` (${reason})` : ""}`);
|
|
288
|
+
await session.acpClient.cancelTurn(session.sessionId);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const session = findActiveSession(undefined);
|
|
292
|
+
if (session) {
|
|
293
|
+
console.error(`[bridge] Received cancellation with no taskId${reason ? ` (${reason})` : ""}. Cancelling active session ${session.sessionId}...`);
|
|
294
|
+
await session.acpClient.cancelTurn(session.sessionId);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
console.error(`[bridge] Received cancellation with no taskId${reason ? ` (${reason})` : ""}` +
|
|
298
|
+
(activeSessions.size === 0
|
|
299
|
+
? ", and no sessions are active"
|
|
300
|
+
: `, but ${activeSessions.size} sessions are active (skipping to avoid aborting unrelated tasks)`));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
167
304
|
// Modes that approve tool calls without asking the user. codex-acp, for
|
|
168
305
|
// example, defaults to an "agent" mode whose reviewer is "auto_review" — an
|
|
169
306
|
// automated Guardian Review that approves on the human's behalf — and also
|
|
@@ -220,6 +357,50 @@ export async function enforceHumanApprovalMode(connection, sessionResult) {
|
|
|
220
357
|
console.error(`[acp] ⚠️ Failed to set session mode to "${modeId}" — tool calls may execute without agentrq approval:`, err);
|
|
221
358
|
}
|
|
222
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* How many times the gateway will drag one session back into a mode that asks
|
|
362
|
+
* the human. An agent that keeps switching back is not going to stop, and an
|
|
363
|
+
* unbounded fight with it would be an endless stream of setSessionMode calls.
|
|
364
|
+
*/
|
|
365
|
+
const MAX_MODE_REENFORCEMENTS = 3;
|
|
366
|
+
/** sessionId → how many times its mode has already been put back. */
|
|
367
|
+
const modeReenforcements = new Map();
|
|
368
|
+
/**
|
|
369
|
+
* Puts a session back into a mode that asks the human, after the agent moved
|
|
370
|
+
* itself out of one.
|
|
371
|
+
*
|
|
372
|
+
* Agents may change modes on their own. If one moves into a mode that approves
|
|
373
|
+
* tool calls on the user's behalf, every later tool call — including
|
|
374
|
+
* destructive ones — executes without ever reaching agentrq, and nothing
|
|
375
|
+
* anywhere says so.
|
|
376
|
+
*/
|
|
377
|
+
export async function handleAgentModeChange(connection, sessionId, currentModeId, modes) {
|
|
378
|
+
const available = modes?.availableModes;
|
|
379
|
+
if (!available?.length)
|
|
380
|
+
return;
|
|
381
|
+
const mode = available.find((m) => m.id === currentModeId);
|
|
382
|
+
// A mode the agent never advertised cannot be vouched for either, so it is
|
|
383
|
+
// treated the same as one that approves on our behalf.
|
|
384
|
+
if (mode && !AUTO_APPROVING_MODE.test(describeMode(mode))) {
|
|
385
|
+
modeReenforcements.delete(sessionId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const attempts = modeReenforcements.get(sessionId) ?? 0;
|
|
389
|
+
if (attempts >= MAX_MODE_REENFORCEMENTS) {
|
|
390
|
+
console.error(`[acp] ⚠️ Agent keeps returning session ${sessionId} to mode "${currentModeId}", ` +
|
|
391
|
+
`which approves tool calls without asking. Giving up after ` +
|
|
392
|
+
`${MAX_MODE_REENFORCEMENTS} attempts — tool calls may now execute without ` +
|
|
393
|
+
`agentrq approval.`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
modeReenforcements.set(sessionId, attempts + 1);
|
|
397
|
+
console.error(`[acp] ⚠️ Agent moved session ${sessionId} into "${currentModeId}", which approves ` +
|
|
398
|
+
`tool calls without asking. Putting it back.`);
|
|
399
|
+
await enforceHumanApprovalMode(connection, {
|
|
400
|
+
sessionId,
|
|
401
|
+
modes: { availableModes: available, currentModeId },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
223
404
|
export function createAcpSessionSwitcher(connection, params, initialSessionId) {
|
|
224
405
|
let currentSessionId = initialSessionId;
|
|
225
406
|
const taskSessionMap = new Map();
|
|
@@ -309,6 +490,7 @@ export class TaskQueue {
|
|
|
309
490
|
export function parseGatewayArgs(args) {
|
|
310
491
|
const options = {
|
|
311
492
|
maxConcurrency: 2,
|
|
493
|
+
permissionTimeoutMs: DEFAULT_PERMISSION_TIMEOUT_MS,
|
|
312
494
|
command: "run",
|
|
313
495
|
allowUnverifiedAgent: false,
|
|
314
496
|
rest: [],
|
|
@@ -328,6 +510,14 @@ export function parseGatewayArgs(args) {
|
|
|
328
510
|
}
|
|
329
511
|
break;
|
|
330
512
|
}
|
|
513
|
+
case "--permission-timeout": {
|
|
514
|
+
const minutes = parseInt(value ?? "", 10);
|
|
515
|
+
if (!isNaN(minutes) && minutes >= 0) {
|
|
516
|
+
options.permissionTimeoutMs = minutes * 60_000;
|
|
517
|
+
i++;
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
331
521
|
case "--auth-method":
|
|
332
522
|
if (value) {
|
|
333
523
|
options.authMethodId = value;
|
|
@@ -356,6 +546,9 @@ export function parseGatewayArgs(args) {
|
|
|
356
546
|
case "--list-agents":
|
|
357
547
|
options.command = "list-agents";
|
|
358
548
|
break;
|
|
549
|
+
case "--agent-info":
|
|
550
|
+
options.command = "agent-info";
|
|
551
|
+
break;
|
|
359
552
|
case "--allow-unverified-agent":
|
|
360
553
|
options.allowUnverifiedAgent = true;
|
|
361
554
|
break;
|
|
@@ -443,12 +636,13 @@ export function assertAgentRunnable(command, usedRegistryId) {
|
|
|
443
636
|
throw new Error(`Agent command "${command}" was not found. ${hint}`);
|
|
444
637
|
}
|
|
445
638
|
/**
|
|
446
|
-
* Runs a one-shot
|
|
639
|
+
* Runs a one-shot command against the agent and shuts it down again.
|
|
447
640
|
*
|
|
448
|
-
* These commands exist so a login
|
|
449
|
-
* arrives
|
|
641
|
+
* These commands exist so a login — or a look at what the agent supports — can
|
|
642
|
+
* be done deliberately, before any task arrives, rather than only when a
|
|
643
|
+
* session is refused.
|
|
450
644
|
*/
|
|
451
|
-
export async function
|
|
645
|
+
export async function runAgentCommand(command, acpCmdArgs, agentrqConfig, mcpBridge, authMethodId) {
|
|
452
646
|
const [cmd, ...cmdArgs] = acpCmdArgs;
|
|
453
647
|
const agent = await openAgentConnection({
|
|
454
648
|
acpCmdArgs,
|
|
@@ -459,6 +653,10 @@ export async function runAuthCommand(command, acpCmdArgs, agentrqConfig, mcpBrid
|
|
|
459
653
|
try {
|
|
460
654
|
const connection = agent.connection;
|
|
461
655
|
const { authMethods, agentCapabilities } = agent.initResult;
|
|
656
|
+
if (command === "agent-info") {
|
|
657
|
+
console.log(describeAgentInfo(agent.initResult, acpCmdArgs.join(" ")));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
462
660
|
if (command === "list-auth-methods") {
|
|
463
661
|
console.log(`Authentication methods for "${acpCmdArgs.join(" ")}":\n${describeAuthMethods(authMethods)}`);
|
|
464
662
|
if (supportsLogout(agentCapabilities)) {
|
|
@@ -504,6 +702,9 @@ AGENT
|
|
|
504
702
|
if needed, instead of a command you supply.
|
|
505
703
|
--list-agents List every agent in the registry, and how each one
|
|
506
704
|
can run on this machine. Exits.
|
|
705
|
+
--agent-info What the agent says it supports — session
|
|
706
|
+
lifecycle, prompt content, MCP transports and
|
|
707
|
+
logins. Only a live handshake can tell you. Exits.
|
|
507
708
|
--allow-unverified-agent Install a registry binary that publishes no
|
|
508
709
|
checksum. Off by default: without a checksum there
|
|
509
710
|
is no way to tell what was downloaded.
|
|
@@ -521,6 +722,10 @@ AUTHENTICATION
|
|
|
521
722
|
BRIDGE
|
|
522
723
|
--max-concurrency <number> How many tasks may prompt the agent at once.
|
|
523
724
|
Defaults to 2.
|
|
725
|
+
--permission-timeout <min> How long a tool call waits for someone to approve
|
|
726
|
+
it before the turn is cancelled. Defaults to 30.
|
|
727
|
+
0 waits indefinitely, which is what a wedged
|
|
728
|
+
gateway looks like — use it knowingly.
|
|
524
729
|
|
|
525
730
|
OTHER
|
|
526
731
|
--help, -h Show this help. Exits.
|
|
@@ -529,6 +734,7 @@ EXAMPLES
|
|
|
529
734
|
acp-gateway --agent gemini Run Gemini from the registry
|
|
530
735
|
acp-gateway -- gemini --acp Run an agent you installed
|
|
531
736
|
acp-gateway --list-agents See what the registry offers
|
|
737
|
+
acp-gateway --agent-info --agent gemini See what that agent supports
|
|
532
738
|
acp-gateway --login -- gemini --acp Log in before running anything
|
|
533
739
|
acp-gateway --max-concurrency 4 -- gemini --acp
|
|
534
740
|
|
|
@@ -591,6 +797,7 @@ async function main() {
|
|
|
591
797
|
agentrqConfig.env = { ...agentrqConfig.env, ...resolved.env };
|
|
592
798
|
}
|
|
593
799
|
authConfig.methodId = authMethodId;
|
|
800
|
+
permissionConfig.timeoutMs = options.permissionTimeoutMs;
|
|
594
801
|
const taskQueue = new TaskQueue(maxConcurrency);
|
|
595
802
|
// 3. Initialize MCP Bridge
|
|
596
803
|
const mcpBridge = new MCPBridge(agentrqConfig);
|
|
@@ -600,7 +807,7 @@ async function main() {
|
|
|
600
807
|
// actually needs to reach agentrq.
|
|
601
808
|
if (command !== "run") {
|
|
602
809
|
try {
|
|
603
|
-
await
|
|
810
|
+
await runAgentCommand(command, acpCmdArgs, agentrqConfig, mcpBridge, authMethodId);
|
|
604
811
|
}
|
|
605
812
|
finally {
|
|
606
813
|
await mcpBridge.close();
|
|
@@ -622,14 +829,25 @@ async function main() {
|
|
|
622
829
|
lastTaskContent.set(taskId, content);
|
|
623
830
|
}
|
|
624
831
|
console.error("\n[bridge] Incoming task from MCP server. Forwarding to ACP agent...");
|
|
832
|
+
const queuedSeq = nextTaskSeq();
|
|
625
833
|
taskQueue.run(async () => {
|
|
834
|
+
if (isTaskCancelled(taskId, queuedSeq)) {
|
|
835
|
+
console.error(`[bridge] Task ${taskId} was cancelled before execution started, skipping`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
626
838
|
try {
|
|
627
839
|
const sessionInfo = await getOrCreateSession(taskId, acpCmdArgs, configs, agentrqConfig, mcpBridge);
|
|
840
|
+
if (isTaskCancelled(taskId, queuedSeq)) {
|
|
841
|
+
console.error(`[bridge] Task ${taskId} was cancelled during session setup, cancelling session`);
|
|
842
|
+
await sessionInfo.acpClient.cancelTurn(sessionInfo.sessionId);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
628
845
|
const result = await sessionInfo.connection.prompt({
|
|
629
846
|
sessionId: sessionInfo.sessionId,
|
|
630
847
|
prompt: [{ type: "text", text: content }],
|
|
631
848
|
});
|
|
632
849
|
await sessionInfo.acpClient.flushReply(sessionInfo.sessionId);
|
|
850
|
+
await sessionInfo.acpClient.reportStopReason(sessionInfo.sessionId, result.stopReason);
|
|
633
851
|
console.error(`\n[acp] Agent completed task. Reason: ${result.stopReason}`);
|
|
634
852
|
}
|
|
635
853
|
catch (err) {
|
|
@@ -639,6 +857,12 @@ async function main() {
|
|
|
639
857
|
console.error("[bridge] Error queuing task:", err);
|
|
640
858
|
});
|
|
641
859
|
});
|
|
860
|
+
// Listen for task cancellation events from the MCP bridge
|
|
861
|
+
mcpBridge.on("cancel", ({ taskId, reason }) => {
|
|
862
|
+
handleTaskCancellation(taskId, reason).catch((err) => {
|
|
863
|
+
console.error("[bridge] Error handling task cancellation:", err);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
642
866
|
// Initial check for a pending task
|
|
643
867
|
await checkForNextTask(mcpBridge, acpCmdArgs, configs, agentrqConfig, taskQueue);
|
|
644
868
|
// Keep the process alive
|
|
@@ -662,6 +886,9 @@ async function main() {
|
|
|
662
886
|
*/
|
|
663
887
|
export async function checkForNextTask(mcpBridge, acpCmdArgsOrConnection, configsOrSessionSwitcher, agentrqConfigOrAcpClient, taskQueue, acpCmdArgs, configs, agentrqConfig) {
|
|
664
888
|
console.error("[bridge] Checking for next task via MCP server...");
|
|
889
|
+
// Stamped before the fetch, so a cancel that arrives while `getTask` is in
|
|
890
|
+
// flight (or while the session is being opened) still stops the task.
|
|
891
|
+
const queuedSeq = nextTaskSeq();
|
|
665
892
|
try {
|
|
666
893
|
const result = await mcpBridge.callTool("getTask");
|
|
667
894
|
if (result.isError) {
|
|
@@ -684,6 +911,10 @@ export async function checkForNextTask(mcpBridge, acpCmdArgsOrConnection, config
|
|
|
684
911
|
}
|
|
685
912
|
console.error(`[bridge] Found task: "${text.slice(0, 50).replace(/\n/g, " ")}..."`);
|
|
686
913
|
const runFn = async () => {
|
|
914
|
+
if (isTaskCancelled(taskId, queuedSeq)) {
|
|
915
|
+
console.error(`[bridge] Task ${taskId} was cancelled before execution started, skipping`);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
687
918
|
let connectionToUse;
|
|
688
919
|
let acpClientToUse;
|
|
689
920
|
let sessionIdToUse;
|
|
@@ -712,11 +943,20 @@ export async function checkForNextTask(mcpBridge, acpCmdArgsOrConnection, config
|
|
|
712
943
|
sessionIdToUse = sessionInfo.sessionId;
|
|
713
944
|
acpClientToUse = sessionInfo.acpClient;
|
|
714
945
|
}
|
|
946
|
+
// Spawning the agent and opening the session takes seconds; a cancel
|
|
947
|
+
// that lands in that window has no turn to stop yet, so check again
|
|
948
|
+
// before handing the agent the work.
|
|
949
|
+
if (isTaskCancelled(taskId, queuedSeq)) {
|
|
950
|
+
console.error(`[bridge] Task ${taskId} was cancelled during session setup, cancelling session`);
|
|
951
|
+
await acpClientToUse?.cancelTurn(sessionIdToUse);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
715
954
|
const promptResult = await connectionToUse.prompt({
|
|
716
955
|
sessionId: sessionIdToUse,
|
|
717
956
|
prompt: [{ type: "text", text }],
|
|
718
957
|
});
|
|
719
958
|
await acpClientToUse.flushReply(sessionIdToUse);
|
|
959
|
+
await acpClientToUse.reportStopReason(sessionIdToUse, promptResult.stopReason);
|
|
720
960
|
console.error(`\n[acp] Agent completed with: ${promptResult.stopReason}`);
|
|
721
961
|
};
|
|
722
962
|
if (taskQueue) {
|