Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
];
```
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/core/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand All @@ -89,6 +95,7 @@ export function normalizeMcpServerConfig(
...(server.headers ? { headers: { ...server.headers } } : {}),
...(startupTimeoutMs ? { startupTimeoutMs } : {}),
...(toolTimeoutMs ? { toolTimeoutMs } : {}),
defaultToolsApprovalMode,
env: normalizeMcpEnvEntries(server.env),
};
}
Expand All @@ -100,6 +107,7 @@ export function normalizeMcpServerConfig(
...(server.args ? { args: server.args.slice() } : {}),
...(startupTimeoutMs ? { startupTimeoutMs } : {}),
...(toolTimeoutMs ? { toolTimeoutMs } : {}),
defaultToolsApprovalMode,
env: normalizeMcpEnvEntries(server.env),
};
}
Expand Down
22 changes: 12 additions & 10 deletions src/providers/claude/launch-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/providers/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ function buildMcpConfigBlock(servers: ReturnType<typeof normalizeMcpServerConfig
if (server.toolTimeoutMs) {
lines.push(`tool_timeout_sec = ${Math.ceil(server.toolTimeoutMs / 1000)}`);
}
lines.push(
`default_tools_approval_mode = "${server.defaultToolsApprovalMode}"`,
);

if (server.env.length > 0) {
lines.push("", `[mcp_servers.${server.name}.env]`);
Expand Down
24 changes: 24 additions & 0 deletions tests/core/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
4 changes: 2 additions & 2 deletions tests/providers/claude-launch-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions tests/providers/codex-launch-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
Expand Down
Loading