From 706fd253fa32363964b7f6f909cb47d0c105b5be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 05:35:18 +0000 Subject: [PATCH 1/2] refactor(autonomy): replace capability-based permission mapping with hardcoded permission sets per agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each harness writer now defines three explicit permission sets (rigid, sensible-defaults, max-autonomy) instead of deriving them from abstract capability flags. This removes the brittle capability→tool-name mapping layer that proved difficult to maintain correctly across nine harnesses. Key changes: - PermissionPolicy simplified to just { profile } — capabilities field removed - autonomy.ts facet recipes simplified to pass { profile } directly - All harness writers updated with hardcoded permission sets per profile - Claude Code MCP rules now forward wildcard allowedTools as mcp____* (previously wildcard servers were silently skipped, causing MCP tools to always prompt for approval) - OpenCode permission rule sets inlined from the now-removed getHarnessPermissionRules() helper https://claude.ai/code/session_01RgRDwsqvUZhuz2hEjkBK53 --- packages/core/src/catalog/catalog.spec.ts | 11 +- packages/core/src/catalog/facets/autonomy.ts | 66 +------ packages/core/src/index.ts | 5 +- packages/core/src/resolver.spec.ts | 26 +-- packages/core/src/resolver.ts | 6 +- packages/core/src/types.ts | 20 --- packages/harnesses/src/permission-policy.ts | 166 +----------------- .../harnesses/src/writers/claude-code.spec.ts | 60 ++----- packages/harnesses/src/writers/claude-code.ts | 49 ++++-- packages/harnesses/src/writers/cline.spec.ts | 42 +---- .../harnesses/src/writers/copilot.spec.ts | 44 +---- packages/harnesses/src/writers/copilot.ts | 42 ++--- packages/harnesses/src/writers/cursor.spec.ts | 42 +---- packages/harnesses/src/writers/cursor.ts | 68 +++---- packages/harnesses/src/writers/kiro.spec.ts | 42 +---- packages/harnesses/src/writers/kiro.ts | 49 +++--- .../harnesses/src/writers/opencode.spec.ts | 42 +---- packages/harnesses/src/writers/opencode.ts | 153 +++++++++++++++- .../harnesses/src/writers/roo-code.spec.ts | 44 +---- packages/harnesses/src/writers/roo-code.ts | 35 ++-- .../harnesses/src/writers/universal.spec.ts | 42 +---- packages/harnesses/src/writers/universal.ts | 75 ++++---- .../harnesses/src/writers/windsurf.spec.ts | 47 +---- packages/harnesses/src/writers/windsurf.ts | 76 +++----- 24 files changed, 391 insertions(+), 861 deletions(-) diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 7e66d9f..c502acb 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -486,16 +486,7 @@ describe("catalog", () => { expect(provision).toBeDefined(); expect(provision!.config).toEqual({ - profile: "sensible-defaults", - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } + profile: "sensible-defaults" }); }); }); diff --git a/packages/core/src/catalog/facets/autonomy.ts b/packages/core/src/catalog/facets/autonomy.ts index c1c0067..d42f5ef 100644 --- a/packages/core/src/catalog/facets/autonomy.ts +++ b/packages/core/src/catalog/facets/autonomy.ts @@ -1,62 +1,4 @@ -import type { - AutonomyCapability, - Facet, - PermissionDecision, - PermissionPolicy -} from "../../types.js"; - -const ALL_CAPABILITIES: AutonomyCapability[] = [ - "read", - "edit_write", - "search_list", - "bash_safe", - "bash_unsafe", - "web", - "task_agent" -]; - -function capabilityMap( - defaultDecision: PermissionDecision, - overrides: Partial> = {} -): Record { - return Object.fromEntries( - ALL_CAPABILITIES.map((capability) => [ - capability, - overrides[capability] ?? defaultDecision - ]) - ) as Record; -} - -function autonomyPolicy( - profile: PermissionPolicy["profile"] -): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: capabilityMap("ask") - }; - case "sensible-defaults": - return { - profile, - capabilities: capabilityMap("ask", { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - task_agent: "allow", - web: "ask" - }) - }; - case "max-autonomy": - return { - profile, - capabilities: capabilityMap("allow", { - web: "ask" - }) - }; - } -} +import type { Facet } from "../../types.js"; export const autonomyFacet: Facet = { id: "autonomy", @@ -74,7 +16,7 @@ export const autonomyFacet: Facet = { recipe: [ { writer: "permission-policy", - config: autonomyPolicy("rigid") + config: { profile: "rigid" } } ] }, @@ -86,7 +28,7 @@ export const autonomyFacet: Facet = { recipe: [ { writer: "permission-policy", - config: autonomyPolicy("sensible-defaults") + config: { profile: "sensible-defaults" } } ] }, @@ -98,7 +40,7 @@ export const autonomyFacet: Facet = { recipe: [ { writer: "permission-policy", - config: autonomyPolicy("max-autonomy") + config: { profile: "max-autonomy" } } ] } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0054403..42251d1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,10 +15,7 @@ export { type ExternalSkill, type GitHook, type PermissionPolicy, - type AutonomyProfile, - type AutonomyCapability, - type PermissionDecision, - type PermissionRule + type AutonomyProfile } from "./types.js"; export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 9ff5866..be1775b 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -580,7 +580,7 @@ describe("resolve", () => { }); describe("autonomy permission policy", () => { - it("adds a capability-based permission policy to LogicalConfig and keeps web access on ask", async () => { + it("adds a profile-based permission policy to LogicalConfig", async () => { const userConfig: UserConfig = { choices: { autonomy: "rigid" } }; @@ -589,20 +589,11 @@ describe("resolve", () => { expect(result).toHaveProperty("permission_policy"); expect((result as Record).permission_policy).toEqual({ - profile: "rigid", - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } + profile: "rigid" }); }); - it("uses curated built-in defaults for the sensible-defaults autonomy profile", async () => { + it("uses the sensible-defaults autonomy profile", async () => { const userConfig: UserConfig = { choices: { autonomy: "sensible-defaults" } }; @@ -610,16 +601,7 @@ describe("resolve", () => { const result = await resolve(userConfig, catalog, registry); expect(result.permission_policy).toEqual({ - profile: "sensible-defaults", - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } + profile: "sensible-defaults" }); }); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 2ba4abf..a12a19e 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -179,11 +179,7 @@ function mergePermissionPolicy( return { ...existing, - ...incoming, - capabilities: { - ...existing.capabilities, - ...incoming.capabilities - } + ...incoming }; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 0d1050b..a59261d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -68,28 +68,8 @@ export interface GitHook { export type AutonomyProfile = "rigid" | "sensible-defaults" | "max-autonomy"; -export type PermissionDecision = "ask" | "allow" | "deny"; - -export type AutonomyCapability = - | "read" - | "edit_write" - | "search_list" - | "bash_safe" - | "bash_unsafe" - | "web" - | "task_agent"; - -/** - * @deprecated Harness-specific tool-level rules are no longer produced by core. - * Kept temporarily as a compatibility type for downstream packages. - */ -export type PermissionRule = - | PermissionDecision - | Record; - export interface PermissionPolicy extends Record { profile: AutonomyProfile; - capabilities: Record; } export interface LogicalConfig extends Record { diff --git a/packages/harnesses/src/permission-policy.ts b/packages/harnesses/src/permission-policy.ts index 883bbeb..0811737 100644 --- a/packages/harnesses/src/permission-policy.ts +++ b/packages/harnesses/src/permission-policy.ts @@ -1,121 +1,4 @@ -import type { - AutonomyCapability, - LogicalConfig, - PermissionDecision, - PermissionRule -} from "@codemcp/ade-core"; - -const SENSIBLE_DEFAULTS_RULES: Record = { - read: { - "*": "allow", - "*.env": "deny", - "*.env.*": "deny", - "*.env.example": "allow" - }, - edit: "allow", - glob: "allow", - grep: "allow", - list: "allow", - lsp: "allow", - task: "allow", - todoread: "deny", - todowrite: "deny", - skill: "deny", - webfetch: "ask", - websearch: "ask", - codesearch: "ask", - bash: { - "*": "deny", - "grep *": "allow", - "rg *": "allow", - "find *": "allow", - "fd *": "allow", - ls: "allow", - "ls *": "allow", - "cat *": "allow", - "head *": "allow", - "tail *": "allow", - "wc *": "allow", - "sort *": "allow", - "uniq *": "allow", - "diff *": "allow", - "echo *": "allow", - "printf *": "allow", - pwd: "allow", - "which *": "allow", - "type *": "allow", - whoami: "allow", - date: "allow", - "date *": "allow", - env: "allow", - "tree *": "allow", - "file *": "allow", - "stat *": "allow", - "readlink *": "allow", - "realpath *": "allow", - "dirname *": "allow", - "basename *": "allow", - "sed *": "allow", - "awk *": "allow", - "cut *": "allow", - "tr *": "allow", - "tee *": "allow", - "xargs *": "allow", - "jq *": "allow", - "yq *": "allow", - "mkdir *": "allow", - "touch *": "allow", - "cp *": "ask", - "mv *": "ask", - "ln *": "ask", - "npm *": "ask", - "node *": "ask", - "pip *": "ask", - "python *": "ask", - "python3 *": "ask", - "rm *": "deny", - "rmdir *": "deny", - "curl *": "deny", - "wget *": "deny", - "chmod *": "deny", - "chown *": "deny", - "sudo *": "deny", - "su *": "deny", - "sh *": "deny", - "bash *": "deny", - "zsh *": "deny", - "eval *": "deny", - "exec *": "deny", - "source *": "deny", - ". *": "deny", - "nohup *": "deny", - "dd *": "deny", - "mkfs *": "deny", - "mount *": "deny", - "umount *": "deny", - "kill *": "deny", - "killall *": "deny", - "pkill *": "deny", - "nc *": "deny", - "ncat *": "deny", - "ssh *": "deny", - "scp *": "deny", - "rsync *": "deny", - "docker *": "deny", - "kubectl *": "deny", - "systemctl *": "deny", - "service *": "deny", - "crontab *": "deny", - reboot: "deny", - "shutdown *": "deny", - "passwd *": "deny", - "useradd *": "deny", - "userdel *": "deny", - "iptables *": "deny" - }, - external_directory: "deny", - doom_loop: "deny" -}; +import type { LogicalConfig } from "@codemcp/ade-core"; export function getAutonomyProfile(config: LogicalConfig) { return config.permission_policy?.profile; @@ -124,50 +7,3 @@ export function getAutonomyProfile(config: LogicalConfig) { export function hasPermissionPolicy(config: LogicalConfig): boolean { return config.permission_policy !== undefined; } - -export function getCapabilityDecision( - config: LogicalConfig, - capability: AutonomyCapability -): PermissionDecision | undefined { - return config.permission_policy?.capabilities?.[capability]; -} - -export function allowsCapability( - config: LogicalConfig, - capability: AutonomyCapability -): boolean { - return getCapabilityDecision(config, capability) === "allow"; -} - -export function keepsWebOnAsk(config: LogicalConfig): boolean { - return getCapabilityDecision(config, "web") === "ask"; -} - -export function getHarnessPermissionRules( - config: LogicalConfig -): Record | undefined { - switch (config.permission_policy?.profile) { - case "rigid": - return { - "*": "ask", - webfetch: "ask", - websearch: "ask", - codesearch: "ask", - external_directory: "deny", - doom_loop: "deny" - }; - case "sensible-defaults": - return SENSIBLE_DEFAULTS_RULES; - case "max-autonomy": - return { - "*": "allow", - webfetch: "ask", - websearch: "ask", - codesearch: "ask", - external_directory: "deny", - doom_loop: "deny" - }; - default: - return undefined; - } -} diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index 656b17c..a32b3da 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -11,47 +11,7 @@ import { claudeCodeWriter } from "./claude-code.js"; import { writeInlineSkills } from "../util.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("claudeCodeWriter", () => { @@ -160,7 +120,7 @@ describe("claudeCodeWriter", () => { ); }); - it("does not invent wildcard MCP permission rules", async () => { + it("forwards wildcard MCP permission rules for servers without explicit allowedTools", async () => { const config: LogicalConfig = { mcp_servers: [ { @@ -182,10 +142,10 @@ describe("claudeCodeWriter", () => { const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); const settings = JSON.parse(raw); - expect(settings.permissions.allow ?? []).toEqual([]); + expect(settings.permissions.allow).toContain("mcp__workflows__*"); }); - it("keeps web on ask for rigid autonomy without broad built-in allows", async () => { + it("allows only Read for rigid autonomy while asking for everything else", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], @@ -201,9 +161,17 @@ describe("claudeCodeWriter", () => { const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); const settings = JSON.parse(raw); - expect(settings.permissions.allow ?? []).toEqual([]); + expect(settings.permissions.allow).toEqual(["Read"]); expect(settings.permissions.ask).toEqual( - expect.arrayContaining(["WebFetch", "WebSearch"]) + expect.arrayContaining([ + "Edit", + "Glob", + "Grep", + "Bash", + "WebFetch", + "WebSearch", + "TodoWrite" + ]) ); }); diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 9f584a7..4d96bb4 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { readJsonOrEmpty, @@ -8,7 +8,7 @@ import { writeAgentMd, writeGitHooks } from "../util.js"; -import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js"; +import { getAutonomyProfile } from "../permission-policy.js"; export const claudeCodeWriter: HarnessWriter = { id: "claude-code", @@ -40,7 +40,7 @@ async function writeClaudeSettings( const existingAllow = asStringArray(existingPerms.allow); const existingAsk = asStringArray(existingPerms.ask); - const autonomyRules = getClaudeAutonomyRules(config); + const autonomyRules = getClaudeAutonomyRules(getAutonomyProfile(config)); const mcpRules = getClaudeMcpAllowRules(config); const allowRules = [ ...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules]) @@ -77,6 +77,7 @@ function getClaudeMcpAllowRules(config: LogicalConfig): string[] { for (const server of config.mcp_servers) { const allowedTools = server.allowedTools; if (!allowedTools || allowedTools.includes("*")) { + allowRules.push(`mcp__${server.ref}__*`); continue; } @@ -88,20 +89,36 @@ function getClaudeMcpAllowRules(config: LogicalConfig): string[] { return allowRules; } -function getClaudeAutonomyRules(config: LogicalConfig): { +function getClaudeAutonomyRules(profile: AutonomyProfile | undefined): { allow: string[]; ask: string[]; } { - const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : []; - - return { - allow: [ - ...(allowsCapability(config, "read") ? ["Read"] : []), - ...(allowsCapability(config, "edit_write") ? ["Edit"] : []), - ...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []), - ...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []), - ...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : []) - ], - ask - }; + switch (profile) { + case "rigid": + return { + allow: ["Read"], + ask: [ + "Edit", + "Write", + "Glob", + "Grep", + "Bash", + "WebFetch", + "WebSearch", + "TodoWrite" + ] + }; + case "sensible-defaults": + return { + allow: ["Read", "Edit", "Write", "Glob", "Grep", "TodoWrite"], + ask: ["WebFetch", "WebSearch"] + }; + case "max-autonomy": + return { + allow: ["Read", "Edit", "Write", "Glob", "Grep", "Bash", "TodoWrite"], + ask: ["WebFetch", "WebSearch"] + }; + default: + return { allow: [], ask: [] }; + } } diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts index 23937d3..8d59506 100644 --- a/packages/harnesses/src/writers/cline.spec.ts +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -10,47 +10,7 @@ import type { import { clineWriter } from "./cline.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("clineWriter", () => { diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index daa98c8..1c389de 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -10,47 +10,7 @@ import type { import { copilotWriter } from "./copilot.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("copilotWriter", () => { @@ -224,7 +184,7 @@ describe("copilotWriter", () => { expect(rigidAgent).not.toContain(" - server/workflows/*"); expect(rigidAgent).toContain(" - workflows/*"); - expect(rigidAgent).not.toContain(" - read"); + expect(rigidAgent).toContain(" - read"); expect(rigidAgent).not.toContain(" - edit"); expect(rigidAgent).not.toContain(" - search"); expect(rigidAgent).not.toContain(" - execute"); diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index fd9c015..ba099f8 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -1,5 +1,9 @@ import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + McpServerEntry +} from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, @@ -7,11 +11,7 @@ import { writeAgentMd, writeGitHooks } from "../util.js"; -import { - allowsCapability, - hasPermissionPolicy, - keepsWebOnAsk -} from "../permission-policy.js"; +import { getAutonomyProfile } from "../permission-policy.js"; export const copilotWriter: HarnessWriter = { id: "copilot", @@ -25,7 +25,7 @@ export const copilotWriter: HarnessWriter = { }); const tools = [ - ...getBuiltInTools(config), + ...getBuiltInTools(getAutonomyProfile(config)), ...getForwardedMcpTools(config.mcp_servers) ]; @@ -41,25 +41,17 @@ export const copilotWriter: HarnessWriter = { } }; -function getBuiltInTools(config: LogicalConfig): string[] { - if (!hasPermissionPolicy(config)) { - return ["read", "edit", "search", "execute", "agent", "web"]; +function getBuiltInTools(profile: AutonomyProfile | undefined): string[] { + switch (profile) { + case "rigid": + return ["read"]; + case "sensible-defaults": + return ["read", "edit", "search", "agent"]; + case "max-autonomy": + return ["read", "edit", "search", "execute", "agent", "todo"]; + default: + return ["read", "edit", "search", "execute", "agent", "web"]; } - - return [ - ...(allowsCapability(config, "read") ? ["read"] : []), - ...(allowsCapability(config, "edit_write") ? ["edit"] : []), - ...(allowsCapability(config, "search_list") ? ["search"] : []), - ...(allowsCapability(config, "bash_unsafe") ? ["execute"] : []), - ...(allowsCapability(config, "task_agent") ? ["agent"] : []), - ...(allowsCapability(config, "task_agent") && - allowsCapability(config, "bash_unsafe") - ? ["todo"] - : []), - ...(!keepsWebOnAsk(config) && allowsCapability(config, "web") - ? ["web"] - : []) - ]; } function getForwardedMcpTools(servers: McpServerEntry[]): string[] { diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts index fdbb5b2..3d899bb 100644 --- a/packages/harnesses/src/writers/cursor.spec.ts +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -10,47 +10,7 @@ import type { import { cursorWriter } from "./cursor.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("cursorWriter", () => { diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 936812b..372fce0 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -1,34 +1,13 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, writeGitHooks } from "../util.js"; import { getAutonomyProfile, - getCapabilityDecision, hasPermissionPolicy } from "../permission-policy.js"; -const CURSOR_CAPABILITY_ORDER: AutonomyCapability[] = [ - "read", - "edit_write", - "search_list", - "bash_safe", - "bash_unsafe", - "web", - "task_agent" -]; - -const CURSOR_CAPABILITY_LABELS: Record = { - read: "read project files", - edit_write: "edit and write project files", - search_list: "search and list project contents", - bash_safe: "run safe local shell commands", - bash_unsafe: "run high-impact shell commands", - web: "use web or network access", - task_agent: "delegate or decompose work into agent tasks" -}; - export const cursorWriter: HarnessWriter = { id: "cursor", label: "Cursor", @@ -68,28 +47,37 @@ function getCursorAutonomyNotes(config: LogicalConfig): string[] { return []; } - const allowedCapabilities = CURSOR_CAPABILITY_ORDER.filter( - (capability) => getCapabilityDecision(config, capability) === "allow" - ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]); - - const approvalGatedCapabilities = CURSOR_CAPABILITY_ORDER.filter( - (capability) => getCapabilityDecision(config, capability) === "ask" - ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]); + const profile = getAutonomyProfile(config); return [ - `Cursor autonomy note (documented, not enforced): ${getAutonomyProfile(config) ?? "custom"}.`, + `Cursor autonomy note (documented, not enforced): ${profile ?? "custom"}.`, "Cursor has no verified committed project-local built-in ask/allow/deny config surface, so ADE documents autonomy intent here instead of writing unsupported permission config.", - ...(allowedCapabilities.length > 0 - ? [ - `Prefer handling these built-in capabilities without extra approval when Cursor permits it: ${allowedCapabilities.join(", ")}.` - ] - : []), - ...(approvalGatedCapabilities.length > 0 - ? [ - `Request approval before these capabilities: ${approvalGatedCapabilities.join(", ")}.` - ] - : []), + ...getCursorProfileGuidance(profile), "Web and network access must remain approval-gated.", "MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning and are not enforced or re-modeled in this rules file." ]; } + +function getCursorProfileGuidance( + profile: AutonomyProfile | undefined +): string[] { + switch (profile) { + case "rigid": + return [ + "Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files.", + "Request approval before these capabilities: edit and write project files, search and list project contents, run safe local shell commands, run high-impact shell commands, use web or network access, delegate or decompose work into agent tasks." + ]; + case "sensible-defaults": + return [ + "Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files, edit and write project files, search and list project contents, run safe local shell commands, delegate or decompose work into agent tasks.", + "Request approval before these capabilities: run high-impact shell commands, use web or network access." + ]; + case "max-autonomy": + return [ + "Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files, edit and write project files, search and list project contents, run safe local shell commands, run high-impact shell commands, delegate or decompose work into agent tasks.", + "Request approval before these capabilities: use web or network access." + ]; + default: + return []; + } +} diff --git a/packages/harnesses/src/writers/kiro.spec.ts b/packages/harnesses/src/writers/kiro.spec.ts index 11cc06d..bbdad48 100644 --- a/packages/harnesses/src/writers/kiro.spec.ts +++ b/packages/harnesses/src/writers/kiro.spec.ts @@ -10,47 +10,7 @@ import type { import { kiroWriter } from "./kiro.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("kiroWriter", () => { diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 283eb4b..6fb6271 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -1,5 +1,9 @@ import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + McpServerEntry +} from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { standardEntry, @@ -7,11 +11,7 @@ import { writeJson, writeMcpServers } from "../util.js"; -import { - allowsCapability, - getCapabilityDecision, - hasPermissionPolicy -} from "../permission-policy.js"; +import { getAutonomyProfile } from "../permission-policy.js"; export const kiroWriter: HarnessWriter = { id: "kiro", @@ -34,8 +34,11 @@ export const kiroWriter: HarnessWriter = { config.instructions.join("\n\n") || "ADE — Agentic Development Environment agent.", mcpServers: getKiroAgentMcpServers(config.mcp_servers), - tools: getKiroTools(config), - allowedTools: getKiroAllowedTools(config), + tools: getKiroTools(getAutonomyProfile(config), config.mcp_servers), + allowedTools: getKiroTools( + getAutonomyProfile(config), + config.mcp_servers + ), useLegacyMcpJson: true }); @@ -43,24 +46,22 @@ export const kiroWriter: HarnessWriter = { } }; -function getKiroTools(config: LogicalConfig): string[] { - const mcpTools = getKiroForwardedMcpTools(config.mcp_servers); +function getKiroTools( + profile: AutonomyProfile | undefined, + servers: McpServerEntry[] +): string[] { + const mcpTools = getKiroForwardedMcpTools(servers); - if (!hasPermissionPolicy(config)) { - return ["read", "write", "shell", "spec", ...mcpTools]; + switch (profile) { + case "rigid": + return ["read", "shell", "spec", ...mcpTools]; + case "sensible-defaults": + return ["read", "write", "shell", "spec", ...mcpTools]; + case "max-autonomy": + return ["read", "write", "shell(*)", "spec", ...mcpTools]; + default: + return ["read", "write", "shell", "spec", ...mcpTools]; } - - return [ - ...(getCapabilityDecision(config, "read") !== "deny" ? ["read"] : []), - ...(allowsCapability(config, "edit_write") ? ["write"] : []), - ...(allowsCapability(config, "bash_unsafe") ? ["shell(*)"] : ["shell"]), - "spec", - ...mcpTools - ]; -} - -function getKiroAllowedTools(config: LogicalConfig): string[] { - return getKiroTools(config); } function getKiroForwardedMcpTools(servers: McpServerEntry[]): string[] { diff --git a/packages/harnesses/src/writers/opencode.spec.ts b/packages/harnesses/src/writers/opencode.spec.ts index fe92f56..9d2a97a 100644 --- a/packages/harnesses/src/writers/opencode.spec.ts +++ b/packages/harnesses/src/writers/opencode.spec.ts @@ -11,47 +11,7 @@ import { parse as parseYaml } from "yaml"; import { opencodeWriter } from "./opencode.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("opencodeWriter", () => { diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 88f0914..a9c9d9e 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -1,8 +1,155 @@ import { join } from "node:path"; -import type { LogicalConfig, PermissionRule } from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeAgentMd, writeGitHooks, writeMcpServers } from "../util.js"; -import { getHarnessPermissionRules } from "../permission-policy.js"; +import { getAutonomyProfile } from "../permission-policy.js"; + +type PermissionRule = string | Record; + +const RIGID_RULES: Record = { + "*": "ask", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + external_directory: "deny", + doom_loop: "deny" +}; + +const SENSIBLE_DEFAULTS_RULES: Record = { + read: { + "*": "allow", + "*.env": "deny", + "*.env.*": "deny", + "*.env.example": "allow" + }, + edit: "allow", + glob: "allow", + grep: "allow", + list: "allow", + lsp: "allow", + task: "allow", + todoread: "deny", + todowrite: "deny", + skill: "deny", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + bash: { + "*": "deny", + "grep *": "allow", + "rg *": "allow", + "find *": "allow", + "fd *": "allow", + ls: "allow", + "ls *": "allow", + "cat *": "allow", + "head *": "allow", + "tail *": "allow", + "wc *": "allow", + "sort *": "allow", + "uniq *": "allow", + "diff *": "allow", + "echo *": "allow", + "printf *": "allow", + pwd: "allow", + "which *": "allow", + "type *": "allow", + whoami: "allow", + date: "allow", + "date *": "allow", + env: "allow", + "tree *": "allow", + "file *": "allow", + "stat *": "allow", + "readlink *": "allow", + "realpath *": "allow", + "dirname *": "allow", + "basename *": "allow", + "sed *": "allow", + "awk *": "allow", + "cut *": "allow", + "tr *": "allow", + "tee *": "allow", + "xargs *": "allow", + "jq *": "allow", + "yq *": "allow", + "mkdir *": "allow", + "touch *": "allow", + "cp *": "ask", + "mv *": "ask", + "ln *": "ask", + "npm *": "ask", + "node *": "ask", + "pip *": "ask", + "python *": "ask", + "python3 *": "ask", + "rm *": "deny", + "rmdir *": "deny", + "curl *": "deny", + "wget *": "deny", + "chmod *": "deny", + "chown *": "deny", + "sudo *": "deny", + "su *": "deny", + "sh *": "deny", + "bash *": "deny", + "zsh *": "deny", + "eval *": "deny", + "exec *": "deny", + "source *": "deny", + ". *": "deny", + "nohup *": "deny", + "dd *": "deny", + "mkfs *": "deny", + "mount *": "deny", + "umount *": "deny", + "kill *": "deny", + "killall *": "deny", + "pkill *": "deny", + "nc *": "deny", + "ncat *": "deny", + "ssh *": "deny", + "scp *": "deny", + "rsync *": "deny", + "docker *": "deny", + "kubectl *": "deny", + "systemctl *": "deny", + "service *": "deny", + "crontab *": "deny", + reboot: "deny", + "shutdown *": "deny", + "passwd *": "deny", + "useradd *": "deny", + "userdel *": "deny", + "iptables *": "deny" + }, + external_directory: "deny", + doom_loop: "deny" +}; + +const MAX_AUTONOMY_RULES: Record = { + "*": "allow", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + external_directory: "deny", + doom_loop: "deny" +}; + +function getPermissionRules( + profile: AutonomyProfile | undefined +): Record | undefined { + switch (profile) { + case "rigid": + return RIGID_RULES; + case "sensible-defaults": + return SENSIBLE_DEFAULTS_RULES; + case "max-autonomy": + return MAX_AUTONOMY_RULES; + default: + return undefined; + } +} export const opencodeWriter: HarnessWriter = { id: "opencode", @@ -20,7 +167,7 @@ export const opencodeWriter: HarnessWriter = { defaults: { $schema: "https://opencode.ai/config.json" } }); - const permission = getHarnessPermissionRules(config); + const permission = getPermissionRules(getAutonomyProfile(config)); await writeAgentMd(config, { path: join(projectRoot, ".opencode", "agents", "ade.md"), diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts index 2b53b9e..d0ed01f 100644 --- a/packages/harnesses/src/writers/roo-code.spec.ts +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -10,47 +10,7 @@ import type { import { rooCodeWriter } from "./roo-code.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("rooCodeWriter", () => { @@ -174,7 +134,7 @@ describe("rooCodeWriter", () => { await readFile(join(rigidRoot, ".roo", "mcp.json"), "utf-8") ); - expect(rigidModes.customModes.ade.groups).toEqual(["mcp"]); + expect(rigidModes.customModes.ade.groups).toEqual(["read", "mcp"]); expect(defaultsModes.customModes.ade.groups).toEqual([ "read", "edit", diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index aa2694c..12f4428 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { readJsonOrEmpty, @@ -9,7 +9,10 @@ import { writeGitHooks, writeJson } from "../util.js"; -import { allowsCapability, hasPermissionPolicy } from "../permission-policy.js"; +import { + getAutonomyProfile, + hasPermissionPolicy +} from "../permission-policy.js"; export const rooCodeWriter: HarnessWriter = { id: "roo-code", @@ -48,7 +51,10 @@ async function writeRooModes( name: "ADE", roleDefinition: "ADE — Agentic Development Environment mode generated by ADE.", - groups: getRooModeGroups(config), + groups: getRooModeGroups( + getAutonomyProfile(config), + config.mcp_servers.length > 0 + ), source: "project" } } @@ -61,11 +67,20 @@ function asRecord(value: unknown): Record { : {}; } -function getRooModeGroups(config: LogicalConfig): string[] { - return [ - ...(allowsCapability(config, "read") ? ["read"] : []), - ...(allowsCapability(config, "edit_write") ? ["edit"] : []), - ...(allowsCapability(config, "bash_unsafe") ? ["command"] : []), - ...(config.mcp_servers.length > 0 ? ["mcp"] : []) - ]; +function getRooModeGroups( + profile: AutonomyProfile | undefined, + hasMcpServers: boolean +): string[] { + const mcpGroup = hasMcpServers ? ["mcp"] : []; + + switch (profile) { + case "rigid": + return ["read", ...mcpGroup]; + case "sensible-defaults": + return ["read", "edit", ...mcpGroup]; + case "max-autonomy": + return ["read", "edit", "command", ...mcpGroup]; + default: + return ["read", "edit", "command", ...mcpGroup]; + } } diff --git a/packages/harnesses/src/writers/universal.spec.ts b/packages/harnesses/src/writers/universal.spec.ts index 50ecdda..0efe013 100644 --- a/packages/harnesses/src/writers/universal.spec.ts +++ b/packages/harnesses/src/writers/universal.spec.ts @@ -10,47 +10,7 @@ import type { import { universalWriter } from "./universal.js"; function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } describe("universalWriter", () => { diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 2723272..c87f55b 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -1,40 +1,16 @@ import { join } from "node:path"; import { writeFile } from "node:fs/promises"; -import type { - AutonomyCapability, - LogicalConfig, - PermissionDecision -} from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, writeGitHooks } from "../util.js"; - -const CAPABILITY_ORDER: AutonomyCapability[] = [ - "read", - "edit_write", - "search_list", - "bash_safe", - "bash_unsafe", - "web", - "task_agent" -]; - -function formatCapabilityGuidance( - capability: AutonomyCapability, - decision: PermissionDecision -): string { - return `- \`${capability}\`: ${decision}`; -} +import { getAutonomyProfile } from "../permission-policy.js"; function renderAutonomyGuidance(config: LogicalConfig): string | undefined { - const policy = config.permission_policy; - if (!policy) { + const profile = getAutonomyProfile(config); + if (!profile) { return undefined; } - const capabilityLines = CAPABILITY_ORDER.map((capability) => - formatCapabilityGuidance(capability, policy.capabilities[capability]) - ); - return [ "## Autonomy", "", @@ -42,15 +18,52 @@ function renderAutonomyGuidance(config: LogicalConfig): string | undefined { "", "Treat this autonomy profile as documentation-only guidance for built-in/basic operations.", "", - `Profile: \`${policy.profile}\``, + `Profile: \`${profile}\``, "", - "Built-in/basic capability guidance:", - ...capabilityLines, + ...getUniversalProfileGuidance(profile), "", "MCP permissions are not re-modeled by autonomy here; any MCP approvals must come from provisioning-aware consuming harnesses rather than the Universal writer." ].join("\n"); } +function getUniversalProfileGuidance(profile: AutonomyProfile): string[] { + switch (profile) { + case "rigid": + return [ + "Built-in/basic capability guidance:", + "- `read`: allow", + "- `edit_write`: ask", + "- `search_list`: ask", + "- `bash_safe`: ask", + "- `bash_unsafe`: ask", + "- `web`: ask", + "- `task_agent`: ask" + ]; + case "sensible-defaults": + return [ + "Built-in/basic capability guidance:", + "- `read`: allow", + "- `edit_write`: allow", + "- `search_list`: allow", + "- `bash_safe`: allow", + "- `bash_unsafe`: ask", + "- `web`: ask", + "- `task_agent`: allow" + ]; + case "max-autonomy": + return [ + "Built-in/basic capability guidance:", + "- `read`: allow", + "- `edit_write`: allow", + "- `search_list`: allow", + "- `bash_safe`: allow", + "- `bash_unsafe`: allow", + "- `web`: ask", + "- `task_agent`: allow" + ]; + } +} + export const universalWriter: HarnessWriter = { id: "universal", label: "Universal (AGENTS.md + .mcp.json)", diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts index ec3b7a0..ae6ce67 100644 --- a/packages/harnesses/src/writers/windsurf.spec.ts +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -95,7 +95,10 @@ describe("windsurfWriter", () => { expect(rigidRules).toContain("Windsurf limitation:"); expect(rigidRules).toContain("advisory only"); expect(rigidRules).toContain( - "Ask before: read files, edit and write files, search and list files, safe local shell commands, unsafe local shell commands, web and network access, task or agent delegation." + "May proceed without extra approval: read files." + ); + expect(rigidRules).toContain( + "Ask before: edit and write files, search and list files, safe local shell commands, unsafe local shell commands, web and network access, task or agent delegation." ); expect(sensibleRules).toContain("Windsurf limitation:"); @@ -134,45 +137,5 @@ describe("windsurfWriter", () => { function autonomyPolicy( profile: "rigid" | "sensible-defaults" | "max-autonomy" ): LogicalConfig["permission_policy"] { - switch (profile) { - case "rigid": - return { - profile, - capabilities: { - read: "ask", - edit_write: "ask", - search_list: "ask", - bash_safe: "ask", - bash_unsafe: "ask", - web: "ask", - task_agent: "ask" - } - }; - case "sensible-defaults": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "ask", - web: "ask", - task_agent: "allow" - } - }; - case "max-autonomy": - return { - profile, - capabilities: { - read: "allow", - edit_write: "allow", - search_list: "allow", - bash_safe: "allow", - bash_unsafe: "allow", - web: "ask", - task_agent: "allow" - } - }; - } + return { profile }; } diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index ce731a9..118f23c 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, @@ -7,7 +7,10 @@ import { writeRulesFile, writeGitHooks } from "../util.js"; -import { hasPermissionPolicy } from "../permission-policy.js"; +import { + getAutonomyProfile, + hasPermissionPolicy +} from "../permission-policy.js"; export const windsurfWriter: HarnessWriter = { id: "windsurf", @@ -32,58 +35,37 @@ function getWindsurfRules(config: LogicalConfig): string[] { return config.instructions; } - const { capabilities } = config.permission_policy!; - const allow = listCapabilities(capabilities, "allow"); - const ask = listCapabilities(capabilities, "ask"); - const deny = listCapabilities(capabilities, "deny"); - const autonomyGuidance = [ "Windsurf limitation: ADE could not verify a stable committed project-local permission schema for Windsurf built-in tools, so this autonomy policy is advisory only and should be applied conservatively.", - formatGuidance(allow, ask, deny) + getWindsurfProfileGuidance(getAutonomyProfile(config)) ]; return [...autonomyGuidance, ...config.instructions]; } -function listCapabilities( - capabilities: NonNullable["capabilities"], - decision: "ask" | "allow" | "deny" -): string[] { - return (Object.entries(capabilities) as Array<[AutonomyCapability, string]>) - .filter(([, value]) => value === decision) - .map(([capability]) => CAPABILITY_LABELS[capability]); -} - -function formatGuidance( - allow: string[], - ask: string[], - deny: string[] +function getWindsurfProfileGuidance( + profile: AutonomyProfile | undefined ): string { - const lines = ["Autonomy guidance for Windsurf built-in capabilities:"]; - - if (allow.length > 0) { - lines.push(`- May proceed without extra approval: ${allow.join(", ")}.`); - } - - if (ask.length > 0) { - lines.push(`- Ask before: ${ask.join(", ")}.`); + switch (profile) { + case "rigid": + return [ + "Autonomy guidance for Windsurf built-in capabilities:", + "- May proceed without extra approval: read files.", + "- Ask before: edit and write files, search and list files, safe local shell commands, unsafe local shell commands, web and network access, task or agent delegation." + ].join("\n"); + case "sensible-defaults": + return [ + "Autonomy guidance for Windsurf built-in capabilities:", + "- May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, task or agent delegation.", + "- Ask before: unsafe local shell commands, web and network access." + ].join("\n"); + case "max-autonomy": + return [ + "Autonomy guidance for Windsurf built-in capabilities:", + "- May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, unsafe local shell commands, task or agent delegation.", + "- Ask before: web and network access." + ].join("\n"); + default: + return "Autonomy guidance for Windsurf built-in capabilities: follow project conventions."; } - - if (deny.length > 0) { - lines.push( - `- Do not use unless the user explicitly overrides: ${deny.join(", ")}.` - ); - } - - return lines.join("\n"); } - -const CAPABILITY_LABELS: Record = { - read: "read files", - edit_write: "edit and write files", - search_list: "search and list files", - bash_safe: "safe local shell commands", - bash_unsafe: "unsafe local shell commands", - web: "web and network access", - task_agent: "task or agent delegation" -}; From d777e9882ec6cb6fe05762ce5d0ccdeafe5a0874 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 06:06:06 +0000 Subject: [PATCH 2/2] simplify: DRY up writer files after permission-set refactor - Extract `formatYamlKey` to `util.ts`; remove duplicate copies from `copilot.ts` and `opencode.ts` - Compute `getKiroTools` once in `kiro.ts` and reuse for both `tools` and `allowedTools` instead of calling it twice with identical args - Strengthen the local `PermissionRule` type in `opencode.ts` from `string | Record` to use the explicit decision union `"ask" | "allow" | "deny"` so the compiler catches invalid values - Hoist repeated header strings out of individual `switch` case arms in `universal.ts` and `windsurf.ts` into a local `const` https://claude.ai/code/session_01RgRDwsqvUZhuz2hEjkBK53 --- packages/harnesses/src/util.ts | 11 +++++++++++ packages/harnesses/src/writers/copilot.ts | 9 ++------- packages/harnesses/src/writers/kiro.ts | 8 +++----- packages/harnesses/src/writers/opencode.ts | 16 ++++++++-------- packages/harnesses/src/writers/universal.ts | 7 ++++--- packages/harnesses/src/writers/windsurf.ts | 9 +++++---- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts index 45b8a32..b4ee683 100644 --- a/packages/harnesses/src/util.ts +++ b/packages/harnesses/src/util.ts @@ -219,3 +219,14 @@ export async function writeInlineSkills( return modified; } + +// --------------------------------------------------------------------------- +// YAML helpers +// --------------------------------------------------------------------------- + +/** Quote a YAML key only when it contains characters that require quoting. */ +export function formatYamlKey(value: string): string { + return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value) + ? value + : JSON.stringify(value); +} diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index ba099f8..25ed502 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -9,7 +9,8 @@ import { writeMcpServers, stdioEntry, writeAgentMd, - writeGitHooks + writeGitHooks, + formatYamlKey } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; @@ -89,9 +90,3 @@ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] { return lines; } - -function formatYamlKey(value: string): string { - return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value) - ? value - : JSON.stringify(value); -} diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 6fb6271..47af6c8 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -26,6 +26,7 @@ export const kiroWriter: HarnessWriter = { }) }); + const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers); await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { name: "ade", description: @@ -34,11 +35,8 @@ export const kiroWriter: HarnessWriter = { config.instructions.join("\n\n") || "ADE — Agentic Development Environment agent.", mcpServers: getKiroAgentMcpServers(config.mcp_servers), - tools: getKiroTools(getAutonomyProfile(config), config.mcp_servers), - allowedTools: getKiroTools( - getAutonomyProfile(config), - config.mcp_servers - ), + tools, + allowedTools: tools, useLegacyMcpJson: true }); diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index a9c9d9e..5cba7f8 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -1,10 +1,16 @@ import { join } from "node:path"; import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; -import { writeAgentMd, writeGitHooks, writeMcpServers } from "../util.js"; +import { + writeAgentMd, + writeGitHooks, + writeMcpServers, + formatYamlKey +} from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; -type PermissionRule = string | Record; +type PermissionDecision = "ask" | "allow" | "deny"; +type PermissionRule = PermissionDecision | Record; const RIGID_RULES: Record = { "*": "ask", @@ -206,9 +212,3 @@ function renderYamlMapping( return lines; } - -function formatYamlKey(value: string): string { - return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value) - ? value - : JSON.stringify(value); -} diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index c87f55b..1631ede 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -27,10 +27,11 @@ function renderAutonomyGuidance(config: LogicalConfig): string | undefined { } function getUniversalProfileGuidance(profile: AutonomyProfile): string[] { + const header = "Built-in/basic capability guidance:"; switch (profile) { case "rigid": return [ - "Built-in/basic capability guidance:", + header, "- `read`: allow", "- `edit_write`: ask", "- `search_list`: ask", @@ -41,7 +42,7 @@ function getUniversalProfileGuidance(profile: AutonomyProfile): string[] { ]; case "sensible-defaults": return [ - "Built-in/basic capability guidance:", + header, "- `read`: allow", "- `edit_write`: allow", "- `search_list`: allow", @@ -52,7 +53,7 @@ function getUniversalProfileGuidance(profile: AutonomyProfile): string[] { ]; case "max-autonomy": return [ - "Built-in/basic capability guidance:", + header, "- `read`: allow", "- `edit_write`: allow", "- `search_list`: allow", diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index 118f23c..f36ddcd 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -46,26 +46,27 @@ function getWindsurfRules(config: LogicalConfig): string[] { function getWindsurfProfileGuidance( profile: AutonomyProfile | undefined ): string { + const header = "Autonomy guidance for Windsurf built-in capabilities:"; switch (profile) { case "rigid": return [ - "Autonomy guidance for Windsurf built-in capabilities:", + header, "- May proceed without extra approval: read files.", "- Ask before: edit and write files, search and list files, safe local shell commands, unsafe local shell commands, web and network access, task or agent delegation." ].join("\n"); case "sensible-defaults": return [ - "Autonomy guidance for Windsurf built-in capabilities:", + header, "- May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, task or agent delegation.", "- Ask before: unsafe local shell commands, web and network access." ].join("\n"); case "max-autonomy": return [ - "Autonomy guidance for Windsurf built-in capabilities:", + header, "- May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, unsafe local shell commands, task or agent delegation.", "- Ask before: web and network access." ].join("\n"); default: - return "Autonomy guidance for Windsurf built-in capabilities: follow project conventions."; + return `${header} follow project conventions.`; } }