Skip to content

Commit 0f48341

Browse files
authored
Merge pull request #9538 from continuedev/allow-mcp-tools-headless
Allow MCP/Bash tools by default in headless mode
2 parents 094c023 + 963d1eb commit 0f48341

18 files changed

+231
-259
lines changed

docs/guides/posthog-github-continuous-ai.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ You only need to configure the PostHog MCP credential - it automatically handles
128128
organization
129129
- It automatically uses your default project (no project ID
130130
needed)
131-
- If you have multiple projects, use `mcp__posthog__switch-project` to
131+
- If you have multiple projects, use `switch-project` to
132132
change
133133
- The MCP connects via `https://mcp.posthog.com/sse` using your account context.
134134

@@ -300,7 +300,7 @@ The main workflow above focuses on analyzing session recordings to identify UX i
300300

301301
```bash
302302
# Get all feature flags and analyze them
303-
cn "Use PostHog MCP to fetch all feature flags with mcp__posthog__feature-flag-get-all. Then analyze each flag to identify: 1) Flags that are 100% rolled out and could be removed, 2) Flags that haven't been updated in 90+ days, 3) Flags with complex targeting that might need simplification, 4) Experimental flags that should be cleaned up."
303+
cn "Use PostHog MCP to fetch all feature flags with feature-flag-get-all. Then analyze each flag to identify: 1) Flags that are 100% rolled out and could be removed, 2) Flags that haven't been updated in 90+ days, 3) Flags with complex targeting that might need simplification, 4) Experimental flags that should be cleaned up."
304304

305305
# Create cleanup issues for identified flags
306306
cn "For each problematic feature flag identified, create a GitHub issue using gh CLI:

extensions/cli/src/permissions/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ The system comes with sensible default policies:
1313
- Read-only tools (`readFile`, `listFiles`, `searchCode`, `fetch`) are **allowed** by default
1414
- Write operations (`writeFile`) require **confirmation** (ask)
1515
- Terminal commands (`runTerminalCommand`) require **confirmation** (ask)
16-
- MCP tools with IDE prefix are **allowed** by default
17-
- Other MCP tools require **confirmation** (ask)
16+
- MCP tools and Bash require **confirmation** (ask) in TUI mode, but are **allowed** automatically in headless mode
1817
- Any unmatched tools default to **ask**
1918

2019
## How It Works

extensions/cli/src/permissions/defaultPolicies.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, expect, it } from "vitest";
22

3-
import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js";
3+
import { getDefaultToolPolicies } from "./defaultPolicies.js";
4+
const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies();
45

56
describe("defaultPolicies", () => {
67
it("should have correct permissions for read-only tools", () => {
@@ -21,6 +22,13 @@ describe("defaultPolicies", () => {
2122
}
2223
});
2324

25+
it("should not have prefix wildcard policies in defaults", () => {
26+
const prefixWildcardPolicy = DEFAULT_TOOL_POLICIES.find(
27+
(p) => p.tool.endsWith("*") && p.tool !== "*",
28+
);
29+
expect(prefixWildcardPolicy).toBeUndefined();
30+
});
31+
2432
it("should have correct permissions for write tools", () => {
2533
const writeTools = ["Write", "Edit", "MultiEdit", "Bash"];
2634

extensions/cli/src/permissions/defaultPolicies.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,64 @@ import { ToolPermissionPolicy } from "./types.js";
44
* Default permission policies for all built-in tools.
55
* These policies are applied in order - first match wins.
66
*/
7-
export const DEFAULT_TOOL_POLICIES: ToolPermissionPolicy[] = [
8-
// Read-only tools are generally safe to allow
9-
{ tool: "Read", permission: "allow" },
10-
{ tool: "List", permission: "allow" },
11-
{ tool: "Search", permission: "allow" },
12-
{ tool: "Fetch", permission: "allow" },
7+
export function getDefaultToolPolicies(
8+
isHeadless = false,
9+
): ToolPermissionPolicy[] {
10+
const policies: ToolPermissionPolicy[] = [
11+
// Write tools
12+
{ tool: "Edit", permission: "ask" },
13+
{ tool: "MultiEdit", permission: "ask" },
14+
{ tool: "Write", permission: "ask" },
1315

14-
// Write operations should require confirmation
15-
{ tool: "Write", permission: "ask" },
16-
{ tool: "Edit", permission: "ask" },
17-
{ tool: "MultiEdit", permission: "ask" },
16+
{ tool: "Checklist", permission: "allow" },
17+
{ tool: "Diff", permission: "allow" },
18+
{ tool: "Exit", permission: "allow" }, // Exit tool is generally safe (headless mode only)
19+
{ tool: "Fetch", permission: "allow" }, // Technically not read only but edge casey to post w query params
20+
{ tool: "List", permission: "allow" },
21+
{ tool: "Read", permission: "allow" },
22+
{ tool: "Search", permission: "allow" },
23+
{ tool: "Status", permission: "allow" },
24+
{ tool: "ReportFailure", permission: "allow" },
25+
{ tool: "UploadArtifact", permission: "allow" },
26+
];
1827

19-
// Write to a checklist
20-
{ tool: "Checklist", permission: "allow" },
28+
// MCP and Bash are ask in TUI mode, auto in headless
29+
if (isHeadless) {
30+
policies.push({ tool: "Bash", permission: "allow" });
31+
policies.push({ tool: "*", permission: "allow" });
32+
} else {
33+
policies.push({ tool: "Bash", permission: "ask" });
34+
policies.push({ tool: "*", permission: "ask" });
35+
}
2136

22-
// Terminal commands should require confirmation by default
23-
{ tool: "Bash", permission: "ask" },
37+
return policies;
38+
}
2439

25-
// Exit tool is generally safe (headless mode only)
26-
{ tool: "Exit", permission: "allow" },
40+
// Plan mode: Complete override - exclude all write operations, allow only reads and bash
41+
export const PLAN_MODE_POLICIES: ToolPermissionPolicy[] = [
42+
{ tool: "Edit", permission: "exclude" },
43+
{ tool: "MultiEdit", permission: "exclude" },
44+
{ tool: "Write", permission: "exclude" },
2745

28-
// View diff is read-only
46+
// TODO address bash read only concerns, maybe make permissions more granular
47+
{ tool: "Bash", permission: "allow" },
48+
49+
{ tool: "Checklist", permission: "allow" },
2950
{ tool: "Diff", permission: "allow" },
51+
{ tool: "Exit", permission: "allow" },
52+
{ tool: "Fetch", permission: "allow" },
53+
{ tool: "List", permission: "allow" },
54+
{ tool: "Read", permission: "allow" },
55+
{ tool: "ReportFailure", permission: "allow" },
56+
{ tool: "Search", permission: "allow" },
57+
{ tool: "Status", permission: "allow" },
58+
{ tool: "UploadArtifact", permission: "allow" },
59+
60+
// Allow MCP tools
61+
{ tool: "*", permission: "allow" },
62+
];
3063

31-
// Default fallback - ask for any unmatched tools
32-
{ tool: "*", permission: "ask" },
64+
// Auto mode: Complete override - allow everything without asking
65+
export const AUTO_MODE_POLICIES: ToolPermissionPolicy[] = [
66+
{ tool: "*", permission: "allow" },
3367
];

extensions/cli/src/permissions/headlessPermissions.integration.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js";
1+
import { getDefaultToolPolicies } from "./defaultPolicies.js";
22
import { checkToolPermission } from "./permissionChecker.js";
33
import { resolvePermissionPrecedence } from "./precedenceResolver.js";
44

5+
const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies();
6+
57
describe("Headless Permissions Integration", () => {
68
describe("precedence resolution", () => {
79
it("should use default policies", () => {

extensions/cli/src/permissions/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export type {
66
ToolPermissions,
77
} from "./types.js";
88

9-
export { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js";
109
export {
1110
checkToolPermission,
1211
matchesArguments,

extensions/cli/src/permissions/permissionChecker.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ describe("Permission Checker", () => {
4444
});
4545

4646
it("should match prefix wildcards", () => {
47-
expect(matchesToolPattern("mcp__ide__getDiagnostics", "mcp__*")).toBe(
47+
expect(
48+
matchesToolPattern("external_ide_getDiagnostics", "external_*"),
49+
).toBe(true);
50+
expect(matchesToolPattern("external_filesystem_read", "external_*")).toBe(
4851
true,
4952
);
50-
expect(matchesToolPattern("mcp__filesystem__read", "mcp__*")).toBe(true);
51-
expect(matchesToolPattern("builtin__readFile", "mcp__*")).toBe(false);
53+
expect(matchesToolPattern("builtin_readFile", "external_*")).toBe(false);
5254
});
5355

5456
it("should match suffix wildcards", () => {
@@ -89,7 +91,7 @@ describe("Permission Checker", () => {
8991
it("should handle wildcard patterns with special regex characters", () => {
9092
expect(matchesToolPattern("test[abc].txt", "test[abc].*")).toBe(true);
9193
expect(matchesToolPattern("test[abc]_file", "test[abc].*")).toBe(false);
92-
expect(matchesToolPattern("mcp__tool[1]", "mcp__*")).toBe(true);
94+
expect(matchesToolPattern("external_tool[1]", "external_*")).toBe(true);
9395
expect(matchesToolPattern("file.test.txt", "*.test.*")).toBe(true);
9496
expect(matchesToolPattern("(tool)_name", "(tool)*")).toBe(true);
9597
expect(matchesToolPattern("tool+plus_extra", "tool+plus*")).toBe(true);
@@ -417,17 +419,17 @@ describe("Permission Checker", () => {
417419
it("should match wildcard patterns", () => {
418420
const permissions: ToolPermissions = {
419421
policies: [
420-
{ tool: "mcp__*", permission: "ask" },
422+
{ tool: "external_*", permission: "ask" },
421423
{ tool: "*", permission: "allow" },
422424
],
423425
};
424426

425-
const mcpResult = checkToolPermission(
426-
{ name: "mcp__ide__getDiagnostics", arguments: {} },
427+
const externalResult = checkToolPermission(
428+
{ name: "external_ide_getDiagnostics", arguments: {} },
427429
permissions,
428430
);
429-
expect(mcpResult.permission).toBe("ask");
430-
expect(mcpResult.matchedPolicy?.tool).toBe("mcp__*");
431+
expect(externalResult.permission).toBe("ask");
432+
expect(externalResult.matchedPolicy?.tool).toBe("external_*");
431433

432434
const builtinResult = checkToolPermission(
433435
{ name: "readFile", arguments: { path: "/test.txt" } },

extensions/cli/src/permissions/permissionChecker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function matchesToolPattern(
4747
return false;
4848
}
4949

50-
// Handle regular wildcard patterns like "mcp__*"
50+
// Handle regular wildcard patterns like "external_*"
5151
if (pattern.includes("*") || pattern.includes("?")) {
5252
// Escape all regex metacharacters except * and ?
5353
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");

extensions/cli/src/permissions/permissionsYamlLoader.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2-
yamlConfigToPolicies,
32
parseToolPattern,
3+
yamlConfigToPolicies,
44
} from "./permissionsYamlLoader.js";
55

66
describe("permissionsYamlLoader", () => {
@@ -46,15 +46,15 @@ describe("permissionsYamlLoader", () => {
4646

4747
it("should handle wildcard patterns", () => {
4848
const config = {
49-
allow: ["mcp__*"],
49+
allow: ["external_*"],
5050
exclude: ["*"],
5151
};
5252

5353
const policies = yamlConfigToPolicies(config);
5454

5555
expect(policies).toEqual([
5656
{ tool: "*", permission: "exclude" },
57-
{ tool: "mcp__*", permission: "allow" },
57+
{ tool: "external_*", permission: "allow" },
5858
]);
5959
});
6060

extensions/cli/src/permissions/precedenceResolver.test.ts

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js";
1+
import { getDefaultToolPolicies } from "./defaultPolicies.js";
22
import { resolvePermissionPrecedence } from "./precedenceResolver.js";
3-
import { ToolPermissionPolicy } from "./types.js";
3+
4+
const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies();
45

56
describe("precedenceResolver", () => {
67
describe("resolvePermissionPrecedence", () => {
@@ -31,7 +32,7 @@ describe("precedenceResolver", () => {
3132
it("should handle wildcard patterns", () => {
3233
const policies = resolvePermissionPrecedence({
3334
commandLineFlags: {
34-
allow: ["mcp__*"],
35+
allow: ["external_*"],
3536
exclude: ["*"],
3637
},
3738
useDefaults: false,
@@ -40,7 +41,7 @@ describe("precedenceResolver", () => {
4041

4142
expect(policies).toEqual([
4243
{ tool: "*", permission: "exclude" },
43-
{ tool: "mcp__*", permission: "allow" },
44+
{ tool: "external_*", permission: "allow" },
4445
]);
4546
});
4647

@@ -92,37 +93,11 @@ describe("precedenceResolver", () => {
9293
]);
9394
});
9495

95-
it("should apply config permissions with proper precedence", () => {
96-
const configPolicies: ToolPermissionPolicy[] = [
97-
{ tool: "Write", permission: "allow" },
98-
{ tool: "Read", permission: "ask" },
99-
];
100-
101-
const policies = resolvePermissionPrecedence({
102-
commandLineFlags: {
103-
exclude: ["Read"], // Should override config
104-
},
105-
configPermissions: configPolicies,
106-
personalSettings: false,
107-
useDefaults: false,
108-
});
109-
110-
// CLI flag should override config
111-
expect(policies[0]).toEqual({ tool: "Read", permission: "exclude" });
112-
// Config policy should be present
113-
expect(policies[1]).toEqual({ tool: "Write", permission: "allow" });
114-
});
115-
11696
it("should handle all layers with proper precedence", () => {
117-
const configPolicies: ToolPermissionPolicy[] = [
118-
{ tool: "Search", permission: "ask" },
119-
];
120-
12197
const policies = resolvePermissionPrecedence({
12298
commandLineFlags: {
12399
allow: ["Write"],
124100
},
125-
configPermissions: configPolicies,
126101
personalSettings: false,
127102
useDefaults: true,
128103
});
@@ -131,10 +106,6 @@ describe("precedenceResolver", () => {
131106
const writePolicy = policies.find((p) => p.tool === "Write");
132107
expect(writePolicy?.permission).toBe("allow");
133108

134-
// Find the Search policy - should be from config
135-
const searchPolicy = policies.find((p) => p.tool === "Search");
136-
expect(searchPolicy?.permission).toBe("ask");
137-
138109
// Should still have default policies
139110
const readPolicy = policies.find((p) => p.tool === "Read");
140111
expect(readPolicy).toBeDefined();
@@ -164,23 +135,6 @@ describe("precedenceResolver", () => {
164135
// Default policies should follow
165136
expect(policies.slice(1)).toEqual(DEFAULT_TOOL_POLICIES);
166137
});
167-
168-
it("should allow config policies to override defaults", () => {
169-
const configPolicies: ToolPermissionPolicy[] = [
170-
{ tool: "Bash", permission: "allow" },
171-
];
172-
173-
const policies = resolvePermissionPrecedence({
174-
configPermissions: configPolicies,
175-
useDefaults: true,
176-
personalSettings: false,
177-
});
178-
179-
// Config policy should override default
180-
expect(policies[0]).toEqual({ tool: "Bash", permission: "allow" });
181-
// Default policies should follow
182-
expect(policies.slice(1)).toEqual(DEFAULT_TOOL_POLICIES);
183-
});
184138
});
185139
});
186140
});

0 commit comments

Comments
 (0)