From 7bd97cb4296c86f68d1515637d75b8f83e3811c0 Mon Sep 17 00:00:00 2001 From: m4tta <427218+m4tta@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:06:02 -0700 Subject: [PATCH 1/2] Add Claude Code auto permission mode as a 4th runtime mode Claude Code's TypeScript Agent SDK exposes an "auto" PermissionMode (default | acceptEdits | bypassPermissions | plan | dontAsk | auto) that runs tool calls through a background safety classifier instead of skipping permission checks entirely. It's distinct from bypassPermissions and never requires allowDangerouslySkipPermissions, so it works in environments where an org policy blocks --dangerously-skip-permissions but still allows auto mode. - Add "auto" to the RuntimeMode contract and map it to the SDK's permissionMode in ClaudeAdapter. - Add a supportsAutoRuntimeMode provider capability flag so only Claude advertises the option; Codex falls back to its workspace-write policy defensively instead of full bypass. - Surface the option in the web composer menus and mobile runtime option lists, gated on that capability flag. --- .../features/threads/NewTaskDraftScreen.tsx | 14 +++++++++-- .../src/features/threads/ThreadComposer.tsx | 14 +++++++++-- .../src/provider/Layers/ClaudeAdapter.ts | 1 + .../src/provider/Layers/ClaudeProvider.ts | 1 + .../provider/Layers/CodexSessionRuntime.ts | 2 ++ apps/server/src/provider/providerSnapshot.ts | 4 +++ apps/web/src/components/chat/ChatComposer.tsx | 25 +++++++++++++++++-- .../chat/CompactComposerControlsMenu.tsx | 16 +++++++++--- apps/web/src/providerModels.ts | 7 ++++++ packages/contracts/src/orchestration.ts | 1 + packages/contracts/src/server.ts | 1 + 11 files changed, 77 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index ce24198f5e22..40031b1000a6 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -198,10 +198,15 @@ 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" }, + ...(flow.selectedModelOption?.providerDriver === "claudeAgent" + ? [{ id: "options:runtime:auto", title: "Auto" }] + : []), { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -229,7 +234,12 @@ export function NewTaskDraftScreen(props: { }), }, ], - [flow.interactionMode, flow.runtimeMode, providerOptionDescriptors], + [ + flow.interactionMode, + flow.runtimeMode, + flow.selectedModelOption?.providerDriver, + providerOptionDescriptors, + ], ); const workspaceMenuActions = useMemo(() => { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 75991cae885a..e956d82a2340 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -552,10 +552,15 @@ 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" }, + ...(currentModelOption?.providerDriver === "claudeAgent" + ? [{ id: "options:runtime:auto", title: "Auto" }] + : []), { id: "options:runtime:full-access", title: "Full access" }, ].map((option) => { const value = option.id.replace("options:runtime:", ""); @@ -583,7 +588,12 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer }), }, ], - [currentInteractionMode, currentRuntimeMode, providerOptionDescriptors], + [ + currentInteractionMode, + currentModelOption?.providerDriver, + currentRuntimeMode, + providerOptionDescriptors, + ], ); // ── Menu handlers ──────────────────────────────────────── diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 97a93f85829c..10e741d06728 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -3431,6 +3431,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/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 59b7d0464de8..edd628e5600e 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -47,6 +47,7 @@ const PROVIDER = ProviderDriverKind.make("claudeAgent"); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, + supportsAutoRuntimeMode: true, } as const; const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 99ac498f0c36..8ca5a9ebe897 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -273,6 +273,7 @@ function runtimeModeToThreadConfig(input: RuntimeMode): { sandbox: "read-only", }; case "auto-accept-edits": + case "auto": return { approvalPolicy: "on-request", sandbox: "workspace-write", @@ -311,6 +312,7 @@ function runtimeModeToTurnSandboxPolicy( type: "readOnly", }; case "auto-accept-edits": + case "auto": return { type: "workspaceWrite", }; diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index dfe31ffdc442..2f8a900a4ef7 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -57,6 +57,7 @@ export interface ServerProviderPresentation { readonly badgeLabel?: string; readonly showInteractionModeToggle?: boolean; readonly requiresNewThreadForModelChange?: boolean; + readonly supportsAutoRuntimeMode?: boolean; } export type ServerProviderDraft = Omit; @@ -234,6 +235,9 @@ export function buildServerProvider(input: { ...(typeof input.presentation.requiresNewThreadForModelChange === "boolean" ? { requiresNewThreadForModelChange: input.presentation.requiresNewThreadForModelChange } : {}), + ...(typeof input.presentation.supportsAutoRuntimeMode === "boolean" + ? { supportsAutoRuntimeMode: input.presentation.supportsAutoRuntimeMode } + : {}), enabled: input.enabled, installed: input.probe.installed, version: input.probe.version, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 5f9aec837ca1..f88fddd2b766 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -101,10 +101,15 @@ import { LockIcon, LockOpenIcon, PenLineIcon, + SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; -import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels"; +import { + getProviderAutoRuntimeModeSupport, + getProviderDisplayName, + getProviderInteractionModeToggle, +} from "../../providerModels"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, @@ -142,6 +147,11 @@ const runtimeModeConfig: Record< description: "Auto-approve edits, ask before other actions.", icon: PenLineIcon, }, + auto: { + label: "Auto", + description: "Run without prompts; a safety classifier reviews actions in the background.", + icon: SparklesIcon, + }, "full-access": { label: "Full access", description: "Allow commands and edits without prompts.", @@ -194,6 +204,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop showInteractionModeToggle: boolean; interactionMode: ProviderInteractionMode; runtimeMode: RuntimeMode; + runtimeModeOptions: ReadonlyArray; showPlanToggle: boolean; planSidebarLabel: string; planSidebarOpen: boolean; @@ -269,7 +280,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop {runtimeModeOption.label} - {runtimeModeOptions.map((mode) => { + {props.runtimeModeOptions.map((mode) => { const option = runtimeModeConfig[mode]; const OptionIcon = option.icon; return ( @@ -817,9 +828,17 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) providerStatuses, selectedProvider, ), + supportsAutoRuntimeMode: getProviderAutoRuntimeModeSupport(providerStatuses, selectedProvider), }), [providerStatuses, selectedProvider], ); + const runtimeModeOptionsForProvider = useMemo( + () => + composerProviderControls.supportsAutoRuntimeMode + ? runtimeModeOptions + : runtimeModeOptions.filter((mode) => mode !== "auto"), + [composerProviderControls.supportsAutoRuntimeMode], + ); const selectedModelSelection = useMemo( () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch), [selectedInstanceId, selectedModel, selectedModelOptionsForDispatch], @@ -2500,6 +2519,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) planSidebarLabel={planSidebarLabel} planSidebarOpen={planSidebarOpen} runtimeMode={runtimeMode} + runtimeModeOptions={runtimeModeOptionsForProvider} showInteractionModeToggle={composerProviderControls.showInteractionModeToggle} traitsMenuContent={providerTraitsMenuContent} onToggleInteractionMode={toggleInteractionMode} @@ -2518,6 +2538,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) showInteractionModeToggle={composerProviderControls.showInteractionModeToggle} interactionMode={interactionMode} runtimeMode={runtimeMode} + runtimeModeOptions={runtimeModeOptionsForProvider} showPlanToggle={showPlanSidebarToggle} planSidebarLabel={planSidebarLabel} planSidebarOpen={planSidebarOpen} diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index f1fbd193a63c..fb7cf6fb3312 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -12,12 +12,20 @@ import { MenuTrigger, } from "../ui/menu"; +const RUNTIME_MODE_LABELS: Record = { + "approval-required": "Supervised", + "auto-accept-edits": "Auto-accept edits", + auto: "Auto", + "full-access": "Full access", +}; + export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: { activePlan: boolean; interactionMode: ProviderInteractionMode; planSidebarLabel: string; planSidebarOpen: boolean; runtimeMode: RuntimeMode; + runtimeModeOptions: ReadonlyArray; showInteractionModeToggle: boolean; traitsMenuContent?: ReactNode; onToggleInteractionMode: () => void; @@ -69,9 +77,11 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls props.onRuntimeModeChange(value as RuntimeMode); }} > - Supervised - Auto-accept edits - Full access + {props.runtimeModeOptions.map((mode) => ( + + {RUNTIME_MODE_LABELS[mode]} + + ))} {props.activePlan ? ( <> diff --git a/apps/web/src/providerModels.ts b/apps/web/src/providerModels.ts index aa9afcf31e12..c39e4fa311b1 100644 --- a/apps/web/src/providerModels.ts +++ b/apps/web/src/providerModels.ts @@ -53,6 +53,13 @@ export function getProviderInteractionModeToggle( return getProviderSnapshot(providers, provider)?.showInteractionModeToggle ?? true; } +export function getProviderAutoRuntimeModeSupport( + providers: ReadonlyArray, + provider: ProviderDriverKind, +): boolean { + return getProviderSnapshot(providers, provider)?.supportsAutoRuntimeMode ?? false; +} + export function isProviderEnabled( providers: ReadonlyArray, provider: ProviderDriverKind, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 623fed0917bf..16e4e53a7572 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -117,6 +117,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; diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index b76ea965afe7..d681570a935a 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -166,6 +166,7 @@ export const ServerProvider = Schema.Struct({ continuation: Schema.optional(ServerProviderContinuation), showInteractionModeToggle: Schema.optional(Schema.Boolean), requiresNewThreadForModelChange: Schema.optional(Schema.Boolean), + supportsAutoRuntimeMode: Schema.optional(Schema.Boolean), enabled: Schema.Boolean, installed: Schema.Boolean, version: Schema.NullOr(TrimmedNonEmptyString), From 45eddfd23d7ffa1ecf691a4e85dbd226268a7966 Mon Sep 17 00:00:00 2001 From: m4tta <427218+m4tta@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:20:06 -0700 Subject: [PATCH 2/2] Gate mobile auto runtime mode on the capability flag, not driver kind Mobile checked providerDriver === "claudeAgent" directly, while the web composer checks the supportsAutoRuntimeMode flag reported by the provider snapshot. Those happen to agree today since Claude always sets the flag to true, but they could silently diverge if the flag is ever made conditional. Add supportsAutoRuntimeMode to ModelOption and gate on that instead, matching the web behavior. --- apps/mobile/src/features/threads/NewTaskDraftScreen.tsx | 4 ++-- apps/mobile/src/features/threads/ThreadComposer.tsx | 4 ++-- apps/mobile/src/lib/modelOptions.ts | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 40031b1000a6..dd146948e50a 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -204,7 +204,7 @@ export function NewTaskDraftScreen(props: { subactions: [ { id: "options:runtime:approval-required", title: "Approve actions" }, { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" }, - ...(flow.selectedModelOption?.providerDriver === "claudeAgent" + ...(flow.selectedModelOption?.supportsAutoRuntimeMode ? [{ id: "options:runtime:auto", title: "Auto" }] : []), { id: "options:runtime:full-access", title: "Full access" }, @@ -237,7 +237,7 @@ export function NewTaskDraftScreen(props: { [ flow.interactionMode, flow.runtimeMode, - flow.selectedModelOption?.providerDriver, + flow.selectedModelOption?.supportsAutoRuntimeMode, providerOptionDescriptors, ], ); diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index e956d82a2340..fc29a6d7e69e 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -558,7 +558,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" }, - ...(currentModelOption?.providerDriver === "claudeAgent" + ...(currentModelOption?.supportsAutoRuntimeMode ? [{ id: "options:runtime:auto", title: "Auto" }] : []), { id: "options:runtime:full-access", title: "Full access" }, @@ -590,7 +590,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ], [ currentInteractionMode, - currentModelOption?.providerDriver, + currentModelOption?.supportsAutoRuntimeMode, currentRuntimeMode, providerOptionDescriptors, ], diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index e21682414d7d..f0e44f6bdf98 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -15,6 +15,7 @@ export type ModelOption = { readonly providerKey: string; readonly providerLabel: string; readonly providerDriver: string; + readonly supportsAutoRuntimeMode: boolean; readonly capabilities: ModelCapabilities | null; readonly selection: ModelSelection; }; @@ -78,6 +79,7 @@ export function buildModelOptions( providerKey: provider.instanceId, providerLabel, providerDriver: provider.driver, + supportsAutoRuntimeMode: provider.supportsAutoRuntimeMode ?? false, capabilities: model.capabilities, selection: normalizeSelectionOptions( { @@ -107,6 +109,7 @@ export function buildModelOptions( providerKey: fallbackModelSelection.instanceId, providerLabel, providerDriver: fallbackModelSelection.instanceId, + supportsAutoRuntimeMode: false, capabilities: null, selection: fallbackModelSelection, });