From 61f0d30bb7d69bf39d9a016518852b729b04c134 Mon Sep 17 00:00:00 2001 From: Ecko95 <8972676+Ecko95@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:08:53 +0200 Subject: [PATCH 1/3] feat(provider): add Claude SDK "auto" permission mode as 4th runtime mode Expose the Claude Agent SDK's native permissionMode "auto" as a fourth RuntimeMode. The SDK's model classifier auto-approves safe tool calls while still gating risky ones; allowDangerouslySkipPermissions is never set. Includes a security guard so "auto" selected on a Codex thread maps to the conservative on-request/workspace-write policy instead of falling through to danger-full-access, and a mobile label fix so an auto thread isn't shown as "Full access". Server-side actor authorization is unchanged and remains mode-independent. Closes #135 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../features/threads/NewTaskDraftScreen.tsx | 4 +- .../src/features/threads/ThreadComposer.tsx | 4 +- .../src/provider/Layers/ClaudeAdapter.test.ts | 20 +++++++++ .../src/provider/Layers/ClaudeAdapter.ts | 1 + .../Layers/CodexSessionRuntime.test.ts | 45 +++++++++++++++++++ .../provider/Layers/CodexSessionRuntime.ts | 2 + apps/web/src/components/chat/ChatComposer.tsx | 6 +++ .../chat/CompactComposerControlsMenu.tsx | 1 + packages/contracts/src/orchestration.ts | 1 + 9 files changed, 82 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index b73e874169c..ed1456fc8c1 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -169,7 +169,9 @@ export function NewTaskDraftScreen(props: { ? "Approve actions" : flow.runtimeMode === "auto-accept-edits" ? "Auto-accept edits" - : "Full access", + : flow.runtimeMode === "auto" + ? "Auto" + : "Full access", subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index e13115d205f..2c92a7c145b 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -499,7 +499,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ? "Approve actions" : currentRuntimeMode === "auto-accept-edits" ? "Auto-accept edits" - : "Full access", + : currentRuntimeMode === "auto" + ? "Auto" + : "Full access", subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 672645c3d5b..5e77f02ed98 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -367,6 +367,25 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("uses native auto permissions for auto claude sessions", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + runtimeMode: "auto", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.permissionMode, "auto"); + assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("forwards claude effort levels into query options", () => { const harness = makeHarness(); return Effect.gen(function* () { @@ -3461,6 +3480,7 @@ describe("ClaudeAdapterLive", () => { { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, { runtimeMode: "approval-required", expectedBase: "default" }, { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" }, + { runtimeMode: "auto", expectedBase: "auto" }, ])( "restores $expectedBase permission mode after plan turn ($runtimeMode)", ({ runtimeMode, expectedBase }) => { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 74bb11b833c..e620cde1833 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -2980,6 +2980,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model); const runtimeModeToPermission: Record = { "auto-accept-edits": "acceptEdits", + auto: "auto", "full-access": "bypassPermissions", }; const permissionMode = runtimeModeToPermission[input.runtimeMode]; diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 93d0ecc8d2b..98a8dec8049 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -176,6 +176,51 @@ describe("buildTurnStartParams", () => { }); }); + it("maps runtimeMode 'auto' to the same conservative policy as auto-accept-edits", () => { + const params = Effect.runSync( + buildTurnStartParams({ + threadId: "provider-thread-1", + runtimeMode: "auto", + prompt: "Implement it", + model: "gpt-5.3-codex", + interactionMode: "default", + attachments: [ + { + type: "image", + url: "data:image/png;base64,abc", + }, + ], + }), + ); + + assert.deepStrictEqual(params, { + threadId: "provider-thread-1", + approvalPolicy: "on-request", + sandboxPolicy: { + type: "workspaceWrite", + }, + input: [ + { + type: "text", + text: "Implement it", + }, + { + type: "image", + url: "data:image/png;base64,abc", + }, + ], + model: "gpt-5.3-codex", + collaborationMode: { + mode: "default", + settings: { + model: "gpt-5.3-codex", + reasoning_effort: "medium", + developer_instructions: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, + }, + }, + }); + }); + it("omits collaboration mode when interaction mode is absent", () => { const params = Effect.runSync( buildTurnStartParams({ diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index d6b62f76074..64ca83a0120 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -320,6 +320,7 @@ function runtimeModeToThreadConfig(input: RuntimeMode): { approvalPolicy: "untrusted", sandbox: "read-only", }; + case "auto": case "auto-accept-edits": return { approvalPolicy: "on-request", @@ -376,6 +377,7 @@ function runtimeModeToTurnSandboxPolicy( return { type: "readOnly", }; + case "auto": case "auto-accept-edits": return { type: "workspaceWrite", diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 711272ffe84..04288dc739c 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -92,6 +92,7 @@ import { LockIcon, LockOpenIcon, PenLineIcon, + SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -132,6 +133,11 @@ const runtimeModeConfig: Record< description: "Auto-approve edits, ask before other actions.", icon: PenLineIcon, }, + auto: { + label: "Auto", + description: "Auto-approves safe actions; asks before risky ones.", + icon: SparklesIcon, + }, "full-access": { label: "Full access", description: "Allow commands and edits without prompts.", diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index fb43335e3ae..ebae48c2740 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -74,6 +74,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls > Supervised Auto-accept edits + Auto Full access {props.showPlanToggle ? ( diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index e910173f842..f1152117e07 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -127,6 +127,7 @@ export type ModelSelection = typeof ModelSelection.Type; export const RuntimeMode = Schema.Literals([ "approval-required", "auto-accept-edits", + "auto", "full-access", ]); export type RuntimeMode = typeof RuntimeMode.Type; From 5c0b15fb8e98d6cdef88e61ddc56110a7e71cc7d Mon Sep 17 00:00:00 2001 From: Codex Peer Date: Fri, 10 Jul 2026 01:39:29 +0200 Subject: [PATCH 2/3] Apply Codex peer cfc98f68 changes --- apps/mobile/src/features/threads/NewTaskDraftScreen.tsx | 1 + apps/mobile/src/features/threads/ThreadComposer.tsx | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index ed1456fc8c1..975309f4d2a 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -175,6 +175,7 @@ export function NewTaskDraftScreen(props: { subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, + { id: "options:runtime:auto", title: "Auto" }, { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 2c92a7c145b..f91c32cb5b7 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -30,6 +30,7 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; +import { ErrorBanner } from "../../components/ErrorBanner"; import { ControlPill, ControlPillMenu } from "../../components/ControlPill"; import { ProviderIcon } from "../../components/ProviderIcon"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; @@ -505,6 +506,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, + { id: "options:runtime:auto", title: "Auto" }, { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -808,6 +810,12 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer automatically. ) : null} + {/* ponytail: no reliable client-side model capability registry exists for auto-mode support */} + {props.selectedThread.session?.lastError ? ( + + + + ) : null} Date: Fri, 10 Jul 2026 13:22:09 +0200 Subject: [PATCH 3/3] test(provider): pin Codex thread-start policy for auto runtime mode Add a buildThreadStartParams assertion that runtimeMode "auto" maps to the conservative on-request/workspace-write policy (not the danger default), closing the thread-start coverage gap alongside the existing turn-start test. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../provider/Layers/CodexSessionRuntime.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 98a8dec8049..214c84204b5 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -687,4 +687,20 @@ describe("buildThreadStartParams visual-plan MCP", () => { }); assert.equal(params.config, undefined); }); + + it("maps runtimeMode 'auto' to the same conservative policy as auto-accept-edits", () => { + const params = buildThreadStartParams({ + cwd: "/tmp/project", + runtimeMode: "auto", + model: undefined, + serviceTier: undefined, + visualPlanMcp: undefined, + }); + + assert.deepStrictEqual(params, { + cwd: "/tmp/project", + approvalPolicy: "on-request", + sandbox: "workspace-write", + }); + }); });