Compare commits

..
Author SHA1 Message Date
陈大猫 bb81b72ff0 fix(ai): avoid empty CodeBuddy tools whitelist that hides MCP tools (#4214)
Omit tools in MCP mode and explicitly disallow CodeBuddy built-ins, preserving injected MCP visibility and the skills-mode Bash whitelist.

Fixes #4214
2026-11-12 14:13:14 +08:00
4 changed files with 61 additions and 67 deletions

No files matched your search

-7
View File
@@ -100,12 +100,6 @@ const OH_MY_ZSH_YS_HOST_LINE_PATTERN = /^#\s+[^\s@]+\s+@\s+[^\s]+(?:\s+[^\n\r]+)
// within the path so partially entered commands do not count as idle prompts.
const ALPINE_PROMPT_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]*:(?:~(?:\/[^#$\n\r]*)?|\/[^#$\n\r]*)[#$]$/;
// Default macOS bash PS1 `\h:\W \u\$` (also seen after `sudo -i`),
// e.g. `ChrisdeMacBook-Air:~ root#`. Pure shape only; no shellKind gate.
// Require a username after the path so Alpine `host:~#` / `host:/path#`
// stay separate; the hostname excludes `@` so POSIX `user@host:...` stays separate.
const MACOS_SUDO_ROOT_PROMPT_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]*:(?:~|\/|[^\s:#$]+)\s+[A-Za-z0-9_.-]+[#$]$/;
// Powerlevel10k / similar Unicode prompt-ending ❯ (U+276F). Common idle forms:
// `~ ❯`, `~/path ❯`, `/tmp ❯`, short-dir `proj ❯`, optional `user@host `
// prefix (`alice@host ~ ❯`). Require a non-empty prefix and whitespace before
@@ -144,7 +138,6 @@ function isDefaultPosixPromptLine(line) {
POSIX_PROMPT_PATTERN.test(withoutEnvPrefix) ||
PAREN_POSIX_PROMPT_PATTERN.test(withoutEnvPrefix) ||
ALPINE_PROMPT_PATTERN.test(withoutEnvPrefix) ||
MACOS_SUDO_ROOT_PROMPT_PATTERN.test(withoutEnvPrefix) ||
ZSH_PROMPT_PATTERN.test(withoutEnvPrefix) ||
POWERLEVEL10K_PROMPT_PATTERN.test(withoutEnvPrefix) ||
OH_MY_ZSH_ROBBYRUSSELL_PROMPT_PATTERN.test(withoutEnvPrefix)
-51
View File
@@ -298,57 +298,6 @@ test("extracts an Alpine idle prompt while preserving its trailing whitespace",
assert.equal(extractTrailingIdlePrompt("localhost:~# \r\n"), "");
});
test("isDefaultPosixPromptLine matches macOS bash host:dir user prompts", () => {
for (const prompt of [
"ChrisdeMacBook-Air:~ root#",
"ChrisdeMacBook-Air:~ root# ",
"host:/tmp root#",
"host:tmp root#",
"host:/ root#",
"SomeHost:~ alice$",
"(venv) ChrisdeMacBook-Air:~ root# ",
]) {
assert.equal(isDefaultPosixPromptLine(prompt), true, prompt);
}
});
test("macOS bash prompt recognition rejects authentication output and pending input", () => {
for (const line of [
"Password:",
"Sorry, try again.",
"ChrisdeMacBook-Air:~ root# ls",
"ChrisdeMacBook-Air:~ root# echo #",
"ChrisdeMacBook-Air:~root#",
]) {
assert.equal(isDefaultPosixPromptLine(line), false, line);
assert.equal(extractTrailingIdlePrompt(`welcome\r\n${line}`), "", line);
}
for (const line of ["PS C:\\Users\\alice>", "C:\\Users\\alice>"]) {
assert.equal(isDefaultPosixPromptLine(line), false, line);
}
});
test("extracts a macOS bash idle prompt while preserving its trailing whitespace", () => {
assert.equal(
extractTrailingIdlePrompt("Welcome\r\n\x1b[32mChrisdeMacBook-Air:~ root# \x1b[0m"),
"ChrisdeMacBook-Air:~ root# ",
);
assert.equal(
extractTrailingIdlePrompt("(venv) ChrisdeMacBook-Air:~ root# "),
"(venv) ChrisdeMacBook-Air:~ root# ",
);
});
test("extracts the macOS root idle prompt after sudo -i finishes authentication", () => {
const transcript = "user@MacBook-Pro ~ % sudo -i\r\nPassword:";
assert.equal(extractTrailingIdlePrompt(transcript), "");
assert.equal(extractTrailingIdlePrompt(`${transcript}\r\nSorry, try again.`), "");
assert.equal(
extractTrailingIdlePrompt(`${transcript}\r\nChrisdeMacBook-Air:~ root# `),
"ChrisdeMacBook-Air:~ root# ",
);
});
test("isDefaultPosixPromptLine matches Powerlevel10k ❯ idle prompts", () => {
for (const prompt of [
"~ ❯",
@@ -24,6 +24,21 @@ const UI_DISALLOWED_TOOLS = ["AskUserQuestion"];
const MCP_MODE_TOOLS = [];
const SKILLS_MODE_TOOLS = ["Bash"];
// Deduped built-in catalog from @tencent-ai/agent-sdk 0.3.258 cli/product.json.
const CODEBUDDY_BUILTIN_TOOLS = [
"Agent", "Bash", "PowerShell", "Glob", "Grep", "Read", "Edit", "Write",
"NotebookEdit", "WebFetch", "ListMcpResources", "ReadMcpResource", "WaitForMcpServers",
"TaskCreate", "TaskGet", "TaskUpdate", "TaskList", "WebSearch", "EnterPlanMode",
"ExitPlanMode", "KillShell", "TaskStop", "TaskOutput", "SlashCommand", "Skill",
"SkillManage", "AskUserQuestion", "AskUserForStructuredInput", "LSP", "StructuredOutput",
"ToolSearch", "DeferExecuteTool", "ImageGen", "ImageEdit", "VideoGen", "Artifact",
"ArtifactControl", "ComputerUse", "TeamCreate", "TeamDelete", "SendMessage",
"SendUserMessage", "EnterWorktree", "LeaveWorktree", "CronCreate", "CronDelete",
"CronList", "DelegateTool", "WeChatReply", "WeComReply", "PushNotification",
"ReportFindings", "Workflow", "Monitor", "REPL", "A2AGetAgentCard", "A2ASendMessage",
"A2AGetTask", "A2ACancelTask",
];
const CODEBUDDY_IMAGE_MEDIA_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
function isCodebuddyImageAttachment(attachment) {
@@ -261,18 +276,19 @@ function buildCodebuddyQueryOptions({
allowDangerouslySkipPermissions: true,
extraArgs: { "dangerously-skip-permissions": null },
mcpServers: toSdkMcpServers(injectedMcpServers),
tools: builtinTools,
// `tools` is the built-in tool whitelist. In mcp mode it is [], so CodeBuddy
// built-ins stay disabled while injected Netcatty MCP tools remain visible.
// Do not mirror that empty list into allowedTools: the SDK treats
// allowedTools as an auto-approval list, and allowedTools: [] prevents MCP
// tool calls from running under bypassPermissions.
disallowedTools: [...UI_DISALLOWED_TOOLS],
// Block built-ins explicitly in mcp mode: tools: [] becomes --tools "",
// which also hides injected MCP tools in CodeBuddy CLI v2.183.0.
// Leave allowedTools unset: an empty auto-approval list prevents MCP tool
// calls from running under bypassPermissions.
disallowedTools: builtinTools.length > 0
? [...UI_DISALLOWED_TOOLS]
: [...new Set([...UI_DISALLOWED_TOOLS, ...CODEBUDDY_BUILTIN_TOOLS])],
// Keep the SDK isolated from user/project settings so local hooks, plugins,
// or extra MCP servers cannot expand Netcatty's controlled tool boundary.
settingSources: [],
env,
};
if (builtinTools.length > 0) options.tools = builtinTools;
const selection = splitCodebuddyModelSelection(model);
if (selection.model) options.model = selection.model;
if (abortController) options.abortController = abortController;
@@ -59,15 +59,51 @@ test("buildCodebuddyQueryOptions wires SDK options in isolated mode", () => {
assert.equal(opts.pathToCodebuddyCode, "/opt/codebuddy/bin/codebuddy");
assert.equal(opts.abortController, ac);
assert.equal(opts.resume, "sess-1");
assert.deepEqual(opts.tools, []);
// allowedTools must stay unset in mcp mode: tools:[] disables built-ins, while
assert.ok(!("tools" in opts));
// allowedTools:[] would prevent injected Netcatty MCP tools from running.
assert.ok(!("allowedTools" in opts));
assert.ok(opts.disallowedTools.includes("Bash"));
assert.ok(opts.disallowedTools.includes("AskUserQuestion"));
assert.equal(opts.mcpServers["netcatty-remote-hosts"].type, "stdio");
assert.deepEqual(opts.mcpServers["netcatty-remote-hosts"].env, { NETCATTY_MCP_PORT: "1" });
});
test("mcp and default mode block built-ins while preserving injected MCP servers", () => {
for (const toolIntegrationMode of ["mcp", undefined]) {
const opts = buildCodebuddyQueryOptions({
toolIntegrationMode,
injectedMcpServers: [{ name: "netcatty", command: "/abs/electron" }],
});
assert.ok(!("tools" in opts));
assert.ok(!("allowedTools" in opts));
assert.equal(opts.permissionMode, "bypassPermissions");
for (const name of ["Bash", "PowerShell", "Read", "Edit", "Write", "Agent", "Skill", "AskUserQuestion"]) {
assert.ok(opts.disallowedTools.includes(name), `${name} must stay disabled`);
}
assert.equal(opts.mcpServers.netcatty.command, "/abs/electron");
}
});
test("mcp mode omits --tools under the SDK serialization rule", () => {
for (const toolIntegrationMode of ["mcp", undefined]) {
const opts = buildCodebuddyQueryOptions({ toolIntegrationMode });
const args = [];
// @tencent-ai/agent-sdk lib/transport/process-transport.js
if (opts.tools !== undefined) {
args.push("--tools", opts.tools.join(","));
}
assert.deepEqual(args, []);
}
});
test("skills mode keeps the Bash whitelist and UI exclusions", () => {
const opts = buildCodebuddyQueryOptions({ toolIntegrationMode: "skills" });
assert.deepEqual(opts.tools, ["Bash"]);
assert.deepEqual(opts.disallowedTools, ["AskUserQuestion"]);
assert.ok(!opts.disallowedTools.includes("Bash"));
assert.ok(!("allowedTools" in opts));
});
test("built-in tools are mode-aware", () => {
assert.deepEqual(codebuddyBuiltinTools("mcp"), []);
assert.deepEqual(codebuddyBuiltinTools(undefined), []);