diff --git a/.gitignore b/.gitignore index 61b00a225c..f8de288985 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage/ .DS_Store data/ .paperclip/ +.paperclip-home/ .pnpm-store/ tmp-* cli/tmp/ @@ -50,3 +51,4 @@ tests/release-smoke/test-results/ tests/release-smoke/playwright-report/ .superset/ .claude/worktrees/ +.gstack/ diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index c755c62714..13c7b1a92c 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -356,9 +356,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 && - (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd)); + const cwdMatchesPrevious = runtimeSessionCwd.length > 0 && path.resolve(runtimeSessionCwd) === path.resolve(cwd); + const cwdUnknown = runtimeSessionCwd.length === 0; + const canResumeSession = runtimeSessionId.length > 0 && (cwdUnknown || cwdMatchesPrevious); const sessionId = canResumeSession ? runtimeSessionId : null; if (runtimeSessionId && !canResumeSession) { await onLog( @@ -366,6 +366,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise {}); + await fs.rm(skillsDir, { recursive: true, force: true }).catch(() => {}); } } diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index b6bda8dfa7..03662977ec 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -396,7 +396,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 && (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd)); - const sessionId = canResumeSession ? runtimeSessionId : null; + // Only resume a session when the run was triggered by an actual task assignment. + // Idle heartbeat runs always start fresh to prevent unbounded session growth and + // the associated cache-miss cost (~400K–900K fresh tokens per idle run). + const isTaskRun = !!wakeTaskId; + const sessionId = (canResumeSession && isTaskRun) ? runtimeSessionId : null; if (runtimeSessionId && !canResumeSession) { await onLog( "stdout", diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 0c5aa424fd..3328349b74 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -31,6 +31,7 @@ export const AGENT_ADAPTER_TYPES = [ "cursor", "openclaw_gateway", "hermes_local", + "gemini_local", ] as const; export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number]; diff --git a/server/src/__tests__/agent-update-routes.test.ts b/server/src/__tests__/agent-update-routes.test.ts new file mode 100644 index 0000000000..6d0d27f0ca --- /dev/null +++ b/server/src/__tests__/agent-update-routes.test.ts @@ -0,0 +1,330 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { agentRoutes } from "../routes/agents.js"; +import { errorHandler } from "../middleware/index.js"; + +const agentId = "11111111-1111-4111-8111-111111111111"; +const companyId = "22222222-2222-4222-8222-222222222222"; + +const baseAgent = { + id: agentId, + companyId, + name: "Frontend Developer", + urlKey: "frontend-developer", + role: "engineer", + title: "Frontend Developer", + icon: "code", + status: "idle", + reportsTo: null, + capabilities: null, + adapterType: "claude_local", + adapterConfig: { + cwd: "/workspace/app", + model: "claude-sonnet-4-6", + maxTurnsPerRun: 40, + chrome: true, + paperclipSkillSync: { + desiredSkills: ["paperclipai/paperclip/paperclip"], + }, + instructionsBundleMode: "managed", + instructionsRootPath: "/tmp/agent/instructions", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/agent/instructions/AGENTS.md", + }, + runtimeConfig: {}, + budgetMonthlyCents: 0, + spentMonthlyCents: 0, + pauseReason: null, + pausedAt: null, + permissions: { canCreateAgents: false }, + lastHeartbeatAt: null, + metadata: null, + createdAt: new Date("2026-03-19T00:00:00.000Z"), + updatedAt: new Date("2026-03-19T00:00:00.000Z"), +}; + +const mockAgentService = vi.hoisted(() => ({ + getById: vi.fn(), + update: vi.fn(), + getChainOfCommand: vi.fn(), +})); + +const mockAccessService = vi.hoisted(() => ({ + canUser: vi.fn(), + hasPermission: vi.fn(), + getMembership: vi.fn(), + ensureMembership: vi.fn(), + listPrincipalGrants: vi.fn(), + setPrincipalPermission: vi.fn(), +})); + +const mockApprovalService = vi.hoisted(() => ({})); +const mockBudgetService = vi.hoisted(() => ({})); +const mockHeartbeatService = vi.hoisted(() => ({})); +const mockIssueApprovalService = vi.hoisted(() => ({})); +const mockIssueService = vi.hoisted(() => ({})); +const mockAgentInstructionsService = vi.hoisted(() => ({ + materializeManagedBundle: vi.fn(), +})); +const mockCompanySkillService = vi.hoisted(() => ({ + listRuntimeSkillEntries: vi.fn(), + resolveRequestedSkillKeys: vi.fn(), +})); +const mockWorkspaceOperationService = vi.hoisted(() => ({})); +const mockInstanceSettingsService = vi.hoisted(() => ({ + getGeneral: vi.fn(), +})); +const mockSecretService = vi.hoisted(() => ({ + normalizeAdapterConfigForPersistence: vi.fn(), + resolveAdapterConfigForRuntime: vi.fn(), +})); +const mockLogActivity = vi.hoisted(() => vi.fn()); +const mockSyncInstructionsBundleConfigFromFilePath = vi.hoisted(() => vi.fn((_agent, config) => config)); +const mockEnsureOpenCodeModelConfiguredAndAvailable = vi.hoisted(() => vi.fn()); + +vi.mock("../services/index.js", () => ({ + agentService: () => mockAgentService, + agentInstructionsService: () => mockAgentInstructionsService, + accessService: () => mockAccessService, + approvalService: () => mockApprovalService, + companySkillService: () => mockCompanySkillService, + budgetService: () => mockBudgetService, + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => mockIssueApprovalService, + issueService: () => mockIssueService, + logActivity: mockLogActivity, + secretService: () => mockSecretService, + syncInstructionsBundleConfigFromFilePath: mockSyncInstructionsBundleConfigFromFilePath, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +vi.mock("../services/instance-settings.js", () => ({ + instanceSettingsService: () => mockInstanceSettingsService, +})); + +vi.mock("../adapters/index.js", () => ({ + findServerAdapter: vi.fn(() => null), + listAdapterModels: vi.fn(), +})); + +vi.mock("@paperclipai/adapter-opencode-local/server", () => ({ + ensureOpenCodeModelConfiguredAndAvailable: mockEnsureOpenCodeModelConfiguredAndAvailable, +})); + +function createDbStub() { + return { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + then: vi.fn().mockResolvedValue([{ + id: companyId, + name: "Paperclip", + requireBoardApprovalForNewAgents: false, + }]), + }), + }), + }), + }; +} + +function createApp() { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as any).actor = { + type: "board", + userId: "local-board", + companyIds: [companyId], + source: "local_implicit", + isInstanceAdmin: true, + }; + next(); + }); + app.use("/api", agentRoutes(createDbStub() as any)); + app.use(errorHandler); + return app; +} + +describe("agent update routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockAgentService.getById.mockResolvedValue(baseAgent); + mockAgentService.getChainOfCommand.mockResolvedValue([]); + mockAgentService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...baseAgent, + ...patch, + })); + mockAccessService.getMembership.mockResolvedValue(null); + mockAccessService.listPrincipalGrants.mockResolvedValue([]); + mockAccessService.canUser.mockResolvedValue(true); + mockAccessService.hasPermission.mockResolvedValue(true); + mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false }); + mockCompanySkillService.listRuntimeSkillEntries.mockResolvedValue([]); + mockCompanySkillService.resolveRequestedSkillKeys.mockResolvedValue([]); + mockSecretService.normalizeAdapterConfigForPersistence.mockImplementation(async (_companyId, config) => config); + mockSecretService.resolveAdapterConfigForRuntime.mockImplementation(async (_companyId, config) => ({ config })); + mockEnsureOpenCodeModelConfiguredAndAvailable.mockResolvedValue(undefined); + mockLogActivity.mockResolvedValue(undefined); + }); + + it("preserves managed instructions and shared runtime config when switching adapters", async () => { + const app = createApp(); + + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "opencode_local", + adapterConfig: { + model: "zai-coding-plan/glm-5-turbo", + command: "/home/victor/.opencode/bin/opencode", + url: "ws://127.0.0.1:18789", + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockAgentService.update).toHaveBeenCalledTimes(1); + + const [, patch] = mockAgentService.update.mock.calls[0]!; + expect(patch.adapterType).toBe("opencode_local"); + expect(patch.adapterConfig).toMatchObject({ + cwd: "/workspace/app", + maxTurnsPerRun: 40, + model: "zai-coding-plan/glm-5-turbo", + command: "/home/victor/.opencode/bin/opencode", + url: "ws://127.0.0.1:18789", + paperclipSkillSync: { + desiredSkills: ["paperclipai/paperclip/paperclip"], + }, + instructionsBundleMode: "managed", + instructionsRootPath: "/tmp/agent/instructions", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/agent/instructions/AGENTS.md", + }); + expect((patch.adapterConfig as Record).chrome).toBeUndefined(); + }); + + it("preserves promptTemplate, extraArgs, timeoutSec, graceSec across adapter switch", async () => { + mockAgentService.getById.mockResolvedValue({ + ...baseAgent, + adapterConfig: { + ...baseAgent.adapterConfig, + promptTemplate: "You are {{agent.name}}. Do your work.", + bootstrapPromptTemplate: "Bootstrap prompt.", + extraArgs: ["--verbose"], + timeoutSec: 600, + graceSec: 30, + }, + }); + + const app = createApp(); + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "codex_local", + adapterConfig: { model: "gpt-5.3-codex" }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const [, patch] = mockAgentService.update.mock.calls[0]!; + const cfg = patch.adapterConfig as Record; + expect(cfg.promptTemplate).toBe("You are {{agent.name}}. Do your work."); + expect(cfg.bootstrapPromptTemplate).toBe("Bootstrap prompt."); + expect(cfg.extraArgs).toEqual(["--verbose"]); + expect(cfg.timeoutSec).toBe(600); + expect(cfg.graceSec).toBe(30); + }); + + it("applies dangerouslySkipPermissions default when switching to claude_local", async () => { + // Start from an opencode_local agent + mockAgentService.getById.mockResolvedValue({ + ...baseAgent, + adapterType: "opencode_local", + adapterConfig: { + cwd: "/workspace/app", + model: "anthropic/claude-sonnet-4-5", + instructionsFilePath: "/tmp/agent/instructions/AGENTS.md", + }, + }); + + const app = createApp(); + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "claude_local", + adapterConfig: { model: "claude-sonnet-4-6" }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const [, patch] = mockAgentService.update.mock.calls[0]!; + const cfg = patch.adapterConfig as Record; + expect(cfg.dangerouslySkipPermissions).toBe(true); + expect(cfg.cwd).toBe("/workspace/app"); + expect(cfg.instructionsFilePath).toBe("/tmp/agent/instructions/AGENTS.md"); + }); + + it("injects OPENCODE_PERMISSION env when switching to opencode_local", async () => { + const app = createApp(); + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "opencode_local", + adapterConfig: { model: "anthropic/claude-sonnet-4-5" }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const [, patch] = mockAgentService.update.mock.calls[0]!; + const cfg = patch.adapterConfig as Record; + const env = cfg.env as Record; + expect(env).toBeDefined(); + expect(env.OPENCODE_PERMISSION).toBeDefined(); + const perms = JSON.parse(env.OPENCODE_PERMISSION as string); + expect(perms.edit).toBe("allow"); + expect(perms.bash).toBe("allow"); + expect(perms.skill).toBe("allow"); + expect(perms.task).toBe("allow"); + }); + + it("does not override existing OPENCODE_PERMISSION env if already set", async () => { + const app = createApp(); + const customPerms = JSON.stringify({ edit: "ask", bash: "allow" }); + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "opencode_local", + adapterConfig: { + model: "anthropic/claude-sonnet-4-5", + env: { OPENCODE_PERMISSION: customPerms }, + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const [, patch] = mockAgentService.update.mock.calls[0]!; + const cfg = patch.adapterConfig as Record; + const env = cfg.env as Record; + expect(env.OPENCODE_PERMISSION).toBe(customPerms); + }); + + it("strips adapter-specific fields when only adapterType is patched without adapterConfig", async () => { + const app = createApp(); + + const res = await request(app) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "codex_local", + // No adapterConfig — only changing adapter type + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const [, patch] = mockAgentService.update.mock.calls[0]!; + const cfg = patch.adapterConfig as Record; + // Cross-adapter fields preserved + expect(cfg.cwd).toBe("/workspace/app"); + expect(cfg.maxTurnsPerRun).toBe(40); + expect(cfg.instructionsFilePath).toBe("/tmp/agent/instructions/AGENTS.md"); + // Adapter-specific fields from claude_local stripped + expect(cfg.chrome).toBeUndefined(); + // Model gets codex default from applyCreateDefaultsByAdapterType + expect(cfg.model).toBe("gpt-5.3-codex"); + }); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index af5a6574e2..5bad1f8f22 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -73,6 +73,22 @@ export function agentRoutes(db: Db) { }; const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS)); const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]); + const CROSS_ADAPTER_CONFIG_KEYS = new Set([ + "cwd", + "env", + "maxTurnsPerRun", + "instructionsBundleMode", + "instructionsRootPath", + "instructionsEntryFile", + "instructionsFilePath", + "paperclipSkillSync", + "workspaceRuntime", + "promptTemplate", + "bootstrapPromptTemplate", + "extraArgs", + "timeoutSec", + "graceSec", + ]); const router = Router(); const svc = agentService(db); @@ -352,11 +368,27 @@ export function agentRoutes(db: Db) { return { ...adapterConfig, devicePrivateKeyPem: generateEd25519PrivateKeyPem() }; } + const OPENCODE_PERMISSION_ALLOW_ALL = JSON.stringify({ + edit: "allow", + bash: "allow", + webfetch: "allow", + external_directory: "allow", + doom_loop: "allow", + skill: "allow", + task: "allow", + }); + function applyCreateDefaultsByAdapterType( adapterType: string | null | undefined, adapterConfig: Record, ): Record { const next = { ...adapterConfig }; + if (adapterType === "claude_local") { + if (typeof next.dangerouslySkipPermissions !== "boolean") { + next.dangerouslySkipPermissions = true; + } + return ensureGatewayDeviceKey(adapterType, next); + } if (adapterType === "codex_local") { if (!asNonEmptyString(next.model)) { next.model = DEFAULT_CODEX_LOCAL_MODEL; @@ -369,11 +401,19 @@ export function agentRoutes(db: Db) { } return ensureGatewayDeviceKey(adapterType, next); } + if (adapterType === "opencode_local") { + // Inject OPENCODE_PERMISSION env var so agents run fully autonomous + const env = (asRecord(next.env) ?? {}) as Record; + if (!asNonEmptyString(env.OPENCODE_PERMISSION)) { + env.OPENCODE_PERMISSION = OPENCODE_PERMISSION_ALLOW_ALL; + next.env = env; + } + return ensureGatewayDeviceKey(adapterType, next); + } if (adapterType === "gemini_local" && !asNonEmptyString(next.model)) { next.model = DEFAULT_GEMINI_LOCAL_MODEL; return ensureGatewayDeviceKey(adapterType, next); } - // OpenCode requires explicit model selection — no default if (adapterType === "cursor" && !asNonEmptyString(next.model)) { next.model = DEFAULT_CURSOR_LOCAL_MODEL; } @@ -417,6 +457,16 @@ export function agentRoutes(db: Db) { return path.resolve(cwd, trimmed); } + function preserveCrossAdapterConfig(adapterConfig: Record) { + const preserved: Record = {}; + for (const key of CROSS_ADAPTER_CONFIG_KEYS) { + if (Object.prototype.hasOwnProperty.call(adapterConfig, key)) { + preserved[key] = adapterConfig[key]; + } + } + return preserved; + } + async function materializeDefaultInstructionsBundleForNewAgent) }; + const requestedAdapterType = + typeof patchData.adapterType === "string" ? patchData.adapterType : existing.adapterType; + const adapterTypeChanged = requestedAdapterType !== existing.adapterType; if (Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")) { const adapterConfig = asRecord(patchData.adapterConfig); if (!adapterConfig) { @@ -1701,11 +1754,35 @@ export function agentRoutes(db: Db) { if (changingInstructionsPath) { await assertCanManageInstructionsPath(req, existing); } - patchData.adapterConfig = adapterConfig; + const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; + patchData.adapterConfig = adapterTypeChanged + ? { + ...preserveCrossAdapterConfig(existingAdapterConfig), + ...adapterConfig, + } + : { + ...existingAdapterConfig, + ...adapterConfig, + }; + } else if (adapterTypeChanged) { + // Adapter type changed but no adapterConfig supplied — strip adapter-specific + // fields from the existing config so stale values don't bleed into the new adapter. + const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; + patchData.adapterConfig = preserveCrossAdapterConfig(existingAdapterConfig); + } + if (Object.prototype.hasOwnProperty.call(patchData, "runtimeConfig")) { + const runtimeConfig = asRecord(patchData.runtimeConfig); + if (patchData.runtimeConfig !== undefined && !runtimeConfig) { + res.status(422).json({ error: "runtimeConfig must be an object" }); + return; + } + if (runtimeConfig) { + patchData.runtimeConfig = { + ...(asRecord(existing.runtimeConfig) ?? {}), + ...runtimeConfig, + }; + } } - - const requestedAdapterType = - typeof patchData.adapterType === "string" ? patchData.adapterType : existing.adapterType; const touchesAdapterConfiguration = Object.prototype.hasOwnProperty.call(patchData, "adapterType") || Object.prototype.hasOwnProperty.call(patchData, "adapterConfig"); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 0694efed98..ff2f5f7a06 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2683,11 +2683,19 @@ export function heartbeatService(db: Db) { } // Ensure the agent is not left stuck in "running" if the inner catch handler's // DB calls threw (e.g. a transient DB error in finalizeAgentStatus). - await finalizeAgentStatus(run.agentId, "failed").catch(() => undefined); + await finalizeAgentStatus(run.agentId, "failed").catch((cleanupErr) => { + logger.error({ err: cleanupErr, runId, agentId: run.agentId }, "finalizeAgentStatus failed in outer catch — agent may be stuck in running state"); + }); } finally { await releaseRuntimeServicesForRun(run.id).catch(() => undefined); activeRunExecutions.delete(run.id); - await startNextQueuedRunForAgent(run.agentId); + // Use setImmediate to break the call stack and prevent unbounded recursion + // when many runs are queued for the same agent. + setImmediate(() => { + startNextQueuedRunForAgent(run.agentId).catch((err) => { + logger.error({ err, agentId: run.agentId }, "failed to promote next queued run after completion"); + }); + }); } } diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 0b515dca4a..ac142cbc7f 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -102,6 +102,22 @@ const emptyOverlay: Overlay = { /** Stable empty object used as fallback for missing env config to avoid new-object-per-render. */ const EMPTY_ENV: Record = {}; +const CROSS_ADAPTER_CONFIG_KEYS = [ + "cwd", + "env", + "maxTurnsPerRun", + "instructionsBundleMode", + "instructionsRootPath", + "instructionsEntryFile", + "instructionsFilePath", + "paperclipSkillSync", + "workspaceRuntime", + "promptTemplate", + "bootstrapPromptTemplate", + "extraArgs", + "timeoutSec", + "graceSec", +] as const; function isOverlayDirty(o: Overlay): boolean { return ( @@ -345,6 +361,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) { if (isCreate) { return uiAdapter.buildAdapterConfig(val!); } + if (overlay.adapterType !== undefined) { + const preserved = Object.fromEntries( + CROSS_ADAPTER_CONFIG_KEYS.flatMap((key) => + Object.prototype.hasOwnProperty.call(config, key) ? [[key, config[key]]] : [], + ), + ); + return { ...preserved, ...overlay.adapterConfig }; + } const base = config as Record; return { ...base, ...overlay.adapterConfig }; } @@ -548,12 +572,18 @@ export function AgentConfigForm(props: AgentConfigFormProps) { } set!(nextValues); } else { + const preserved = Object.fromEntries( + CROSS_ADAPTER_CONFIG_KEYS.flatMap((key) => + Object.prototype.hasOwnProperty.call(config, key) ? [[key, config[key]]] : [], + ), + ); // Clear all adapter config and explicitly blank out model + effort/mode keys // so the old adapter's values don't bleed through via eff() setOverlay((prev) => ({ ...prev, adapterType: t, adapterConfig: { + ...preserved, model: t === "codex_local" ? DEFAULT_CODEX_LOCAL_MODEL @@ -566,12 +596,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) { modelReasoningEffort: "", variant: "", mode: "", - ...(t === "codex_local" - ? { - dangerouslyBypassApprovalsAndSandbox: - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - } - : {}), + ...(t === "claude_local" + ? { dangerouslySkipPermissions: true } + : t === "codex_local" + ? { + dangerouslyBypassApprovalsAndSandbox: + DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, + } + : {}), }, })); }