mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
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>
This commit is contained in:
1 parent
7bc03e0acd
commit
45c99a0d06
20 files changed
+119
-220
No files matched your search
@@ -53,7 +53,7 @@ Core fields:
|
||||
- chrome (boolean, optional): pass --chrome when running Claude
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
- maxTurnsPerRun (number, optional): max turns for one run
|
||||
- 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.
|
||||
- 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.
|
||||
- command (string, optional): defaults to "claude"
|
||||
- extraArgs (string[], optional): additional CLI args
|
||||
- env (object, optional): KEY=VALUE environment variables
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
import { createWorkspaceRestoreTeardown } from "@paperclipai/adapter-utils/workspace-restore-teardown";
|
||||
import { buildLocalAdapterTestProbeEnv } from "./probe-env.js";
|
||||
import { detectClaudeLoginRequired, extractClaudeRetryNotBefore, isClaudeProviderQuotaError, parseClaudeStreamJson } from "./parse.js";
|
||||
import { buildClaudeProbePermissionArgs } from "./permissions.js";
|
||||
import { buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
|
||||
import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js";
|
||||
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
@@ -647,6 +647,9 @@ export async function probeClaudeAcpSandboxLogin(input: {
|
||||
cwd = asString(config.cwd, process.cwd());
|
||||
}
|
||||
|
||||
Object.assign(env, claudeSandboxPermissionEnv({
|
||||
dangerouslySkipPermissions: asBoolean(config.dangerouslySkipPermissions, true), targetIsSandbox,
|
||||
}));
|
||||
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
|
||||
if (config.managedAiConnection) args.push("--setting-sources", "user");
|
||||
args.push(
|
||||
|
||||
@@ -193,11 +193,8 @@ describe("claude remote execution", () => {
|
||||
| [string, string, string[], { env: Record<string, string>; remoteExecution?: { remoteCwd: string } | null }]
|
||||
| undefined;
|
||||
expect(call?.[2]).toEqual(expect.arrayContaining(["--model", "claude-opus-5"]));
|
||||
expect(call?.[2]).toContain("--allowedTools");
|
||||
expect(call?.[2]).toContain(
|
||||
"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",
|
||||
);
|
||||
expect(call?.[2]).not.toContain("--dangerously-skip-permissions");
|
||||
expect(call?.[2]).toContain("--dangerously-skip-permissions");
|
||||
expect(call?.[2]).not.toContain("--allowedTools");
|
||||
expect(call?.[2]).toContain("--append-system-prompt-file");
|
||||
expect(call?.[2]).toContain(
|
||||
`${managedRemoteWorkspace}/.paperclip-runtime/claude/skills/agent-instructions.md`,
|
||||
|
||||
@@ -92,7 +92,7 @@ import {
|
||||
import { resolveClaudeDesiredSkillNames } from "./skills.js";
|
||||
import { isBedrockModelId } from "./models.js";
|
||||
import { prepareClaudePromptBundle } from "./prompt-cache.js";
|
||||
import { buildClaudeExecutionPermissionArgs } from "./permissions.js";
|
||||
import { buildClaudeExecutionPermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
|
||||
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import {
|
||||
createClaudeAcpExecutor,
|
||||
@@ -480,6 +480,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
graceSec,
|
||||
extraArgs,
|
||||
} = runtimeConfig;
|
||||
Object.assign(env, claudeSandboxPermissionEnv({ dangerouslySkipPermissions, targetIsSandbox: executionTargetIsSandbox }));
|
||||
let loggedEnv = initialLoggedEnv;
|
||||
let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
|
||||
const terminalResultCleanupGraceMs = Math.max(
|
||||
@@ -937,7 +938,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
}
|
||||
if (dangerouslySkipPermissions && executionTargetIsRemote) {
|
||||
commandNotes.push(
|
||||
"Using a broad --allowedTools whitelist for remote execution so hosted targets do not inherit local Claude bypass permissions.",
|
||||
"Using full Claude permission bypass for remote execution, including connected tools.",
|
||||
);
|
||||
}
|
||||
if (attemptInstructionsFilePath && !resumeSessionId) {
|
||||
|
||||
@@ -1,79 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildClaudeExecutionPermissionArgs, buildClaudeProbePermissionArgs } from "./permissions.js";
|
||||
import { buildClaudeExecutionPermissionArgs, buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
|
||||
|
||||
const SANDBOX_ALLOWED_TOOLS =
|
||||
"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";
|
||||
|
||||
describe("claude-local remote permission args", () => {
|
||||
it("uses the canonical Bash tool grant for remote execution", () => {
|
||||
expect(buildClaudeExecutionPermissionArgs({ dangerouslySkipPermissions: true, targetIsRemote: true })).toEqual([
|
||||
"--allowedTools",
|
||||
SANDBOX_ALLOWED_TOOLS,
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the canonical Bash tool grant for remote probes", () => {
|
||||
expect(buildClaudeProbePermissionArgs({ dangerouslySkipPermissions: true, targetIsRemote: true })).toEqual([
|
||||
"--allowedTools",
|
||||
SANDBOX_ALLOWED_TOOLS,
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not use Bash(*) because Claude Code treats Bash grants as command-prefix patterns", () => {
|
||||
const [, allowedTools] = buildClaudeExecutionPermissionArgs({
|
||||
dangerouslySkipPermissions: true,
|
||||
targetIsRemote: true,
|
||||
describe("Claude full-auto permission args", () => {
|
||||
for (const [name, build] of [["execution", buildClaudeExecutionPermissionArgs], ["probe", buildClaudeProbePermissionArgs]] as const) {
|
||||
it.each([
|
||||
{ targetIsRemote: false, localProcessUid: 1000 },
|
||||
{ targetIsRemote: true, localProcessUid: 1000 },
|
||||
{ targetIsRemote: false, localProcessUid: 0 },
|
||||
{ targetIsRemote: true, localProcessUid: 0 },
|
||||
])(`${name} requests full bypass for %j`, (target) => {
|
||||
expect(build({ ...target, dangerouslySkipPermissions: true }))
|
||||
.toEqual(["--dangerously-skip-permissions"]);
|
||||
expect(build({ ...target, dangerouslySkipPermissions: false })).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allowedTools.split(" ")).toContain("Bash");
|
||||
expect(allowedTools).not.toContain("Bash(*)");
|
||||
});
|
||||
|
||||
it("does not pass permission flags when skip-permissions is disabled", () => {
|
||||
expect(buildClaudeExecutionPermissionArgs({ dangerouslySkipPermissions: false, targetIsRemote: true })).toEqual([]);
|
||||
expect(buildClaudeProbePermissionArgs({ dangerouslySkipPermissions: false, targetIsRemote: true })).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses dangerously-skip-permissions for non-root local execution", () => {
|
||||
expect(
|
||||
buildClaudeExecutionPermissionArgs({
|
||||
dangerouslySkipPermissions: true,
|
||||
targetIsRemote: false,
|
||||
localProcessUid: 1000,
|
||||
}),
|
||||
).toEqual(["--dangerously-skip-permissions"]);
|
||||
});
|
||||
|
||||
it("uses dangerously-skip-permissions for non-root local probes", () => {
|
||||
expect(
|
||||
buildClaudeProbePermissionArgs({
|
||||
dangerouslySkipPermissions: true,
|
||||
targetIsRemote: false,
|
||||
localProcessUid: 1000,
|
||||
}),
|
||||
).toEqual(["--dangerously-skip-permissions"]);
|
||||
});
|
||||
|
||||
it("uses allowedTools for local root execution because Claude refuses dangerously-skip-permissions as root", () => {
|
||||
expect(
|
||||
buildClaudeExecutionPermissionArgs({
|
||||
dangerouslySkipPermissions: true,
|
||||
targetIsRemote: false,
|
||||
localProcessUid: 0,
|
||||
}),
|
||||
).toEqual(["--allowedTools", SANDBOX_ALLOWED_TOOLS]);
|
||||
});
|
||||
|
||||
it("uses allowedTools for local root probes because Claude refuses dangerously-skip-permissions as root", () => {
|
||||
expect(
|
||||
buildClaudeProbePermissionArgs({
|
||||
dangerouslySkipPermissions: true,
|
||||
targetIsRemote: false,
|
||||
localProcessUid: 0,
|
||||
}),
|
||||
).toEqual(["--allowedTools", SANDBOX_ALLOWED_TOOLS]);
|
||||
it("identifies managed sandboxes for Claude's root launch check only when full auto is enabled", () => {
|
||||
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: true, targetIsSandbox: true })).toEqual({ IS_SANDBOX: "1" });
|
||||
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: false, targetIsSandbox: true })).toEqual({});
|
||||
expect(claudeSandboxPermissionEnv({ dangerouslySkipPermissions: true, targetIsSandbox: false })).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +1,23 @@
|
||||
// Explicit allowlist of Claude Code tools we permit when running on a remote
|
||||
// target. We use this instead of `--dangerously-skip-permissions` for remote
|
||||
// targets because the permission-approval prompts can't be answered by a
|
||||
// human inside a non-interactive run, but blanket-allowing every tool would
|
||||
// defeat the point of having a separate hosted/sandbox code path.
|
||||
//
|
||||
// Maintenance: this list must be reviewed when Claude Code releases a new
|
||||
// tool. The canonical list of built-in tools is documented at
|
||||
// https://docs.claude.com/en/docs/claude-code/built-in-tools — when a tool
|
||||
// is added there, decide whether it should be allowed in remote runs and
|
||||
// either add it here or document the deliberate exclusion. Omitting a tool
|
||||
// silently disables it inside remote targets, which can look like the tool is
|
||||
// "broken" rather than intentionally gated.
|
||||
const SANDBOX_ALLOWED_TOOLS =
|
||||
"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";
|
||||
|
||||
function shouldUseAllowedTools(input: { targetIsRemote: boolean; localProcessUid?: number | null }): boolean {
|
||||
// Claude Code refuses `--dangerously-skip-permissions` when the process runs
|
||||
// as root. Use the same explicit allowlist that remote targets use so local
|
||||
// Docker/root probes and executions fail safe instead of hard-failing before
|
||||
// auth/runtime validation can complete.
|
||||
return input.targetIsRemote || input.localProcessUid === 0;
|
||||
}
|
||||
|
||||
export function buildClaudeProbePermissionArgs(input: {
|
||||
interface ClaudePermissionInput {
|
||||
dangerouslySkipPermissions: boolean;
|
||||
targetIsRemote: boolean;
|
||||
localProcessUid?: number | null;
|
||||
}): string[] {
|
||||
if (!input.dangerouslySkipPermissions) return [];
|
||||
// For remote targets and local root processes, mirror the execution path:
|
||||
// pass `--allowedTools` with the curated allowlist instead of dropping the
|
||||
// flag entirely. The hello probe is a one-shot prompt that should never
|
||||
// trigger a tool, but if a future probe prompt does, we don't want Claude CLI
|
||||
// to stall on an interactive permission prompt that no human can answer.
|
||||
if (shouldUseAllowedTools(input)) return ["--allowedTools", SANDBOX_ALLOWED_TOOLS];
|
||||
return ["--dangerously-skip-permissions"];
|
||||
}
|
||||
|
||||
export function buildClaudeExecutionPermissionArgs(input: {
|
||||
// Permission defaults are identical for local, remote, and connected tools.
|
||||
// A tool allowlist is not equivalent to full bypass: it misses MCP tools and
|
||||
// tools added by later provider releases. Let Claude enforce its own launch
|
||||
// requirements rather than silently downgrading the requested permission mode.
|
||||
export function buildClaudeExecutionPermissionArgs(input: ClaudePermissionInput): string[] {
|
||||
return input.dangerouslySkipPermissions ? ["--dangerously-skip-permissions"] : [];
|
||||
}
|
||||
|
||||
export const buildClaudeProbePermissionArgs = buildClaudeExecutionPermissionArgs;
|
||||
|
||||
/** Claude permits full bypass as root only inside an identified sandbox. */
|
||||
export function claudeSandboxPermissionEnv(input: {
|
||||
dangerouslySkipPermissions: boolean;
|
||||
targetIsRemote: boolean;
|
||||
localProcessUid?: number | null;
|
||||
}): string[] {
|
||||
if (!input.dangerouslySkipPermissions) return [];
|
||||
if (shouldUseAllowedTools(input)) {
|
||||
return ["--allowedTools", SANDBOX_ALLOWED_TOOLS];
|
||||
}
|
||||
return ["--dangerously-skip-permissions"];
|
||||
targetIsSandbox: boolean;
|
||||
}): Record<string, string> {
|
||||
return input.dangerouslySkipPermissions && input.targetIsSandbox ? { IS_SANDBOX: "1" } : {};
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
readClaudeCommandVersion,
|
||||
} from "./cli-capabilities.js";
|
||||
import { isBedrockModelId } from "./models.js";
|
||||
import { buildClaudeProbePermissionArgs } from "./permissions.js";
|
||||
import { buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
|
||||
import { prepareSandboxClaudeProbeRuntime } from "./claude-config.js";
|
||||
import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { resolveClaudeExecutionEngineForRun, testClaudeAcpEnvironment } from "./acp.js";
|
||||
@@ -322,6 +322,7 @@ export async function testEnvironment(
|
||||
const chrome = asBoolean(config.chrome, false);
|
||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
|
||||
Object.assign(env, claudeSandboxPermissionEnv({ dangerouslySkipPermissions, targetIsSandbox }));
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
|
||||
@@ -2,6 +2,13 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildCodexExecArgs } from "./codex-args.js";
|
||||
|
||||
describe("buildCodexExecArgs", () => {
|
||||
it.each([null, "existing-session"])("defaults direct and resumed launches to full bypass (%s)", (resumeSessionId) => {
|
||||
const { args } = buildCodexExecArgs({}, { resumeSessionId });
|
||||
expect(args).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
expect(args).not.toContain('sandbox_mode="workspace-write"');
|
||||
if (resumeSessionId) expect(args.slice(-3)).toEqual(["resume", resumeSessionId, "-"]);
|
||||
});
|
||||
|
||||
it("forwards GPT-6 Astra, its ultra reasoning effort, and fast mode", () => {
|
||||
const result = buildCodexExecArgs({
|
||||
model: "gpt-6-astra",
|
||||
@@ -15,10 +22,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-6-astra",
|
||||
"-c",
|
||||
@@ -58,10 +62,7 @@ describe("buildCodexExecArgs", () => {
|
||||
"--search",
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5.4",
|
||||
"-c",
|
||||
@@ -84,10 +85,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5.5",
|
||||
"-c",
|
||||
@@ -110,10 +108,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"future-codex-model",
|
||||
"-c",
|
||||
@@ -135,10 +130,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"-c",
|
||||
'service_tier="fast"',
|
||||
"-c",
|
||||
@@ -161,10 +153,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5",
|
||||
"-",
|
||||
@@ -182,10 +171,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5.4-mini",
|
||||
"-",
|
||||
@@ -203,11 +189,8 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--skip-git-repo-check",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5.5",
|
||||
"-",
|
||||
@@ -227,10 +210,7 @@ describe("buildCodexExecArgs", () => {
|
||||
expect(result.args).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--model",
|
||||
"gpt-5.5",
|
||||
"--skip-git-repo-check",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asBoolean, asString, asStringArray } from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS,
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
isCodexLocalFastModeSupported,
|
||||
normalizeCodexModel,
|
||||
} from "../index.js";
|
||||
@@ -48,20 +49,21 @@ export function buildCodexExecArgs(
|
||||
const search = asBoolean(record.search, false);
|
||||
const fastModeRequested = asBoolean(record.fastMode, false);
|
||||
const fastModeApplied = fastModeRequested && isCodexLocalFastModeSupported(model);
|
||||
const bypass = asBoolean(
|
||||
record.dangerouslyBypassApprovalsAndSandbox,
|
||||
asBoolean(record.dangerouslyBypassSandbox, false),
|
||||
);
|
||||
const extraArgs = readExtraArgs(record);
|
||||
|
||||
const args = ["exec", "--json"];
|
||||
// `codex exec` otherwise defaults to read-only/never, which cannot perform
|
||||
// Paperclip work. Keep the sandbox, but make unattended workspace work and
|
||||
// API calls possible. Explicit operator modes/profiles retain their meaning.
|
||||
// Explicit CLI modes/profiles remain deliberate overrides. An omitted
|
||||
// setting uses the same full-auto default as agent creation and onboarding.
|
||||
const explicitSandbox = extraArgs.some((arg) =>
|
||||
/^(--sandbox(?:=|$)|-s|--profile(?:=|$)|-p|--full-auto$|--yolo$|--dangerously-bypass-approvals-and-sandbox$)/.test(arg)
|
||||
|| /^(?:(?:--config=|-c=?)\s*)?(?:sandbox_mode|profile)\s*=/.test(arg),
|
||||
);
|
||||
const explicitPermissionRestriction = extraArgs.some((arg) =>
|
||||
/^(?:(?:--config=|-c=?)\s*)?(?:approval_policy\s*=|sandbox_workspace_write\.network_access\s*=\s*false)/.test(arg),
|
||||
);
|
||||
const bypass = asBoolean(
|
||||
record.dangerouslyBypassApprovalsAndSandbox,
|
||||
asBoolean(record.dangerouslyBypassSandbox, !explicitSandbox && !explicitPermissionRestriction && options.networkAccess !== false && DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX),
|
||||
);
|
||||
const args = ["exec", "--json"];
|
||||
if (!bypass && !explicitSandbox) {
|
||||
args.push("-c", 'sandbox_mode="workspace-write"');
|
||||
args.push("-c", `sandbox_workspace_write.network_access=${options.networkAccess !== false}`);
|
||||
|
||||
@@ -394,10 +394,7 @@ describe("codex remote execution", () => {
|
||||
expect(call?.[2]).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"-",
|
||||
]);
|
||||
});
|
||||
@@ -470,10 +467,7 @@ describe("codex remote execution", () => {
|
||||
expect(call?.[2]).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"resume",
|
||||
"session-123",
|
||||
"-",
|
||||
@@ -554,10 +548,7 @@ describe("codex remote execution", () => {
|
||||
expect(call?.[2]).toEqual([
|
||||
"exec",
|
||||
"--json",
|
||||
"-c",
|
||||
'sandbox_mode="workspace-write"',
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"resume",
|
||||
"session-123",
|
||||
"-",
|
||||
|
||||
@@ -192,7 +192,7 @@ export async function testEnvironment(
|
||||
});
|
||||
} else {
|
||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
|
||||
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, true) ? "yolo" : "default");
|
||||
const sandbox = asBoolean(config.sandbox, false);
|
||||
const helloProbeTimeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, 60));
|
||||
const extraArgs = (() => {
|
||||
|
||||
@@ -80,7 +80,7 @@ Core fields:
|
||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||
- model (string, required): OpenCode model id in provider/model format (for example anthropic/claude-sonnet-4-5)
|
||||
- variant (string, optional): provider-specific reasoning/profile variant passed as --variant (for example minimal|low|medium|high|xhigh|max)
|
||||
- dangerouslySkipPermissions (boolean, optional): inject a runtime OpenCode config that allows \`external_directory\` access without interactive prompts; defaults to true for unattended Paperclip runs
|
||||
- dangerouslySkipPermissions (boolean, optional): inject a runtime OpenCode config with \`permission=allow\` for all tools and connections; defaults to true for unattended Paperclip runs
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
- command (string, optional): defaults to "opencode"
|
||||
- extraArgs (string[], optional): additional CLI args
|
||||
@@ -100,6 +100,6 @@ Notes:
|
||||
writing an opencode.json config file into the project working directory. Model \
|
||||
selection is passed via the --model CLI flag instead.
|
||||
- When \`dangerouslySkipPermissions\` is enabled, Paperclip injects a temporary \
|
||||
runtime config with \`permission.external_directory=allow\` so headless runs do \
|
||||
runtime config with \`permission=allow\` so headless runs do \
|
||||
not stall on approval prompts.
|
||||
`;
|
||||
@@ -31,10 +31,12 @@ async function makeConfigHome(initialConfig?: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
describe("prepareOpenCodeRuntimeConfig", () => {
|
||||
it("injects an external_directory allow rule by default", async () => {
|
||||
it("allows all tools and connected tools by default", async () => {
|
||||
const configHome = await makeConfigHome({
|
||||
permission: {
|
||||
read: "allow",
|
||||
bash: "ask",
|
||||
"mcp__example__write": "deny",
|
||||
},
|
||||
theme: "system",
|
||||
});
|
||||
@@ -54,10 +56,7 @@ describe("prepareOpenCodeRuntimeConfig", () => {
|
||||
) as Record<string, unknown>;
|
||||
expect(runtimeConfig).toMatchObject({
|
||||
theme: "system",
|
||||
permission: {
|
||||
read: "allow",
|
||||
external_directory: "allow",
|
||||
},
|
||||
permission: "allow",
|
||||
});
|
||||
|
||||
await prepared.cleanup();
|
||||
@@ -92,7 +91,7 @@ describe("prepareOpenCodeRuntimeConfig", () => {
|
||||
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
expect(runtimeConfig).toMatchObject({
|
||||
permission: { read: "allow", external_directory: "allow" },
|
||||
permission: "allow",
|
||||
provider: providers,
|
||||
});
|
||||
expect(prepared.notes.some((n) => n.includes("bifrost"))).toBe(true);
|
||||
|
||||
@@ -149,11 +149,8 @@ export async function prepareOpenCodeRuntimeConfig(input: {
|
||||
}
|
||||
|
||||
const existingConfig = await readJsonObject(runtimeConfigPath);
|
||||
const existingPermission = isPlainObject(existingConfig.permission)
|
||||
? existingConfig.permission
|
||||
: {};
|
||||
const notes = [
|
||||
"Injected runtime OpenCode config with permission.external_directory=allow to avoid headless approval prompts.",
|
||||
"Injected runtime OpenCode config with permission=allow for all tools and connections.",
|
||||
];
|
||||
|
||||
// Merge gateway/custom provider definitions supplied via PAPERCLIP_OPENCODE_PROVIDERS
|
||||
@@ -207,10 +204,7 @@ export async function prepareOpenCodeRuntimeConfig(input: {
|
||||
|
||||
const nextConfig: Record<string, unknown> = {
|
||||
...existingConfig,
|
||||
permission: {
|
||||
...existingPermission,
|
||||
external_directory: "allow",
|
||||
},
|
||||
permission: "allow",
|
||||
};
|
||||
if (Object.keys(nextProvider).length > 0) {
|
||||
nextConfig.provider = nextProvider;
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function testEnvironment(
|
||||
checks.push({
|
||||
code: "opencode_headless_permissions_enabled",
|
||||
level: "info",
|
||||
message: "Headless OpenCode external-directory permissions are auto-approved for unattended runs.",
|
||||
message: "Headless OpenCode permissions are auto-approved for all tools and connections.",
|
||||
});
|
||||
}
|
||||
let restoreWorkspace: (() => Promise<void>) | null = null;
|
||||
|
||||
Reference in new issue
Block a user