diff --git a/docs/guides/posthog-github-continuous-ai.mdx b/docs/guides/posthog-github-continuous-ai.mdx index e36e1c85f0d..1df960ba099 100644 --- a/docs/guides/posthog-github-continuous-ai.mdx +++ b/docs/guides/posthog-github-continuous-ai.mdx @@ -128,7 +128,7 @@ You only need to configure the PostHog MCP credential - it automatically handles organization - It automatically uses your default project (no project ID needed) - - If you have multiple projects, use `mcp__posthog__switch-project` to + - If you have multiple projects, use `switch-project` to change - The MCP connects via `https://mcp.posthog.com/sse` using your account context. @@ -300,7 +300,7 @@ The main workflow above focuses on analyzing session recordings to identify UX i ```bash # Get all feature flags and analyze them -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." +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." # Create cleanup issues for identified flags cn "For each problematic feature flag identified, create a GitHub issue using gh CLI: diff --git a/extensions/cli/src/permissions/README.md b/extensions/cli/src/permissions/README.md index 3251b4ce501..3caee4f2813 100644 --- a/extensions/cli/src/permissions/README.md +++ b/extensions/cli/src/permissions/README.md @@ -13,8 +13,7 @@ The system comes with sensible default policies: - Read-only tools (`readFile`, `listFiles`, `searchCode`, `fetch`) are **allowed** by default - Write operations (`writeFile`) require **confirmation** (ask) - Terminal commands (`runTerminalCommand`) require **confirmation** (ask) -- MCP tools with IDE prefix are **allowed** by default -- Other MCP tools require **confirmation** (ask) +- MCP tools and Bash require **confirmation** (ask) in TUI mode, but are **allowed** automatically in headless mode - Any unmatched tools default to **ask** ## How It Works diff --git a/extensions/cli/src/permissions/defaultPolicies.test.ts b/extensions/cli/src/permissions/defaultPolicies.test.ts index 6ea954e27e0..796940c0bf5 100644 --- a/extensions/cli/src/permissions/defaultPolicies.test.ts +++ b/extensions/cli/src/permissions/defaultPolicies.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; -import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js"; +import { getDefaultToolPolicies } from "./defaultPolicies.js"; +const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies(); describe("defaultPolicies", () => { it("should have correct permissions for read-only tools", () => { @@ -21,6 +22,13 @@ describe("defaultPolicies", () => { } }); + it("should not have prefix wildcard policies in defaults", () => { + const prefixWildcardPolicy = DEFAULT_TOOL_POLICIES.find( + (p) => p.tool.endsWith("*") && p.tool !== "*", + ); + expect(prefixWildcardPolicy).toBeUndefined(); + }); + it("should have correct permissions for write tools", () => { const writeTools = ["Write", "Edit", "MultiEdit", "Bash"]; diff --git a/extensions/cli/src/permissions/defaultPolicies.ts b/extensions/cli/src/permissions/defaultPolicies.ts index 90cf1fb02d7..4951169a598 100644 --- a/extensions/cli/src/permissions/defaultPolicies.ts +++ b/extensions/cli/src/permissions/defaultPolicies.ts @@ -4,30 +4,64 @@ import { ToolPermissionPolicy } from "./types.js"; * Default permission policies for all built-in tools. * These policies are applied in order - first match wins. */ -export const DEFAULT_TOOL_POLICIES: ToolPermissionPolicy[] = [ - // Read-only tools are generally safe to allow - { tool: "Read", permission: "allow" }, - { tool: "List", permission: "allow" }, - { tool: "Search", permission: "allow" }, - { tool: "Fetch", permission: "allow" }, +export function getDefaultToolPolicies( + isHeadless = false, +): ToolPermissionPolicy[] { + const policies: ToolPermissionPolicy[] = [ + // Write tools + { tool: "Edit", permission: "ask" }, + { tool: "MultiEdit", permission: "ask" }, + { tool: "Write", permission: "ask" }, - // Write operations should require confirmation - { tool: "Write", permission: "ask" }, - { tool: "Edit", permission: "ask" }, - { tool: "MultiEdit", permission: "ask" }, + { tool: "Checklist", permission: "allow" }, + { tool: "Diff", permission: "allow" }, + { tool: "Exit", permission: "allow" }, // Exit tool is generally safe (headless mode only) + { tool: "Fetch", permission: "allow" }, // Technically not read only but edge casey to post w query params + { tool: "List", permission: "allow" }, + { tool: "Read", permission: "allow" }, + { tool: "Search", permission: "allow" }, + { tool: "Status", permission: "allow" }, + { tool: "ReportFailure", permission: "allow" }, + { tool: "UploadArtifact", permission: "allow" }, + ]; - // Write to a checklist - { tool: "Checklist", permission: "allow" }, + // MCP and Bash are ask in TUI mode, auto in headless + if (isHeadless) { + policies.push({ tool: "Bash", permission: "allow" }); + policies.push({ tool: "*", permission: "allow" }); + } else { + policies.push({ tool: "Bash", permission: "ask" }); + policies.push({ tool: "*", permission: "ask" }); + } - // Terminal commands should require confirmation by default - { tool: "Bash", permission: "ask" }, + return policies; +} - // Exit tool is generally safe (headless mode only) - { tool: "Exit", permission: "allow" }, +// Plan mode: Complete override - exclude all write operations, allow only reads and bash +export const PLAN_MODE_POLICIES: ToolPermissionPolicy[] = [ + { tool: "Edit", permission: "exclude" }, + { tool: "MultiEdit", permission: "exclude" }, + { tool: "Write", permission: "exclude" }, - // View diff is read-only + // TODO address bash read only concerns, maybe make permissions more granular + { tool: "Bash", permission: "allow" }, + + { tool: "Checklist", permission: "allow" }, { tool: "Diff", permission: "allow" }, + { tool: "Exit", permission: "allow" }, + { tool: "Fetch", permission: "allow" }, + { tool: "List", permission: "allow" }, + { tool: "Read", permission: "allow" }, + { tool: "ReportFailure", permission: "allow" }, + { tool: "Search", permission: "allow" }, + { tool: "Status", permission: "allow" }, + { tool: "UploadArtifact", permission: "allow" }, + + // Allow MCP tools + { tool: "*", permission: "allow" }, +]; - // Default fallback - ask for any unmatched tools - { tool: "*", permission: "ask" }, +// Auto mode: Complete override - allow everything without asking +export const AUTO_MODE_POLICIES: ToolPermissionPolicy[] = [ + { tool: "*", permission: "allow" }, ]; diff --git a/extensions/cli/src/permissions/headlessPermissions.integration.test.ts b/extensions/cli/src/permissions/headlessPermissions.integration.test.ts index 0fd35d53d14..36abda954b0 100644 --- a/extensions/cli/src/permissions/headlessPermissions.integration.test.ts +++ b/extensions/cli/src/permissions/headlessPermissions.integration.test.ts @@ -1,7 +1,9 @@ -import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js"; +import { getDefaultToolPolicies } from "./defaultPolicies.js"; import { checkToolPermission } from "./permissionChecker.js"; import { resolvePermissionPrecedence } from "./precedenceResolver.js"; +const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies(); + describe("Headless Permissions Integration", () => { describe("precedence resolution", () => { it("should use default policies", () => { diff --git a/extensions/cli/src/permissions/index.ts b/extensions/cli/src/permissions/index.ts index dbb352f9087..07ef029698d 100644 --- a/extensions/cli/src/permissions/index.ts +++ b/extensions/cli/src/permissions/index.ts @@ -6,7 +6,6 @@ export type { ToolPermissions, } from "./types.js"; -export { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js"; export { checkToolPermission, matchesArguments, diff --git a/extensions/cli/src/permissions/permissionChecker.test.ts b/extensions/cli/src/permissions/permissionChecker.test.ts index e21b80f02d7..7fa1cb62d92 100644 --- a/extensions/cli/src/permissions/permissionChecker.test.ts +++ b/extensions/cli/src/permissions/permissionChecker.test.ts @@ -44,11 +44,13 @@ describe("Permission Checker", () => { }); it("should match prefix wildcards", () => { - expect(matchesToolPattern("mcp__ide__getDiagnostics", "mcp__*")).toBe( + expect( + matchesToolPattern("external_ide_getDiagnostics", "external_*"), + ).toBe(true); + expect(matchesToolPattern("external_filesystem_read", "external_*")).toBe( true, ); - expect(matchesToolPattern("mcp__filesystem__read", "mcp__*")).toBe(true); - expect(matchesToolPattern("builtin__readFile", "mcp__*")).toBe(false); + expect(matchesToolPattern("builtin_readFile", "external_*")).toBe(false); }); it("should match suffix wildcards", () => { @@ -89,7 +91,7 @@ describe("Permission Checker", () => { it("should handle wildcard patterns with special regex characters", () => { expect(matchesToolPattern("test[abc].txt", "test[abc].*")).toBe(true); expect(matchesToolPattern("test[abc]_file", "test[abc].*")).toBe(false); - expect(matchesToolPattern("mcp__tool[1]", "mcp__*")).toBe(true); + expect(matchesToolPattern("external_tool[1]", "external_*")).toBe(true); expect(matchesToolPattern("file.test.txt", "*.test.*")).toBe(true); expect(matchesToolPattern("(tool)_name", "(tool)*")).toBe(true); expect(matchesToolPattern("tool+plus_extra", "tool+plus*")).toBe(true); @@ -417,17 +419,17 @@ describe("Permission Checker", () => { it("should match wildcard patterns", () => { const permissions: ToolPermissions = { policies: [ - { tool: "mcp__*", permission: "ask" }, + { tool: "external_*", permission: "ask" }, { tool: "*", permission: "allow" }, ], }; - const mcpResult = checkToolPermission( - { name: "mcp__ide__getDiagnostics", arguments: {} }, + const externalResult = checkToolPermission( + { name: "external_ide_getDiagnostics", arguments: {} }, permissions, ); - expect(mcpResult.permission).toBe("ask"); - expect(mcpResult.matchedPolicy?.tool).toBe("mcp__*"); + expect(externalResult.permission).toBe("ask"); + expect(externalResult.matchedPolicy?.tool).toBe("external_*"); const builtinResult = checkToolPermission( { name: "readFile", arguments: { path: "/test.txt" } }, diff --git a/extensions/cli/src/permissions/permissionChecker.ts b/extensions/cli/src/permissions/permissionChecker.ts index 093137d30f6..3b5ad0ae20a 100644 --- a/extensions/cli/src/permissions/permissionChecker.ts +++ b/extensions/cli/src/permissions/permissionChecker.ts @@ -47,7 +47,7 @@ export function matchesToolPattern( return false; } - // Handle regular wildcard patterns like "mcp__*" + // Handle regular wildcard patterns like "external_*" if (pattern.includes("*") || pattern.includes("?")) { // Escape all regex metacharacters except * and ? const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&"); diff --git a/extensions/cli/src/permissions/permissionsYamlLoader.test.ts b/extensions/cli/src/permissions/permissionsYamlLoader.test.ts index a7fcfce8d15..52cb96f1ca3 100644 --- a/extensions/cli/src/permissions/permissionsYamlLoader.test.ts +++ b/extensions/cli/src/permissions/permissionsYamlLoader.test.ts @@ -1,6 +1,6 @@ import { - yamlConfigToPolicies, parseToolPattern, + yamlConfigToPolicies, } from "./permissionsYamlLoader.js"; describe("permissionsYamlLoader", () => { @@ -46,7 +46,7 @@ describe("permissionsYamlLoader", () => { it("should handle wildcard patterns", () => { const config = { - allow: ["mcp__*"], + allow: ["external_*"], exclude: ["*"], }; @@ -54,7 +54,7 @@ describe("permissionsYamlLoader", () => { expect(policies).toEqual([ { tool: "*", permission: "exclude" }, - { tool: "mcp__*", permission: "allow" }, + { tool: "external_*", permission: "allow" }, ]); }); diff --git a/extensions/cli/src/permissions/precedenceResolver.test.ts b/extensions/cli/src/permissions/precedenceResolver.test.ts index e7c9e987501..d8e7c3e5fef 100644 --- a/extensions/cli/src/permissions/precedenceResolver.test.ts +++ b/extensions/cli/src/permissions/precedenceResolver.test.ts @@ -1,6 +1,7 @@ -import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js"; +import { getDefaultToolPolicies } from "./defaultPolicies.js"; import { resolvePermissionPrecedence } from "./precedenceResolver.js"; -import { ToolPermissionPolicy } from "./types.js"; + +const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies(); describe("precedenceResolver", () => { describe("resolvePermissionPrecedence", () => { @@ -31,7 +32,7 @@ describe("precedenceResolver", () => { it("should handle wildcard patterns", () => { const policies = resolvePermissionPrecedence({ commandLineFlags: { - allow: ["mcp__*"], + allow: ["external_*"], exclude: ["*"], }, useDefaults: false, @@ -40,7 +41,7 @@ describe("precedenceResolver", () => { expect(policies).toEqual([ { tool: "*", permission: "exclude" }, - { tool: "mcp__*", permission: "allow" }, + { tool: "external_*", permission: "allow" }, ]); }); @@ -92,37 +93,11 @@ describe("precedenceResolver", () => { ]); }); - it("should apply config permissions with proper precedence", () => { - const configPolicies: ToolPermissionPolicy[] = [ - { tool: "Write", permission: "allow" }, - { tool: "Read", permission: "ask" }, - ]; - - const policies = resolvePermissionPrecedence({ - commandLineFlags: { - exclude: ["Read"], // Should override config - }, - configPermissions: configPolicies, - personalSettings: false, - useDefaults: false, - }); - - // CLI flag should override config - expect(policies[0]).toEqual({ tool: "Read", permission: "exclude" }); - // Config policy should be present - expect(policies[1]).toEqual({ tool: "Write", permission: "allow" }); - }); - it("should handle all layers with proper precedence", () => { - const configPolicies: ToolPermissionPolicy[] = [ - { tool: "Search", permission: "ask" }, - ]; - const policies = resolvePermissionPrecedence({ commandLineFlags: { allow: ["Write"], }, - configPermissions: configPolicies, personalSettings: false, useDefaults: true, }); @@ -131,10 +106,6 @@ describe("precedenceResolver", () => { const writePolicy = policies.find((p) => p.tool === "Write"); expect(writePolicy?.permission).toBe("allow"); - // Find the Search policy - should be from config - const searchPolicy = policies.find((p) => p.tool === "Search"); - expect(searchPolicy?.permission).toBe("ask"); - // Should still have default policies const readPolicy = policies.find((p) => p.tool === "Read"); expect(readPolicy).toBeDefined(); @@ -164,23 +135,6 @@ describe("precedenceResolver", () => { // Default policies should follow expect(policies.slice(1)).toEqual(DEFAULT_TOOL_POLICIES); }); - - it("should allow config policies to override defaults", () => { - const configPolicies: ToolPermissionPolicy[] = [ - { tool: "Bash", permission: "allow" }, - ]; - - const policies = resolvePermissionPrecedence({ - configPermissions: configPolicies, - useDefaults: true, - personalSettings: false, - }); - - // Config policy should override default - expect(policies[0]).toEqual({ tool: "Bash", permission: "allow" }); - // Default policies should follow - expect(policies.slice(1)).toEqual(DEFAULT_TOOL_POLICIES); - }); }); }); }); diff --git a/extensions/cli/src/permissions/precedenceResolver.ts b/extensions/cli/src/permissions/precedenceResolver.ts index c559a036285..84c4539c678 100644 --- a/extensions/cli/src/permissions/precedenceResolver.ts +++ b/extensions/cli/src/permissions/precedenceResolver.ts @@ -1,6 +1,4 @@ -import { logger } from "../util/logger.js"; - -import { DEFAULT_TOOL_POLICIES } from "./defaultPolicies.js"; +import { getDefaultToolPolicies } from "./defaultPolicies.js"; import { loadPermissionsYaml, yamlConfigToPolicies, @@ -14,11 +12,10 @@ export interface PermissionSources { ask?: string[]; exclude?: string[]; }; - /** Config.yaml permissions - second precedence (not implemented yet) */ - configPermissions?: ToolPermissionPolicy[]; /** ~/.continue/permissions.yaml - third precedence */ personalSettings?: boolean; // Whether to load from permissions.yaml /** Default policies - lowest precedence */ + isHeadless?: boolean; useDefaults?: boolean; } @@ -36,61 +33,30 @@ export interface PermissionSources { export function resolvePermissionPrecedence( sources: PermissionSources, ): ToolPermissionPolicy[] { - const layers: { - name: string; - policies: ToolPermissionPolicy[]; - }[] = []; + const policies: ToolPermissionPolicy[] = []; - // Layer 4: Default policies (lowest precedence) - if (sources.useDefaults !== false) { - layers.push({ - name: "defaults", - policies: [...DEFAULT_TOOL_POLICIES], - }); + // Layer 1: Command line flags (highest precedence) + if (sources.commandLineFlags) { + const cliPolicies = commandLineFlagsToPolicies(sources.commandLineFlags); + policies.push(...cliPolicies); } - // Layer 3: Personal settings from ~/.continue/permissions.yaml + // Layer 2: Personal settings from ~/.continue/permissions.yaml if (sources.personalSettings !== false) { const yamlConfig = loadPermissionsYaml(); if (yamlConfig) { const yamlPolicies = yamlConfigToPolicies(yamlConfig); - if (yamlPolicies.length > 0) { - layers.push({ - name: "personal-settings", - policies: yamlPolicies, - }); - } + policies.push(...yamlPolicies); } } - // Layer 2: Config permissions (when implemented) - if (sources.configPermissions) { - layers.push({ - name: "config", - policies: sources.configPermissions, - }); - } - - // Layer 1: Command line flags (highest precedence) - if (sources.commandLineFlags) { - const cliPolicies = commandLineFlagsToPolicies(sources.commandLineFlags); - if (cliPolicies.length > 0) { - layers.push({ - name: "cli-flags", - policies: cliPolicies, - }); - } + // Layer 3: Default policies (lowest precedence) + if (sources.useDefaults !== false) { + const defaultPolicies = getDefaultToolPolicies(sources.isHeadless); + policies.push(...defaultPolicies); } - // Combine layers with proper precedence - const combinedPolicies = combineLayersWithPrecedence(layers); - - logger.debug("Resolved permission precedence", { - layers: layers.map((l) => ({ name: l.name, count: l.policies.length })), - totalPolicies: combinedPolicies.length, - }); - - return combinedPolicies; + return policies; } /** @@ -127,24 +93,3 @@ function commandLineFlagsToPolicies(flags: { return policies; } - -/** - * Combines permission layers with proper precedence. - * Higher precedence layers come later in the array and are prepended to the result. - */ -function combineLayersWithPrecedence( - layers: Array<{ name: string; policies: ToolPermissionPolicy[] }>, -): ToolPermissionPolicy[] { - // Start with empty array - let combined: ToolPermissionPolicy[] = []; - - // Add layers in order (lowest to highest precedence) - // Since we're building from lowest to highest, and we want first-match-wins, - // we prepend higher precedence policies - for (const layer of layers) { - // Prepend this layer's policies (higher precedence at the front) - combined = [...layer.policies, ...combined]; - } - - return combined; -} diff --git a/extensions/cli/src/services/ToolPermissionService.integration.test.ts b/extensions/cli/src/services/ToolPermissionService.integration.test.ts index 12362bb4e65..086452570a4 100644 --- a/extensions/cli/src/services/ToolPermissionService.integration.test.ts +++ b/extensions/cli/src/services/ToolPermissionService.integration.test.ts @@ -1,7 +1,9 @@ -import { DEFAULT_TOOL_POLICIES } from "../permissions/defaultPolicies.js"; +import { getDefaultToolPolicies } from "../permissions/defaultPolicies.js"; import { ToolPermissionService } from "./ToolPermissionService.js"; +const DEFAULT_TOOL_POLICIES = getDefaultToolPolicies(); + describe("ToolPermissionService E2E", () => { let service: ToolPermissionService; diff --git a/extensions/cli/src/services/ToolPermissionService.modes.test.ts b/extensions/cli/src/services/ToolPermissionService.modes.test.ts index 1df15362225..2afbe134f7c 100644 --- a/extensions/cli/src/services/ToolPermissionService.modes.test.ts +++ b/extensions/cli/src/services/ToolPermissionService.modes.test.ts @@ -40,20 +40,15 @@ describe("ToolPermissionService - Mode Functionality", () => { policies.some((p) => p.tool === "Read" && p.permission === "allow"), ).toBe(true); expect( - policies.some((p) => p.tool === "Grep" && p.permission === "allow"), + policies.some((p) => p.tool === "Search" && p.permission === "allow"), ).toBe(true); expect( - policies.some((p) => p.tool === "LS" && p.permission === "allow"), + policies.some((p) => p.tool === "List" && p.permission === "allow"), ).toBe(true); - // Should allow MCP tools + // Plan mode allows all other tools (including MCP) with wildcard expect( - policies.some((p) => p.tool === "mcp:*" && p.permission === "allow"), - ).toBe(true); - - // Should have an exclusion policy for all other tools as fallback - expect( - policies.some((p) => p.tool === "*" && p.permission === "exclude"), + policies.some((p) => p.tool === "*" && p.permission === "allow"), ).toBe(true); }); @@ -271,10 +266,9 @@ describe("ToolPermissionService - Mode Functionality", () => { // Mode policies should come first, and there should be policies expect(policies.length).toBeGreaterThan(0); - // Should have MCP and wildcard exclusion policies - expect(policies.some((p) => p.tool === "mcp:*")).toBe(true); + // Plan mode has wildcard allow policy for MCP and other tools expect( - policies.some((p) => p.tool === "*" && p.permission === "exclude"), + policies.some((p) => p.tool === "*" && p.permission === "allow"), ).toBe(true); }); }); diff --git a/extensions/cli/src/services/ToolPermissionService.real.test.ts b/extensions/cli/src/services/ToolPermissionService.real.test.ts index d4d3b33acf0..e412eae1065 100644 --- a/extensions/cli/src/services/ToolPermissionService.real.test.ts +++ b/extensions/cli/src/services/ToolPermissionService.real.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { checkToolPermission } from "../permissions/permissionChecker.js"; @@ -76,16 +76,17 @@ describe("ToolPermissionService - Real Tool Permission Test", () => { expect(result.permission).toBe("allow"); }); - it("should deny unknown tools in plan mode (wildcard exclude)", () => { + it("should allow unknown tools in plan mode (for MCP tools via wildcard)", () => { const permissions = service.getPermissions(); const toolCall = { - name: "unknown_write_tool", + name: "some_mcp_tool", arguments: {}, }; const result = checkToolPermission(toolCall, permissions); - console.log(`unknown_write_tool permission check result:`, result); - expect(result.permission).toBe("exclude"); + console.log(`some_mcp_tool permission check result:`, result); + // Plan mode allows MCP and other non-write tools via wildcard + expect(result.permission).toBe("allow"); }); }); diff --git a/extensions/cli/src/services/ToolPermissionService.test.ts b/extensions/cli/src/services/ToolPermissionService.test.ts index 2f63ab4fd00..d58cc47e185 100644 --- a/extensions/cli/src/services/ToolPermissionService.test.ts +++ b/extensions/cli/src/services/ToolPermissionService.test.ts @@ -59,6 +59,7 @@ describe("ToolPermissionService", () => { commandLineFlags: overrides, personalSettings: true, useDefaults: true, + isHeadless: false, }); }); @@ -123,13 +124,13 @@ describe("ToolPermissionService", () => { // Should allow read tools and bash expect(policies).toContainEqual({ tool: "Bash", permission: "allow" }); expect(policies).toContainEqual({ tool: "Read", permission: "allow" }); - expect(policies).toContainEqual({ tool: "Grep", permission: "allow" }); - expect(policies).toContainEqual({ tool: "LS", permission: "allow" }); + expect(policies).toContainEqual({ tool: "Search", permission: "allow" }); + expect(policies).toContainEqual({ tool: "List", permission: "allow" }); - // Should have wildcard exclusion at the end + // Plan mode has wildcard allow at the end for MCP and other tools expect(policies[policies.length - 1]).toEqual({ tool: "*", - permission: "exclude", + permission: "allow", }); }); @@ -361,6 +362,83 @@ describe("ToolPermissionService", () => { }); }); + describe("Headless Mode Behavior", () => { + test("should pass isHeadless=true to precedence resolver", () => { + vi.mocked(precedenceResolver.resolvePermissionPrecedence).mockReturnValue( + [], + ); + + const state = service.initializeSync({ isHeadless: true }); + + expect( + precedenceResolver.resolvePermissionPrecedence, + ).toHaveBeenCalledWith(expect.objectContaining({ isHeadless: true })); + expect(state.isHeadless).toBe(true); + }); + + test("should pass isHeadless=false to precedence resolver", () => { + vi.mocked(precedenceResolver.resolvePermissionPrecedence).mockReturnValue( + [], + ); + + const state = service.initializeSync({ isHeadless: false }); + + expect( + precedenceResolver.resolvePermissionPrecedence, + ).toHaveBeenCalledWith(expect.objectContaining({ isHeadless: false })); + expect(state.isHeadless).toBe(false); + }); + + test("should apply plan mode policies in headless plan mode", () => { + const state = service.initializeSync({ + isHeadless: true, + mode: "plan", + }); + + // Plan mode should exclude write tools + const writeExcludeIndex = state.permissions.policies.findIndex( + (p) => p.tool === "Write" && p.permission === "exclude", + ); + expect(writeExcludeIndex).toBeGreaterThanOrEqual(0); + + // Plan mode allows all other tools via wildcard + expect(state.permissions.policies).toContainEqual({ + tool: "*", + permission: "allow", + }); + expect(state.isHeadless).toBe(true); + }); + + test("should apply auto mode policies in headless auto mode", () => { + const state = service.initializeSync({ + isHeadless: true, + mode: "auto", + }); + + // Auto mode has wildcard allow + expect(state.permissions.policies).toContainEqual({ + tool: "*", + permission: "allow", + }); + expect(state.isHeadless).toBe(true); + }); + + test("should have modePolicyCount of 0 in normal mode regardless of headless", () => { + vi.mocked(precedenceResolver.resolvePermissionPrecedence).mockReturnValue( + [], + ); + + const headlessState = service.initializeSync({ isHeadless: true }); + expect(headlessState.modePolicyCount).toBe(0); + + const nonHeadlessService = new ToolPermissionService(); + const nonHeadlessState = nonHeadlessService.initializeSync({ + isHeadless: false, + }); + expect(nonHeadlessState.modePolicyCount).toBe(0); + }); + }); + describe("reloadPermissions", () => { test("should reload permissions from files in normal mode", async () => { const mockPolicies = [ diff --git a/extensions/cli/src/services/ToolPermissionService.ts b/extensions/cli/src/services/ToolPermissionService.ts index 4e0802272b4..93baa71858d 100644 --- a/extensions/cli/src/services/ToolPermissionService.ts +++ b/extensions/cli/src/services/ToolPermissionService.ts @@ -1,3 +1,7 @@ +import { + AUTO_MODE_POLICIES, + PLAN_MODE_POLICIES, +} from "src/permissions/defaultPolicies.js"; import { ALL_BUILT_IN_TOOLS } from "src/tools/allBuiltIns.js"; import { ensurePermissionsYamlExists } from "../permissions/permissionsYamlLoader.js"; @@ -168,40 +172,12 @@ export class ToolPermissionService private generateModePolicies(): ToolPermissionPolicy[] { switch (this.currentState.currentMode) { case "plan": - // Plan mode: Complete override - exclude all write operations, allow only reads and bash - return [ - // Exclude all write tools with absolute priority - { tool: "Write", permission: "exclude" }, - { tool: "Edit", permission: "exclude" }, - { tool: "MultiEdit", permission: "exclude" }, - { tool: "NotebookEdit", permission: "exclude" }, - // Allow all read tools and bash - { tool: "Bash", permission: "allow" }, - { tool: "Read", permission: "allow" }, - { tool: "List", permission: "allow" }, - { tool: "Search", permission: "allow" }, - { tool: "Fetch", permission: "allow" }, - { tool: "Diff", permission: "allow" }, - { tool: "Checklist", permission: "allow" }, - { tool: "NotebookRead", permission: "allow" }, - { tool: "LS", permission: "allow" }, - { tool: "Glob", permission: "allow" }, - { tool: "Grep", permission: "allow" }, - { tool: "WebFetch", permission: "allow" }, - { tool: "WebSearch", permission: "allow" }, - // Allow MCP tools (no way to know if they're read only but shouldn't disable mcp usage in plan) - { tool: "mcp:*", permission: "allow" }, - // Default: exclude everything else to ensure no writes - { tool: "*", permission: "exclude" }, - ]; - + return [...PLAN_MODE_POLICIES]; case "auto": - // Auto mode: Complete override - allow everything without asking - return [{ tool: "*", permission: "allow" }]; - + return [...AUTO_MODE_POLICIES]; case "normal": default: - // Normal mode: No mode policies, use existing configuration + // Normal mode uses the more nuanced policy loading return []; } } @@ -241,15 +217,16 @@ export class ToolPermissionService this.currentState.currentMode === "auto" ) { // For plan and auto modes, use ONLY mode policies (absolute override) - allPolicies = modePolicies; + allPolicies = [...modePolicies]; } else { - // Normal mode: combine mode policies with user configuration + // Normal mode: combine headless + mode policies with user configuration const compiledPolicies = resolvePermissionPrecedence({ commandLineFlags: runtimeOverrides, personalSettings: true, // Enable loading from ~/.continue/permissions.yaml useDefaults: true, + isHeadless: this.currentState.isHeadless, }); - allPolicies = [...modePolicies, ...compiledPolicies]; + allPolicies = [...compiledPolicies]; } this.setState({ @@ -331,7 +308,6 @@ export class ToolPermissionService }); this.emit("modeChanged", newMode, currentMode); - // Regenerate policies with the new mode const modePolicies = this.generateModePolicies(); // For plan and auto modes, use ONLY mode policies (absolute override) @@ -339,7 +315,7 @@ export class ToolPermissionService let allPolicies: ToolPermissionPolicy[]; if (newMode === "plan" || newMode === "auto") { // Absolute override: ignore all user configuration - allPolicies = modePolicies; + allPolicies = [...modePolicies]; } else { // Normal mode: restore original policies if we have them if (this.currentState.originalPolicies) { @@ -351,7 +327,7 @@ export class ToolPermissionService this.currentState.originalPolicies.policies.slice( originalModePolicyCount, ); - allPolicies = [...modePolicies, ...originalNonModePolicies]; + allPolicies = [...originalNonModePolicies]; logger.debug( `Restored ${originalNonModePolicies.length} original user policies`, ); @@ -363,7 +339,7 @@ export class ToolPermissionService existingPolicies.length > previousModePolicyCount ? existingPolicies.slice(previousModePolicyCount) : []; - allPolicies = [...modePolicies, ...nonModePolicies]; + allPolicies = [...nonModePolicies]; } } @@ -412,20 +388,15 @@ export class ToolPermissionService useDefaults: true, }); - // Generate mode-specific policies (should be empty for normal mode) - const modePolicies = this.generateModePolicies(); - // Combine mode policies with freshly loaded user policies - const allPolicies = [...modePolicies, ...freshPolicies]; + const allPolicies = [...freshPolicies]; this.setState({ permissions: { policies: allPolicies }, - modePolicyCount: modePolicies.length, + modePolicyCount: 0, }); - logger.debug( - `Reloaded permissions: ${freshPolicies.length} user policies, ${modePolicies.length} mode policies`, - ); + logger.debug(`Reloaded permissions: ${freshPolicies.length} user policies`); } /** @@ -434,24 +405,4 @@ export class ToolPermissionService override isReady(): boolean { return true; } - - public getAvailableModes(): Array<{ - mode: PermissionMode; - description: string; - }> { - return [ - { - mode: "normal", - description: "Default mode - follows configured permission policies", - }, - { - mode: "plan", - description: "Planning mode - only allow read-only tools for analysis", - }, - { - mode: "auto", - description: "Automatically allow all tools without asking", - }, - ]; - } } diff --git a/extensions/cli/src/tools/allBuiltIns.ts b/extensions/cli/src/tools/allBuiltIns.ts index 4e2f4a6d92c..4751cf3e9d1 100644 --- a/extensions/cli/src/tools/allBuiltIns.ts +++ b/extensions/cli/src/tools/allBuiltIns.ts @@ -9,24 +9,27 @@ import { readFileTool } from "./readFile.js"; import { reportFailureTool } from "./reportFailure.js"; import { runTerminalCommandTool } from "./runTerminalCommand.js"; import { searchCodeTool } from "./searchCode.js"; -import type { Tool } from "./types.js"; +import { statusTool } from "./status.js"; import { uploadArtifactTool } from "./uploadArtifact.js"; +import { viewDiffTool } from "./viewDiff.js"; import { writeChecklistTool } from "./writeChecklist.js"; import { writeFileTool } from "./writeFile.js"; // putting in here for circular import issue -export const ALL_BUILT_IN_TOOLS: Tool[] = [ - readFileTool, +export const ALL_BUILT_IN_TOOLS = [ editTool, - multiEditTool, - writeFileTool, + exitTool, + fetchTool, listFilesTool, - searchCodeTool, + multiEditTool, + readFileTool, + reportFailureTool, runTerminalCommandTool, - fetchTool, - writeChecklistTool, + searchCodeTool, + statusTool, SUBAGENT_TOOL_META, - exitTool, - reportFailureTool, uploadArtifactTool, + viewDiffTool, + writeChecklistTool, + writeFileTool, ]; diff --git a/extensions/cli/src/tools/index.tsx b/extensions/cli/src/tools/index.tsx index 8ec0aca3574..b6db812344c 100644 --- a/extensions/cli/src/tools/index.tsx +++ b/extensions/cli/src/tools/index.tsx @@ -185,7 +185,7 @@ export function convertToolToChatCompletionTool( export function convertMcpToolToContinueTool(mcpTool: MCPTool): Tool { return { name: mcpTool.name, - displayName: mcpTool.name.replace("mcp__", "").replace("ide__", ""), + displayName: mcpTool.name, description: mcpTool.description ?? "", parameters: { type: "object",