diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index ce81db8c486..e328bcef00e 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -582,10 +582,13 @@ 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" }, + { 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 e712664d30a..9c9a90f7a6f 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -627,10 +627,13 @@ 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" }, + { 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/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 2735b69a187..ba8b1b848a8 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -356,6 +356,25 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("derives auto permission mode from auto runtime policy without skip flag", () => { + 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("loads Claude filesystem settings sources for SDK sessions", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 757d7a00eb2..285d9dac608 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -3509,6 +3509,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 119fa36303a..d7346a0e0db 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -104,6 +104,7 @@ describe("buildTurnStartParams", () => { NodeAssert.deepStrictEqual(params, { threadId: "provider-thread-1", approvalPolicy: "never", + approvalsReviewer: "user", sandboxPolicy: { type: "dangerFullAccess", }, @@ -149,6 +150,7 @@ describe("buildTurnStartParams", () => { NodeAssert.deepStrictEqual(params, { threadId: "provider-thread-1", approvalPolicy: "on-request", + approvalsReviewer: "user", sandboxPolicy: { type: "workspaceWrite", }, @@ -193,6 +195,31 @@ describe("buildTurnStartParams", () => { NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`)); }); + it.effect("routes approvals to the auto reviewer in auto mode", () => + Effect.gen(function* () { + const params = yield* buildTurnStartParams({ + threadId: "provider-thread-1", + runtimeMode: "auto", + prompt: "Ship it", + }); + + NodeAssert.deepStrictEqual(params, { + threadId: "provider-thread-1", + approvalPolicy: "on-request", + approvalsReviewer: "auto_review", + sandboxPolicy: { + type: "workspaceWrite", + }, + input: [ + { + type: "text", + text: "Ship it", + }, + ], + }); + }), + ); + it("omits collaboration mode when interaction mode is absent", () => { const params = Effect.runSync( buildTurnStartParams({ @@ -205,6 +232,7 @@ describe("buildTurnStartParams", () => { NodeAssert.deepStrictEqual(params, { threadId: "provider-thread-1", approvalPolicy: "untrusted", + approvalsReviewer: "user", sandboxPolicy: { type: "readOnly", }, diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 5a81e915e34..67108dd4dbb 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -264,23 +264,35 @@ function readResumeCursorThreadId( function runtimeModeToThreadConfig(input: RuntimeMode): { readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval; readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode; + // Always explicit: omitting the field on resume keeps the thread's previous + // reviewer, which would leave auto_review sticky after switching modes. + readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer; } { switch (input) { case "approval-required": return { approvalPolicy: "untrusted", sandbox: "read-only", + approvalsReviewer: "user", }; case "auto-accept-edits": return { approvalPolicy: "on-request", sandbox: "workspace-write", + approvalsReviewer: "user", + }; + case "auto": + return { + approvalPolicy: "on-request", + sandbox: "workspace-write", + approvalsReviewer: "auto_review", }; case "full-access": default: return { approvalPolicy: "never", sandbox: "danger-full-access", + approvalsReviewer: "user", }; } } @@ -296,6 +308,7 @@ function buildThreadStartParams(input: { cwd: input.cwd, approvalPolicy: config.approvalPolicy, sandbox: config.sandbox, + approvalsReviewer: config.approvalsReviewer, ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), }; @@ -310,6 +323,7 @@ function runtimeModeToTurnSandboxPolicy( type: "readOnly", }; case "auto-accept-edits": + case "auto": return { type: "workspaceWrite", }; @@ -382,6 +396,7 @@ export function buildTurnStartParams(input: { threadId: input.threadId, input: turnInput, approvalPolicy: config.approvalPolicy, + approvalsReviewer: config.approvalsReviewer, sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode), ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 94e4af3bba6..ac137c8b075 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -107,6 +107,7 @@ import { LockIcon, LockOpenIcon, PenLineIcon, + SparklesIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -148,6 +149,11 @@ const runtimeModeConfig: Record< description: "Auto-approve edits, ask before other actions.", icon: PenLineIcon, }, + auto: { + label: "Auto", + description: "An AI reviewer approves routine actions; risky ones still ask.", + 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 f1fbd193a63..b808f562920 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -71,6 +71,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls > Supervised Auto-accept edits + Auto Full access {props.activePlan ? ( diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index b01df310062..e1ddbdbd0cc 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;