diff --git a/README.md b/README.md index ced17e6..8c45bfa 100644 --- a/README.md +++ b/README.md @@ -401,7 +401,7 @@ permission: { The SDK maps this policy to each provider. Workspace App runs default to `full-access` when the host omits `permission`: Codex uses its unrestricted -sandbox mode, Claude uses `bypassPermissions`, and ACP requests select a +sandbox mode, Claude uses `--dangerously-skip-permissions`, and ACP requests select a recognized permissive option when the peer offers one (otherwise the request is cancelled). An App can pass an explicit narrower semantic for a run. For ACP, every @@ -480,6 +480,9 @@ const mcpServers = [ env: { APP_TOOL_TOKEN: runScopedToken }, toolTimeoutMs: 30 * 60_000, startupTimeoutMs: 2 * 60_000, + // Optional override: "auto" | "prompt" | "writes" | "approve". + // Run-scoped application MCP servers default to "approve". + defaultToolsApprovalMode: "approve" as const, }, ]; ``` @@ -488,6 +491,10 @@ Timeouts are normalized by provider. Codex writes `startup_timeout_sec` and `tool_timeout_sec` into its per-run config. Claude Code writes per-server `timeout` for tool calls. Generic ACP providers receive only standard ACP MCP server fields because the ACP MCP server schema does not define timeout fields. +Codex also writes `default_tools_approval_mode`; it defaults to `approve` so +trusted run-scoped application MCP tools can execute in non-interactive runs. +Hosts can explicitly choose `auto`, `prompt`, or `writes` for less-trusted MCP +servers. Claude full-access runs use `--dangerously-skip-permissions`. Codex and Claude MCP delivery uses those provider-native, run-scoped config paths. Generic ACP providers receive normalized MCP servers through ACP. diff --git a/package.json b/package.json index 2c4937b..64100cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tutti-os/agent-acp-kit", - "version": "0.7.2", + "version": "0.7.3", "type": "module", "description": "ACP-oriented toolkit for detecting, launching, and streaming local coding agents.", "license": "Apache-2.0", diff --git a/src/core/mcp.ts b/src/core/mcp.ts index b775886..5538366 100644 --- a/src/core/mcp.ts +++ b/src/core/mcp.ts @@ -6,6 +6,11 @@ export type LocalAgentMcpEnvEntry = { type LocalAgentMcpTimeoutConfig = { startupTimeoutMs?: number; toolTimeoutMs?: number; + /** + * Default provider approval policy for tools exposed by this MCP server. + * Trusted, run-scoped application MCP servers default to `approve`. + */ + defaultToolsApprovalMode?: "auto" | "prompt" | "writes" | "approve"; }; export type LocalAgentMcpStdioServerConfig = { @@ -80,6 +85,7 @@ export function normalizeMcpServerConfig( ): NormalizedLocalAgentMcpServerConfig { const startupTimeoutMs = normalizePositiveInteger(server.startupTimeoutMs); const toolTimeoutMs = normalizePositiveInteger(server.toolTimeoutMs); + const defaultToolsApprovalMode = server.defaultToolsApprovalMode ?? "approve"; if (server.type === "http") { return { @@ -89,6 +95,7 @@ export function normalizeMcpServerConfig( ...(server.headers ? { headers: { ...server.headers } } : {}), ...(startupTimeoutMs ? { startupTimeoutMs } : {}), ...(toolTimeoutMs ? { toolTimeoutMs } : {}), + defaultToolsApprovalMode, env: normalizeMcpEnvEntries(server.env), }; } @@ -100,6 +107,7 @@ export function normalizeMcpServerConfig( ...(server.args ? { args: server.args.slice() } : {}), ...(startupTimeoutMs ? { startupTimeoutMs } : {}), ...(toolTimeoutMs ? { toolTimeoutMs } : {}), + defaultToolsApprovalMode, env: normalizeMcpEnvEntries(server.env), }; } diff --git a/src/providers/claude/launch-plan.ts b/src/providers/claude/launch-plan.ts index 17ef93c..9f5b2a7 100644 --- a/src/providers/claude/launch-plan.ts +++ b/src/providers/claude/launch-plan.ts @@ -12,20 +12,25 @@ function resolveProviderResumeId( return (resume.providerSessionId ?? resume.resumeToken)?.trim() || undefined; } -function resolveClaudePermissionMode( +function applyClaudePermissionArgs( + args: string[], permission: AgentRunParams<"local-agent", "claude-code">["permission"], ) { switch (permission?.semantic) { case "accept-edits": - return "acceptEdits"; + args.push("--permission-mode", "acceptEdits"); + break; case "locked-down": - return "dontAsk"; + args.push("--permission-mode", "dontAsk"); + break; case "auto": - return "auto"; + args.push("--permission-mode", "auto"); + break; case "full-access": - return "bypassPermissions"; + args.push("--dangerously-skip-permissions"); + break; default: - return undefined; + break; } } @@ -49,10 +54,7 @@ export function buildClaudeLaunchPlan( for (const dir of params.extraAllowedDirs ?? []) { if (dir) args.push("--add-dir", dir); } - const permissionMode = resolveClaudePermissionMode(params.permission); - if (permissionMode) { - args.push("--permission-mode", permissionMode); - } + applyClaudePermissionArgs(args, params.permission); const plan: ProviderLaunchPlan = { args, command: executablePath, diff --git a/src/providers/codex/index.ts b/src/providers/codex/index.ts index 60b3bc3..652df08 100644 --- a/src/providers/codex/index.ts +++ b/src/providers/codex/index.ts @@ -222,6 +222,9 @@ function buildMcpConfigBlock(servers: ReturnType 0) { lines.push("", `[mcp_servers.${server.name}.env]`); diff --git a/tests/core/mcp.test.ts b/tests/core/mcp.test.ts new file mode 100644 index 0000000..8044fa0 --- /dev/null +++ b/tests/core/mcp.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeMcpServerConfig } from "../../src/core/mcp.js"; + +describe("normalizeMcpServerConfig", () => { + it("defaults run-scoped MCP tools to approve", () => { + expect( + normalizeMcpServerConfig({ + name: "app-tools", + command: "node", + }).defaultToolsApprovalMode, + ).toBe("approve"); + }); + + it("preserves an explicit approval policy", () => { + expect( + normalizeMcpServerConfig({ + name: "external-tools", + command: "node", + defaultToolsApprovalMode: "prompt", + }).defaultToolsApprovalMode, + ).toBe("prompt"); + }); +}); diff --git a/tests/providers/claude-launch-plan.test.ts b/tests/providers/claude-launch-plan.test.ts index 9fd36b5..6aedbc5 100644 --- a/tests/providers/claude-launch-plan.test.ts +++ b/tests/providers/claude-launch-plan.test.ts @@ -67,7 +67,7 @@ describe("buildClaudeLaunchPlan", () => { ...base, permission: { modeId: "bypassPermissions", semantic: "full-access" }, }).args, - ).toEqual(expect.arrayContaining(["--permission-mode", "bypassPermissions"])); + ).toContain("--dangerously-skip-permissions"); expect( buildClaudeLaunchPlan({ ...base, @@ -151,7 +151,7 @@ describe("buildClaudeLaunchPlan", () => { const configIndex = plan.args.indexOf("--mcp-config"); expect(configIndex).toBeGreaterThan(-1); expect(plan.args).toEqual( - expect.arrayContaining(["--permission-mode", "bypassPermissions"]), + expect.arrayContaining(["--dangerously-skip-permissions"]), ); const configPath = plan.args[configIndex + 1]; expect(configPath).toContain(cwd); diff --git a/tests/providers/codex-launch-plan.test.ts b/tests/providers/codex-launch-plan.test.ts index f104457..c5251d4 100644 --- a/tests/providers/codex-launch-plan.test.ts +++ b/tests/providers/codex-launch-plan.test.ts @@ -980,6 +980,7 @@ describe("buildCodexLaunchPlan", () => { expect(config).toContain('command = "node"'); expect(config).toContain("startup_timeout_sec = 120"); expect(config).toContain("tool_timeout_sec = 1800"); + expect(config).toContain('default_tools_approval_mode = "approve"'); expect(config).toContain('AIMC_TOOL_TOKEN = "tool-token"'); expect(config).not.toContain('command = "old-node"'); expect(config).not.toContain('OLD_TOKEN = "old-token"');