Skip to content

Commit b57a429

Browse files
authored
fix: merge user-provided disallowedTools instead of overwriting (agentclientprotocol#335)
The disallowedTools merge fix from agentclientprotocol#295 was regressed by agentclientprotocol#316 when the options object was restructured. Restore the merge behavior and add regression tests for all three documented merge fields (disallowedTools, hooks, mcpServers). Fixes agentclientprotocol#334
1 parent 101f09d commit b57a429

2 files changed

Lines changed: 186 additions & 1 deletion

File tree

src/acp-agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1251,7 +1251,7 @@ export class ClaudeAcpAgent implements Agent {
12511251
: isStaticBinary()
12521252
? { pathToClaudeCodeExecutable: process.execPath }
12531253
: {}),
1254-
disallowedTools,
1254+
disallowedTools: [...(userProvidedOptions?.disallowedTools || []), ...disallowedTools],
12551255
tools: { type: "preset", preset: "claude_code" },
12561256
hooks: {
12571257
...userProvidedOptions?.hooks,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import { AgentSideConnection, SessionNotification } from "@agentclientprotocol/sdk";
3+
import type { Options } from "@anthropic-ai/claude-agent-sdk";
4+
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
5+
6+
let capturedOptions: Options | undefined;
7+
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
8+
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
9+
"@anthropic-ai/claude-agent-sdk",
10+
);
11+
return {
12+
...actual,
13+
query: (args: { prompt: unknown; options: Options }) => {
14+
capturedOptions = args.options;
15+
return {
16+
initializationResult: async () => ({
17+
models: [
18+
{ value: "claude-sonnet-4-5", displayName: "Claude Sonnet", description: "Fast" },
19+
],
20+
}),
21+
setModel: async () => {},
22+
supportedCommands: async () => [],
23+
[Symbol.asyncIterator]: async function* () {},
24+
};
25+
},
26+
};
27+
});
28+
29+
vi.mock("../tools.js", async () => {
30+
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
31+
return {
32+
...actual,
33+
registerHookCallback: vi.fn(),
34+
};
35+
});
36+
37+
describe("createSession options merging", () => {
38+
let agent: ClaudeAcpAgentType;
39+
let ClaudeAcpAgent: typeof ClaudeAcpAgentType;
40+
41+
function createMockClient(): AgentSideConnection {
42+
return {
43+
sessionUpdate: async (_notification: SessionNotification) => {},
44+
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
45+
readTextFile: async () => ({ content: "" }),
46+
writeTextFile: async () => ({}),
47+
} as unknown as AgentSideConnection;
48+
}
49+
50+
beforeEach(async () => {
51+
capturedOptions = undefined;
52+
53+
vi.resetModules();
54+
const acpAgent = await import("../acp-agent.js");
55+
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
56+
57+
agent = new ClaudeAcpAgent(createMockClient());
58+
});
59+
60+
it("merges user-provided disallowedTools with ACP internal list", async () => {
61+
await agent.newSession({
62+
cwd: "/test",
63+
mcpServers: [],
64+
_meta: {
65+
claudeCode: {
66+
options: {
67+
disallowedTools: ["WebSearch", "WebFetch"],
68+
},
69+
},
70+
},
71+
});
72+
73+
// User-provided tools should be present
74+
expect(capturedOptions!.disallowedTools).toContain("WebSearch");
75+
expect(capturedOptions!.disallowedTools).toContain("WebFetch");
76+
// ACP's internal disallowed tool should also be present
77+
expect(capturedOptions!.disallowedTools).toContain("AskUserQuestion");
78+
});
79+
80+
it("works when user provides no disallowedTools", async () => {
81+
await agent.newSession({
82+
cwd: "/test",
83+
mcpServers: [],
84+
});
85+
86+
expect(capturedOptions!.disallowedTools).toContain("AskUserQuestion");
87+
});
88+
89+
it("works when user provides empty disallowedTools", async () => {
90+
await agent.newSession({
91+
cwd: "/test",
92+
mcpServers: [],
93+
_meta: {
94+
claudeCode: {
95+
options: {
96+
disallowedTools: [],
97+
},
98+
},
99+
},
100+
});
101+
102+
expect(capturedOptions!.disallowedTools).toContain("AskUserQuestion");
103+
});
104+
105+
it("includes both user and built-in disallowed tools when disableBuiltInTools is true", async () => {
106+
await agent.newSession({
107+
cwd: "/test",
108+
mcpServers: [],
109+
_meta: {
110+
disableBuiltInTools: true,
111+
claudeCode: {
112+
options: {
113+
disallowedTools: ["CustomTool"],
114+
},
115+
},
116+
},
117+
});
118+
119+
const disallowed = capturedOptions!.disallowedTools!;
120+
// User-provided
121+
expect(disallowed).toContain("CustomTool");
122+
// ACP internal
123+
expect(disallowed).toContain("AskUserQuestion");
124+
// Built-in tools disabled by disableBuiltInTools
125+
expect(disallowed).toContain("Read");
126+
expect(disallowed).toContain("Write");
127+
expect(disallowed).toContain("Bash");
128+
});
129+
130+
it("merges user-provided hooks with ACP hooks", async () => {
131+
const userPreToolUseHook = { hooks: [{ command: "echo pre" }] };
132+
133+
await agent.newSession({
134+
cwd: "/test",
135+
mcpServers: [],
136+
_meta: {
137+
claudeCode: {
138+
options: {
139+
hooks: {
140+
PreToolUse: [userPreToolUseHook],
141+
PostToolUse: [{ hooks: [{ command: "echo user-post" }] }],
142+
},
143+
},
144+
},
145+
},
146+
});
147+
148+
// User's PreToolUse hooks should be preserved
149+
expect(capturedOptions!.hooks?.PreToolUse).toEqual([userPreToolUseHook]);
150+
// PostToolUse should contain both user and ACP hooks
151+
expect(capturedOptions!.hooks?.PostToolUse).toHaveLength(2);
152+
});
153+
154+
it("merges user-provided mcpServers with ACP mcpServers", async () => {
155+
await agent.newSession({
156+
cwd: "/test",
157+
mcpServers: [
158+
{
159+
name: "acp-server",
160+
command: "node",
161+
args: ["acp-server.js"],
162+
env: [],
163+
},
164+
],
165+
_meta: {
166+
claudeCode: {
167+
options: {
168+
mcpServers: {
169+
"user-server": {
170+
type: "stdio",
171+
command: "node",
172+
args: ["server.js"],
173+
},
174+
},
175+
},
176+
},
177+
},
178+
});
179+
180+
// User-provided MCP server should be present
181+
expect(capturedOptions!.mcpServers).toHaveProperty("user-server");
182+
// ACP-provided MCP server should also be present
183+
expect(capturedOptions!.mcpServers).toHaveProperty("acp-server");
184+
});
185+
});

0 commit comments

Comments
 (0)