Skip to content

Commit 45c99a0

Browse files
fix(adapters): default legacy harnesses and connected tools to full auto (#13693)
## Thinking Path > - Paperclip lets people manage AI agents and their work. > - Legacy adapters launch provider CLIs and expose connected tools. > - Existing defaults did not consistently grant full automatic permission. > - Remote Claude used a fixed tool list that omitted MCP tools and future tools. > - Direct Codex launches and OpenCode configuration also used narrower defaults. > - This change gives all these paths the same full-auto default as native runners. > - Explicit restrictive settings continue to work. ## Linked Issues or Issue Description Refs #13686. This PR is stacked on that native-runner and task-reassignment PR. Merge #13686 first. Related: #831 (constructed Claude agents), #1935 (adapter-switching permission defaults). ## What Changed - Use actual Claude permission bypass for local and remote runs and probes. Remove the fixed tool list so MCP and future provider tools are included. - Identify actual managed sandbox targets to Claude with `IS_SANDBOX=1`. Do not mark ordinary host execution as a sandbox. - Default direct Codex execution to approval and sandbox bypass, matching agent creation. Preserve explicit false, CLI profiles, sandbox modes, approval policy, and network restrictions. - Set OpenCode's full-auto runtime permission to `allow` for every tool and connection. Preserve the existing explicit opt-out. - Default Gemini probes to the same YOLO mode as execution. Make the legacy ACP `default` alias use `approve-all` for fresh and resumed sessions. - Add default, opt-out, remote, probe, connected-tool, and resume regression tests. Update adapter configuration documentation. - Other adapter paths already request full automatic permission or have no provider approval gate. ## Verification - Full workspace `pnpm -r typecheck` and `pnpm build` passed locally after rebasing onto current master. Targeted adapter/server and legacy ACP tests passed, including defaults, explicit opt-outs, remote launches, connected tools, and fresh/resumed sessions. - Greptile reviewed current head `8ca135eaffcf9cfdba6f1368e896a781a0891d50` at **5/5**. The security reviewer acknowledged the documented full-auto requirement. Acknowledged discussions are resolved. - Current head has **54 passing checks**. [PR checks](https://github.com/paperclipai/paperclip/pull/13693/checks). The process-adapter signoff browser shard passed on one retry after its first attempt exceeded a three-second issue-run wait. - **Six native Claude/Codex real-provider cases passed on their first attempt, with cleanup passing**, against the combined branch: plans, reassignment, and backlog creation/status. [Campaign and downloadable evidence](https://github.com/paperclipai/paperclip/actions/runs/35469926548). This does not claim a real-provider run of every legacy adapter. - The live-tested revision is `a37881c824dcd7170380fc4b788732fc743e5da7`. The current head differs only in the corrected heartbeat test expectation; application code is identical. - The campaign result-enforcement job passed. The separate report publisher failed during frozen dependency installation because the trusted workflow's patched-dependency configuration does not match its lockfile. Passing case evidence remains downloadable from the workflow. - Full-suite coverage comes from CI partitions. The separate unsharded local run was stopped after the corresponding CI partitions passed; it is not counted as a completed local run. ## Risks - Missing permission settings now grant all provider operations, including connected tools. OpenCode full-auto also overrides ambient provider permission rules. An explicit Paperclip permission opt-out preserves restrictive behavior. - Claude refuses full bypass as root outside an identified sandbox. Ordinary host deployments must run Claude as a non-root user. Managed sandbox launches include the required marker. - These defaults do not grant additional Paperclip roles, connections, or company access. Existing controller authorization and governance still apply. - This PR depends on #13686. Retarget it to master after that PR merges. ## Model Used OpenAI Codex, based on GPT-6, with code execution and repository tools. The exact deployment model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 7bc03e0 commit 45c99a0

20 files changed

Lines changed: 120 additions & 221 deletions

File tree

packages/adapter-utils/src/acpx-engine/execute.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,29 @@ const ALLOWED_TURN_SPAN_ATTRIBUTE_KEYS = new Set<string>([
369369
]);
370370

371371
describe("shared ACPX engine runtime behavior", () => {
372+
it.each(["claude", "codex", "gemini", "kimi", "custom"])("defaults the legacy %s engine to full auto on fresh and resumed runs", async (agent) => {
373+
const root = await makeTempRoot();
374+
const config = {
375+
agent, cwd: root, stateDir: path.join(root, "state"),
376+
...(agent === "custom" ? { agentCommand: "node ./fake-acp.js" } : {}),
377+
};
378+
const first = await runExecutor(config);
379+
const resumed = await runExecutor(config, { runtime: { sessionParams: first.result.sessionParams } });
380+
for (const run of [first, resumed]) {
381+
expect(run.runtimeOptions[0]?.permissionMode).toBe("approve-all");
382+
expect(run.result.resultJson?.permissionMode).toBe("approve-all");
383+
}
384+
});
385+
386+
it.each([
387+
["default", "approve-all"], ["", "approve-all"],
388+
["approve-reads", "approve-reads"], ["deny-all", "deny-all"],
389+
])("resolves the legacy %j permission setting to %s", async (permissionMode, expected) => {
390+
const root = await makeTempRoot();
391+
const run = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js", cwd: root, stateDir: path.join(root, "state"), permissionMode });
392+
expect(run.runtimeOptions[0]?.permissionMode).toBe(expected);
393+
});
394+
372395
it("persists ACP agent process identity before prompting on each run (host lane re-creates, no warm reuse)", async () => {
373396
const root = await makeTempRoot();
374397
const startedAt = "2026-07-30T07:00:00.000Z";

packages/adapter-utils/src/acpx-engine/execute.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ function normalizeMode(config: Record<string, unknown>): "persistent" | "oneshot
14221422
function normalizePermissionMode(config: Record<string, unknown>): "approve-all" | "approve-reads" | "deny-all" {
14231423
const value = asString(config.permissionMode, DEFAULT_ACP_ENGINE_PERMISSION_MODE).trim();
14241424
if (value === "approve-reads" || value === "deny-all") return value;
1425-
if (value === "default") return "approve-reads";
1425+
if (value === "default") return DEFAULT_ACP_ENGINE_PERMISSION_MODE;
14261426
return "approve-all";
14271427
}
14281428

packages/adapters/claude-local/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Core fields:
5353
- chrome (boolean, optional): pass --chrome when running Claude
5454
- promptTemplate (string, optional): run prompt template
5555
- maxTurnsPerRun (number, optional): max turns for one run
56-
- dangerouslySkipPermissions (boolean, optional, default true): allow non-interactive Claude runs to proceed without approval prompts. Local targets receive --dangerously-skip-permissions; remote targets receive a curated --allowedTools list so they do not inherit local bypass permissions.
56+
- dangerouslySkipPermissions (boolean, optional, default true): allow non-interactive Claude runs to proceed without approval prompts. Local and remote targets receive --dangerously-skip-permissions for all built-in and connected tools. Managed sandbox targets also identify themselves to Claude so root container launches support bypass. Non-sandbox root processes must run Claude as a non-root user; Paperclip does not silently downgrade the requested mode.
5757
- command (string, optional): defaults to "claude"
5858
- extraArgs (string[], optional): additional CLI args
5959
- env (object, optional): KEY=VALUE environment variables

packages/adapters/claude-local/src/server/acp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
import { createWorkspaceRestoreTeardown } from "@paperclipai/adapter-utils/workspace-restore-teardown";
5555
import { buildLocalAdapterTestProbeEnv } from "./probe-env.js";
5656
import { detectClaudeLoginRequired, extractClaudeRetryNotBefore, isClaudeProviderQuotaError, parseClaudeStreamJson } from "./parse.js";
57-
import { buildClaudeProbePermissionArgs } from "./permissions.js";
57+
import { buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
5858
import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js";
5959
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
6060

@@ -647,6 +647,9 @@ export async function probeClaudeAcpSandboxLogin(input: {
647647
cwd = asString(config.cwd, process.cwd());
648648
}
649649

650+
Object.assign(env, claudeSandboxPermissionEnv({
651+
dangerouslySkipPermissions: asBoolean(config.dangerouslySkipPermissions, true), targetIsSandbox,
652+
}));
650653
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
651654
if (config.managedAiConnection) args.push("--setting-sources", "user");
652655
args.push(

packages/adapters/claude-local/src/server/execute.remote.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,8 @@ describe("claude remote execution", () => {
193193
| [string, string, string[], { env: Record<string, string>; remoteExecution?: { remoteCwd: string } | null }]
194194
| undefined;
195195
expect(call?.[2]).toEqual(expect.arrayContaining(["--model", "claude-opus-5"]));
196-
expect(call?.[2]).toContain("--allowedTools");
197-
expect(call?.[2]).toContain(
198-
"Task AskUserQuestion Bash CronCreate CronDelete CronList Edit EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Glob Grep Monitor NotebookEdit PushNotification Read RemoteTrigger ScheduleWakeup Skill TaskOutput TaskStop TodoWrite ToolSearch WebFetch WebSearch Write",
199-
);
200-
expect(call?.[2]).not.toContain("--dangerously-skip-permissions");
196+
expect(call?.[2]).toContain("--dangerously-skip-permissions");
197+
expect(call?.[2]).not.toContain("--allowedTools");
201198
expect(call?.[2]).toContain("--append-system-prompt-file");
202199
expect(call?.[2]).toContain(
203200
`${managedRemoteWorkspace}/.paperclip-runtime/claude/skills/agent-instructions.md`,

packages/adapters/claude-local/src/server/execute.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import {
9292
import { resolveClaudeDesiredSkillNames } from "./skills.js";
9393
import { isBedrockModelId } from "./models.js";
9494
import { prepareClaudePromptBundle } from "./prompt-cache.js";
95-
import { buildClaudeExecutionPermissionArgs } from "./permissions.js";
95+
import { buildClaudeExecutionPermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
9696
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
9797
import {
9898
createClaudeAcpExecutor,
@@ -480,6 +480,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
480480
graceSec,
481481
extraArgs,
482482
} = runtimeConfig;
483+
Object.assign(env, claudeSandboxPermissionEnv({ dangerouslySkipPermissions, targetIsSandbox: executionTargetIsSandbox }));
483484
let loggedEnv = initialLoggedEnv;
484485
let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
485486
const terminalResultCleanupGraceMs = Math.max(
@@ -937,7 +938,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
937938
}
938939
if (dangerouslySkipPermissions && executionTargetIsRemote) {
939940
commandNotes.push(
940-
"Using a broad --allowedTools whitelist for remote execution so hosted targets do not inherit local Claude bypass permissions.",
941+
"Using full Claude permission bypass for remote execution, including connected tools.",
941942
);
942943
}
943944
if (attemptInstructionsFilePath && !resumeSessionId) {
Lines changed: 18 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,23 @@
11
import { describe, expect, it } from "vitest";
2-
import { buildClaudeExecutionPermissionArgs, buildClaudeProbePermissionArgs } from "./permissions.js";
3-
4-
const SANDBOX_ALLOWED_TOOLS =
5-
"Task AskUserQuestion Bash CronCreate CronDelete CronList Edit " +
6-
"EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Glob Grep Monitor " +
7-
"NotebookEdit PushNotification Read RemoteTrigger ScheduleWakeup Skill " +
8-
"TaskOutput TaskStop TodoWrite ToolSearch WebFetch WebSearch Write";
9-
10-
describe("claude-local remote permission args", () => {
11-
it("uses the canonical Bash tool grant for remote execution", () => {
12-
expect(buildClaudeExecutionPermissionArgs({ dangerouslySkipPermissions: true, targetIsRemote: true })).toEqual([
13-
"--allowedTools",
14-
SANDBOX_ALLOWED_TOOLS,
15-
]);
16-
});
17-
18-
it("uses the canonical Bash tool grant for remote probes", () => {
19-
expect(buildClaudeProbePermissionArgs({ dangerouslySkipPermissions: true, targetIsRemote: true })).toEqual([
20-
"--allowedTools",
21-
SANDBOX_ALLOWED_TOOLS,
22-
]);
23-
});
24-
25-
it("does not use Bash(*) because Claude Code treats Bash grants as command-prefix patterns", () => {
26-
const [, allowedTools] = buildClaudeExecutionPermissionArgs({
27-
dangerouslySkipPermissions: true,
28-
targetIsRemote: true,
2+
import { buildClaudeExecutionPermissionArgs, buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
3+
4+
describe("Claude full-auto permission args", () => {
5+
for (const [name, build] of [["execution", buildClaudeExecutionPermissionArgs], ["probe", buildClaudeProbePermissionArgs]] as const) {
6+
it.each([
7+
{ targetIsRemote: false, localProcessUid: 1000 },
8+
{ targetIsRemote: true, localProcessUid: 1000 },
9+
{ targetIsRemote: false, localProcessUid: 0 },
10+
{ targetIsRemote: true, localProcessUid: 0 },
11+
])(`${name} requests full bypass for %j`, (target) => {
12+
expect(build({ ...target, dangerouslySkipPermissions: true }))
13+
.toEqual(["--dangerously-skip-permissions"]);
14+
expect(build({ ...target, dangerouslySkipPermissions: false })).toEqual([]);
2915
});
16+
}
3017

31-
expect(allowedTools.split(" ")).toContain("Bash");
32-
expect(allowedTools).not.toContain("Bash(*)");
33-
});
34-
35-
it("does not pass permission flags when skip-permissions is disabled", () => {
36-
expect(buildClaudeExecutionPermissionArgs({ dangerouslySkipPermissions: false, targetIsRemote: true })).toEqual([]);
37-
expect(buildClaudeProbePermissionArgs({ dangerouslySkipPermissions: false, targetIsRemote: true })).toEqual([]);
38-
});
39-
40-
it("uses dangerously-skip-permissions for non-root local execution", () => {
41-
expect(
42-
buildClaudeExecutionPermissionArgs({
43-
dangerouslySkipPermissions: true,
44-
targetIsRemote: false,
45-
localProcessUid: 1000,
46-
}),
47-
).toEqual(["--dangerously-skip-permissions"]);
48-
});
49-
50-
it("uses dangerously-skip-permissions for non-root local probes", () => {
51-
expect(
52-
buildClaudeProbePermissionArgs({
53-
dangerouslySkipPermissions: true,
54-
targetIsRemote: false,
55-
localProcessUid: 1000,
56-
}),
57-
).toEqual(["--dangerously-skip-permissions"]);
58-
});
59-
60-
it("uses allowedTools for local root execution because Claude refuses dangerously-skip-permissions as root", () => {
61-
expect(
62-
buildClaudeExecutionPermissionArgs({
63-
dangerouslySkipPermissions: true,
64-
targetIsRemote: false,
65-
localProcessUid: 0,
66-
}),
67-
).toEqual(["--allowedTools", SANDBOX_ALLOWED_TOOLS]);
68-
});
69-
70-
it("uses allowedTools for local root probes because Claude refuses dangerously-skip-permissions as root", () => {
71-
expect(
72-
buildClaudeProbePermissionArgs({
73-
dangerouslySkipPermissions: true,
74-
targetIsRemote: false,
75-
localProcessUid: 0,
76-
}),
77-
).toEqual(["--allowedTools", SANDBOX_ALLOWED_TOOLS]);
18+
it("identifies managed sandboxes for Claude's root launch check only when full auto is enabled", () => {
19+
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: true, targetIsSandbox: true })).toEqual({ IS_SANDBOX: "1" });
20+
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: false, targetIsSandbox: true })).toEqual({});
21+
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: true, targetIsSandbox: false })).toEqual({});
7822
});
7923
});
Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,23 @@
1-
// Explicit allowlist of Claude Code tools we permit when running on a remote
2-
// target. We use this instead of `--dangerously-skip-permissions` for remote
3-
// targets because the permission-approval prompts can't be answered by a
4-
// human inside a non-interactive run, but blanket-allowing every tool would
5-
// defeat the point of having a separate hosted/sandbox code path.
6-
//
7-
// Maintenance: this list must be reviewed when Claude Code releases a new
8-
// tool. The canonical list of built-in tools is documented at
9-
// https://docs.claude.com/en/docs/claude-code/built-in-tools — when a tool
10-
// is added there, decide whether it should be allowed in remote runs and
11-
// either add it here or document the deliberate exclusion. Omitting a tool
12-
// silently disables it inside remote targets, which can look like the tool is
13-
// "broken" rather than intentionally gated.
14-
const SANDBOX_ALLOWED_TOOLS =
15-
"Task AskUserQuestion Bash CronCreate CronDelete CronList Edit " +
16-
"EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Glob Grep Monitor " +
17-
"NotebookEdit PushNotification Read RemoteTrigger ScheduleWakeup Skill " +
18-
"TaskOutput TaskStop TodoWrite ToolSearch WebFetch WebSearch Write";
19-
20-
function shouldUseAllowedTools(input: { targetIsRemote: boolean; localProcessUid?: number | null }): boolean {
21-
// Claude Code refuses `--dangerously-skip-permissions` when the process runs
22-
// as root. Use the same explicit allowlist that remote targets use so local
23-
// Docker/root probes and executions fail safe instead of hard-failing before
24-
// auth/runtime validation can complete.
25-
return input.targetIsRemote || input.localProcessUid === 0;
26-
}
27-
28-
export function buildClaudeProbePermissionArgs(input: {
1+
interface ClaudePermissionInput {
292
dangerouslySkipPermissions: boolean;
303
targetIsRemote: boolean;
314
localProcessUid?: number | null;
32-
}): string[] {
33-
if (!input.dangerouslySkipPermissions) return [];
34-
// For remote targets and local root processes, mirror the execution path:
35-
// pass `--allowedTools` with the curated allowlist instead of dropping the
36-
// flag entirely. The hello probe is a one-shot prompt that should never
37-
// trigger a tool, but if a future probe prompt does, we don't want Claude CLI
38-
// to stall on an interactive permission prompt that no human can answer.
39-
if (shouldUseAllowedTools(input)) return ["--allowedTools", SANDBOX_ALLOWED_TOOLS];
40-
return ["--dangerously-skip-permissions"];
415
}
426

43-
export function buildClaudeExecutionPermissionArgs(input: {
7+
// Permission defaults are identical for local, remote, and connected tools.
8+
// A tool allowlist is not equivalent to full bypass: it misses MCP tools and
9+
// tools added by later provider releases. Let Claude enforce its own launch
10+
// requirements rather than silently downgrading the requested permission mode.
11+
export function buildClaudeExecutionPermissionArgs(input: ClaudePermissionInput): string[] {
12+
return input.dangerouslySkipPermissions ? ["--dangerously-skip-permissions"] : [];
13+
}
14+
15+
export const buildClaudeProbePermissionArgs = buildClaudeExecutionPermissionArgs;
16+
17+
/** Claude permits full bypass as root only inside an identified sandbox. */
18+
export function claudeSandboxPermissionEnv(input: {
4419
dangerouslySkipPermissions: boolean;
45-
targetIsRemote: boolean;
46-
localProcessUid?: number | null;
47-
}): string[] {
48-
if (!input.dangerouslySkipPermissions) return [];
49-
if (shouldUseAllowedTools(input)) {
50-
return ["--allowedTools", SANDBOX_ALLOWED_TOOLS];
51-
}
52-
return ["--dangerously-skip-permissions"];
20+
targetIsSandbox: boolean;
21+
}): Record<string, string> {
22+
return input.dangerouslySkipPermissions && input.targetIsSandbox ? { IS_SANDBOX: "1" } : {};
5323
}

packages/adapters/claude-local/src/server/test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
readClaudeCommandVersion,
3333
} from "./cli-capabilities.js";
3434
import { isBedrockModelId } from "./models.js";
35-
import { buildClaudeProbePermissionArgs } from "./permissions.js";
35+
import { buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
3636
import { prepareSandboxClaudeProbeRuntime } from "./claude-config.js";
3737
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
3838
import { resolveClaudeExecutionEngineForRun, testClaudeAcpEnvironment } from "./acp.js";
@@ -322,6 +322,7 @@ export async function testEnvironment(
322322
const chrome = asBoolean(config.chrome, false);
323323
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
324324
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
325+
Object.assign(env, claudeSandboxPermissionEnv({ dangerouslySkipPermissions, targetIsSandbox }));
325326
const extraArgs = (() => {
326327
const fromExtraArgs = asStringArray(config.extraArgs);
327328
if (fromExtraArgs.length > 0) return fromExtraArgs;

0 commit comments

Comments
 (0)