Skip to content

Commit e6e4169

Browse files
authored
acp: fail honestly in bridge mode (openclaw#41424)
Merged via squash. Prepared head SHA: b5e6e13 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
1 parent 1bc59cc commit e6e4169

File tree

5 files changed

+153
-7
lines changed

5 files changed

+153
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
2727
- Cron/subagent followup: do not misclassify empty or `NO_REPLY` cron responses as interim acknowledgements that need a rerun, so deliberately silent cron jobs are no longer retried. (#41383) thanks @jackal092927.
2828
- Auth/cooldowns: reset expired auth-profile cooldown error counters before computing the next backoff so stale on-disk counters do not re-escalate into long cooldown loops after expiry. (#41028) thanks @zerone0x.
2929
- Gateway/node pending drain followup: keep `hasMore` true when the deferred baseline status item still needs delivery, and avoid allocating empty pending-work state for drain-only nodes with no queued work. (#41429) Thanks @mbelinky.
30+
- ACP/bridge mode: reject unsupported per-session MCP server setup and propagate rejected session-mode changes so IDE clients see explicit bridge limitations instead of silent success. (#41424) Thanks @mbelinky.
3031

3132
## 2026.3.8
3233

docs.acp.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,40 @@ Key goals:
1717
- Works with existing Gateway session store (list/resolve/reset).
1818
- Safe defaults (isolated ACP session keys by default).
1919

20+
## Bridge Scope
21+
22+
`openclaw acp` is a Gateway-backed ACP bridge, not a full ACP-native editor
23+
runtime. It is designed to route IDE prompts into an existing OpenClaw Gateway
24+
session with predictable session mapping and basic streaming updates.
25+
26+
## Compatibility Matrix
27+
28+
| ACP area | Status | Notes |
29+
| --------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
30+
| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. |
31+
| `listSessions`, slash commands | Implemented | Session list works against Gateway session state; commands are advertised via `available_commands_update`. |
32+
| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key. Stored history is not replayed yet. |
33+
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
34+
| Session modes | Partial | `session/set_mode` is supported, but this bridge does not yet expose broader ACP-native mode or config surfaces. |
35+
| Tool streaming | Partial | Tool start and result updates are forwarded, but without ACP-native terminal or richer editor metadata. |
36+
| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. |
37+
| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. |
38+
| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. |
39+
| Session plans / thought streaming | Unsupported | The bridge currently emits output text and tool status, not ACP plan or thought updates. |
40+
41+
## Known Limitations
42+
43+
- `loadSession` rebinds to an existing Gateway session, but it does not replay
44+
prior user or assistant history yet.
45+
- If multiple ACP clients share the same Gateway session key, event and cancel
46+
routing are best-effort rather than strictly isolated per client. Prefer the
47+
default isolated `acp:<uuid>` sessions when you need clean editor-local
48+
turns.
49+
- Gateway stop states are translated into ACP stop reasons, but that mapping is
50+
less expressive than a fully ACP-native runtime.
51+
- Tool follow-along data is intentionally narrow in bridge mode. The bridge
52+
does not yet emit ACP terminals, file locations, or structured diffs.
53+
2054
## How can I use this
2155

2256
Use ACP when an IDE or tooling speaks Agent Client Protocol and you want it to

docs/cli/acp.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,38 @@ Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge t
1313
This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway
1414
over WebSocket. It keeps ACP sessions mapped to Gateway session keys.
1515

16+
`openclaw acp` is a Gateway-backed ACP bridge, not a full ACP-native editor
17+
runtime. It focuses on session routing, prompt delivery, and basic streaming
18+
updates.
19+
20+
## Compatibility Matrix
21+
22+
| ACP area | Status | Notes |
23+
| --------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
24+
| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. |
25+
| `listSessions`, slash commands | Implemented | Session list works against Gateway session state; commands are advertised via `available_commands_update`. |
26+
| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key. Stored history is not replayed yet. |
27+
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
28+
| Session modes | Partial | `session/set_mode` is supported, but this bridge does not yet expose broader ACP-native mode or config surfaces. |
29+
| Tool streaming | Partial | Tool start and result updates are forwarded, but without ACP-native terminal or richer editor metadata. |
30+
| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. |
31+
| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. |
32+
| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. |
33+
| Session plans / thought streaming | Unsupported | The bridge currently emits output text and tool status, not ACP plan or thought updates. |
34+
35+
## Known Limitations
36+
37+
- `loadSession` rebinds to an existing Gateway session, but it does not replay
38+
prior user or assistant history yet.
39+
- If multiple ACP clients share the same Gateway session key, event and cancel
40+
routing are best-effort rather than strictly isolated per client. Prefer the
41+
default isolated `acp:<uuid>` sessions when you need clean editor-local
42+
turns.
43+
- Gateway stop states are translated into ACP stop reasons, but that mapping is
44+
less expressive than a fully ACP-native runtime.
45+
- Tool follow-along data is intentionally narrow in bridge mode. The bridge
46+
does not yet emit ACP terminals, file locations, or structured diffs.
47+
1648
## Usage
1749

1850
```bash

src/acp/translator.session-rate-limit.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
LoadSessionRequest,
33
NewSessionRequest,
44
PromptRequest,
5+
SetSessionModeRequest,
56
} from "@agentclientprotocol/sdk";
67
import { describe, expect, it, vi } from "vitest";
78
import type { GatewayClient } from "../gateway/client.js";
@@ -38,6 +39,14 @@ function createPromptRequest(
3839
} as unknown as PromptRequest;
3940
}
4041

42+
function createSetSessionModeRequest(sessionId: string, modeId: string): SetSessionModeRequest {
43+
return {
44+
sessionId,
45+
modeId,
46+
_meta: {},
47+
} as unknown as SetSessionModeRequest;
48+
}
49+
4150
async function expectOversizedPromptRejected(params: { sessionId: string; text: string }) {
4251
const request = vi.fn(async () => ({ ok: true })) as GatewayClient["request"];
4352
const sessionStore = createInMemorySessionStore();
@@ -97,6 +106,71 @@ describe("acp session creation rate limit", () => {
97106
});
98107
});
99108

109+
describe("acp unsupported bridge session setup", () => {
110+
it("rejects per-session MCP servers on newSession", async () => {
111+
const sessionStore = createInMemorySessionStore();
112+
const connection = createAcpConnection();
113+
const sessionUpdate = vi.spyOn(connection, "sessionUpdate");
114+
const agent = new AcpGatewayAgent(connection, createAcpGateway(), {
115+
sessionStore,
116+
});
117+
118+
await expect(
119+
agent.newSession({
120+
...createNewSessionRequest(),
121+
mcpServers: [{ name: "docs", command: "mcp-docs" }] as never[],
122+
}),
123+
).rejects.toThrow(/does not support per-session MCP servers/i);
124+
125+
expect(sessionStore.hasSession("docs-session")).toBe(false);
126+
expect(sessionUpdate).not.toHaveBeenCalled();
127+
sessionStore.clearAllSessionsForTest();
128+
});
129+
130+
it("rejects per-session MCP servers on loadSession", async () => {
131+
const sessionStore = createInMemorySessionStore();
132+
const connection = createAcpConnection();
133+
const sessionUpdate = vi.spyOn(connection, "sessionUpdate");
134+
const agent = new AcpGatewayAgent(connection, createAcpGateway(), {
135+
sessionStore,
136+
});
137+
138+
await expect(
139+
agent.loadSession({
140+
...createLoadSessionRequest("docs-session"),
141+
mcpServers: [{ name: "docs", command: "mcp-docs" }] as never[],
142+
}),
143+
).rejects.toThrow(/does not support per-session MCP servers/i);
144+
145+
expect(sessionStore.hasSession("docs-session")).toBe(false);
146+
expect(sessionUpdate).not.toHaveBeenCalled();
147+
sessionStore.clearAllSessionsForTest();
148+
});
149+
});
150+
151+
describe("acp setSessionMode bridge behavior", () => {
152+
it("surfaces gateway mode patch failures instead of succeeding silently", async () => {
153+
const sessionStore = createInMemorySessionStore();
154+
const request = vi.fn(async (method: string) => {
155+
if (method === "sessions.patch") {
156+
throw new Error("gateway rejected mode");
157+
}
158+
return { ok: true };
159+
}) as GatewayClient["request"];
160+
const agent = new AcpGatewayAgent(createAcpConnection(), createAcpGateway(request), {
161+
sessionStore,
162+
});
163+
164+
await agent.loadSession(createLoadSessionRequest("mode-session"));
165+
166+
await expect(
167+
agent.setSessionMode(createSetSessionModeRequest("mode-session", "high")),
168+
).rejects.toThrow(/gateway rejected mode/i);
169+
170+
sessionStore.clearAllSessionsForTest();
171+
});
172+
});
173+
100174
describe("acp prompt size hardening", () => {
101175
it("rejects oversized prompt blocks without leaking active runs", async () => {
102176
await expectOversizedPromptRejected({

src/acp/translator.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,7 @@ export class AcpGatewayAgent implements Agent {
170170
}
171171

172172
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
173-
if (params.mcpServers.length > 0) {
174-
this.log(`ignoring ${params.mcpServers.length} MCP servers`);
175-
}
173+
this.assertSupportedSessionSetup(params.mcpServers);
176174
this.enforceSessionCreateRateLimit("newSession");
177175

178176
const sessionId = randomUUID();
@@ -193,9 +191,7 @@ export class AcpGatewayAgent implements Agent {
193191
}
194192

195193
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
196-
if (params.mcpServers.length > 0) {
197-
this.log(`ignoring ${params.mcpServers.length} MCP servers`);
198-
}
194+
this.assertSupportedSessionSetup(params.mcpServers);
199195
if (!this.sessionStore.hasSession(params.sessionId)) {
200196
this.enforceSessionCreateRateLimit("loadSession");
201197
}
@@ -256,7 +252,7 @@ export class AcpGatewayAgent implements Agent {
256252
this.log(`setSessionMode: ${session.sessionId} -> ${params.modeId}`);
257253
} catch (err) {
258254
this.log(`setSessionMode error: ${String(err)}`);
259-
throw err;
255+
throw err instanceof Error ? err : new Error(String(err));
260256
}
261257
return {};
262258
}
@@ -536,6 +532,15 @@ export class AcpGatewayAgent implements Agent {
536532
});
537533
}
538534

535+
private assertSupportedSessionSetup(mcpServers: ReadonlyArray<unknown>): void {
536+
if (mcpServers.length === 0) {
537+
return;
538+
}
539+
throw new Error(
540+
"ACP bridge mode does not support per-session MCP servers. Configure MCP on the OpenClaw gateway or agent instead.",
541+
);
542+
}
543+
539544
private enforceSessionCreateRateLimit(method: "newSession" | "loadSession"): void {
540545
const budget = this.sessionCreateRateLimiter.consume();
541546
if (budget.allowed) {

0 commit comments

Comments
 (0)