authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-23 02:06:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 02:06:24-07:00
logfbd77420f27c1322d49726dd927861b8b5903494
treece1574cdb761d8f722efd97df2a814e975591592
parent16491a84b9bb2842ad6db41635d087647fc2df5b
signature Signed by PGP key B5690EEEBB952194

feat: add "Auto" runtime mode — AI-reviewed approvals for Codex and Claude (#4272)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

9 files changed, 79 insertions(+), 2 deletions(-)

apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+4-1
...@@ -582,10 +582,13 @@ export function NewTaskDraftScreen(props: {...@@ -582,10 +582,13 @@ export function NewTaskDraftScreen(props: {
582 ? "Approve actions"582 ? "Approve actions"
583 : flow.runtimeMode === "auto-accept-edits"583 : flow.runtimeMode === "auto-accept-edits"
584 ? "Auto-accept edits"584 ? "Auto-accept edits"
585 : "Full access",585 : flow.runtimeMode === "auto"
586 ? "Auto"
587 : "Full access",
586 subactions: [588 subactions: [
587 { id: "options:runtime:approval-required", title: "Approve actions" },589 { id: "options:runtime:approval-required", title: "Approve actions" },
588 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },590 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },
591 { id: "options:runtime:auto", title: "Auto" },
589 { id: "options:runtime:full-access", title: "Full access" },592 { id: "options:runtime:full-access", title: "Full access" },
590 ].map((option) => {593 ].map((option) => {
591 const value = option.id.replace("options:runtime:", "");594 const value = option.id.replace("options:runtime:", "");
apps/mobile/src/features/threads/ThreadComposer.tsx+4-1
...@@ -638,10 +638,13 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -638,10 +638,13 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
638 ? "Approve actions"638 ? "Approve actions"
639 : currentRuntimeMode === "auto-accept-edits"639 : currentRuntimeMode === "auto-accept-edits"
640 ? "Auto-accept edits"640 ? "Auto-accept edits"
641 : "Full access",641 : currentRuntimeMode === "auto"
642 ? "Auto"
643 : "Full access",
642 subactions: [644 subactions: [
643 { id: "options:runtime:approval-required", title: "Approve actions" },645 { id: "options:runtime:approval-required", title: "Approve actions" },
644 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },646 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },
647 { id: "options:runtime:auto", title: "Auto" },
645 { id: "options:runtime:full-access", title: "Full access" },648 { id: "options:runtime:full-access", title: "Full access" },
646 ].map((option) => {649 ].map((option) => {
647 const value = option.id.replace("options:runtime:", "");650 const value = option.id.replace("options:runtime:", "");
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+19
...@@ -356,6 +356,25 @@ describe("ClaudeAdapterLive", () => {...@@ -356,6 +356,25 @@ describe("ClaudeAdapterLive", () => {
356 );356 );
357 });357 });
358358
359 it.effect("derives auto permission mode from auto runtime policy without skip flag", () => {
360 const harness = makeHarness();
361 return Effect.gen(function* () {
362 const adapter = yield* ClaudeAdapter;
363 yield* adapter.startSession({
364 threadId: THREAD_ID,
365 provider: ProviderDriverKind.make("claudeAgent"),
366 runtimeMode: "auto",
367 });
368
369 const createInput = harness.getLastCreateQueryInput();
370 assert.equal(createInput?.options.permissionMode, "auto");
371 assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined);
372 }).pipe(
373 Effect.provideService(Random.Random, makeDeterministicRandomService()),
374 Effect.provide(harness.layer),
375 );
376 });
377
359 it.effect("loads Claude filesystem settings sources for SDK sessions", () => {378 it.effect("loads Claude filesystem settings sources for SDK sessions", () => {
360 const harness = makeHarness();379 const harness = makeHarness();
361 return Effect.gen(function* () {380 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeAdapter.ts+1
...@@ -3509,6 +3509,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3509,6 +3509,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3509 const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);3509 const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);
3510 const runtimeModeToPermission: Record<string, PermissionMode> = {3510 const runtimeModeToPermission: Record<string, PermissionMode> = {
3511 "auto-accept-edits": "acceptEdits",3511 "auto-accept-edits": "acceptEdits",
3512 auto: "auto",
3512 "full-access": "bypassPermissions",3513 "full-access": "bypassPermissions",
3513 };3514 };
3514 const permissionMode = runtimeModeToPermission[input.runtimeMode];3515 const permissionMode = runtimeModeToPermission[input.runtimeMode];
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+28
...@@ -104,6 +104,7 @@ describe("buildTurnStartParams", () => {...@@ -104,6 +104,7 @@ describe("buildTurnStartParams", () => {
104 NodeAssert.deepStrictEqual(params, {104 NodeAssert.deepStrictEqual(params, {
105 threadId: "provider-thread-1",105 threadId: "provider-thread-1",
106 approvalPolicy: "never",106 approvalPolicy: "never",
107 approvalsReviewer: "user",
107 sandboxPolicy: {108 sandboxPolicy: {
108 type: "dangerFullAccess",109 type: "dangerFullAccess",
109 },110 },
...@@ -149,6 +150,7 @@ describe("buildTurnStartParams", () => {...@@ -149,6 +150,7 @@ describe("buildTurnStartParams", () => {
149 NodeAssert.deepStrictEqual(params, {150 NodeAssert.deepStrictEqual(params, {
150 threadId: "provider-thread-1",151 threadId: "provider-thread-1",
151 approvalPolicy: "on-request",152 approvalPolicy: "on-request",
153 approvalsReviewer: "user",
152 sandboxPolicy: {154 sandboxPolicy: {
153 type: "workspaceWrite",155 type: "workspaceWrite",
154 },156 },
...@@ -193,6 +195,31 @@ describe("buildTurnStartParams", () => {...@@ -193,6 +195,31 @@ describe("buildTurnStartParams", () => {
193 NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));195 NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));
194 });196 });
195197
198 it.effect("routes approvals to the auto reviewer in auto mode", () =>
199 Effect.gen(function* () {
200 const params = yield* buildTurnStartParams({
201 threadId: "provider-thread-1",
202 runtimeMode: "auto",
203 prompt: "Ship it",
204 });
205
206 NodeAssert.deepStrictEqual(params, {
207 threadId: "provider-thread-1",
208 approvalPolicy: "on-request",
209 approvalsReviewer: "auto_review",
210 sandboxPolicy: {
211 type: "workspaceWrite",
212 },
213 input: [
214 {
215 type: "text",
216 text: "Ship it",
217 },
218 ],
219 });
220 }),
221 );
222
196 it("omits collaboration mode when interaction mode is absent", () => {223 it("omits collaboration mode when interaction mode is absent", () => {
197 const params = Effect.runSync(224 const params = Effect.runSync(
198 buildTurnStartParams({225 buildTurnStartParams({
...@@ -205,6 +232,7 @@ describe("buildTurnStartParams", () => {...@@ -205,6 +232,7 @@ describe("buildTurnStartParams", () => {
205 NodeAssert.deepStrictEqual(params, {232 NodeAssert.deepStrictEqual(params, {
206 threadId: "provider-thread-1",233 threadId: "provider-thread-1",
207 approvalPolicy: "untrusted",234 approvalPolicy: "untrusted",
235 approvalsReviewer: "user",
208 sandboxPolicy: {236 sandboxPolicy: {
209 type: "readOnly",237 type: "readOnly",
210 },238 },
apps/server/src/provider/Layers/CodexSessionRuntime.ts+15
...@@ -264,23 +264,35 @@ function readResumeCursorThreadId(...@@ -264,23 +264,35 @@ function readResumeCursorThreadId(
264function runtimeModeToThreadConfig(input: RuntimeMode): {264function runtimeModeToThreadConfig(input: RuntimeMode): {
265 readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval;265 readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval;
266 readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode;266 readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode;
267 // Always explicit: omitting the field on resume keeps the thread's previous
268 // reviewer, which would leave auto_review sticky after switching modes.
269 readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer;
267} {270} {
268 switch (input) {271 switch (input) {
269 case "approval-required":272 case "approval-required":
270 return {273 return {
271 approvalPolicy: "untrusted",274 approvalPolicy: "untrusted",
272 sandbox: "read-only",275 sandbox: "read-only",
276 approvalsReviewer: "user",
273 };277 };
274 case "auto-accept-edits":278 case "auto-accept-edits":
275 return {279 return {
276 approvalPolicy: "on-request",280 approvalPolicy: "on-request",
277 sandbox: "workspace-write",281 sandbox: "workspace-write",
282 approvalsReviewer: "user",
283 };
284 case "auto":
285 return {
286 approvalPolicy: "on-request",
287 sandbox: "workspace-write",
288 approvalsReviewer: "auto_review",
278 };289 };
279 case "full-access":290 case "full-access":
280 default:291 default:
281 return {292 return {
282 approvalPolicy: "never",293 approvalPolicy: "never",
283 sandbox: "danger-full-access",294 sandbox: "danger-full-access",
295 approvalsReviewer: "user",
284 };296 };
285 }297 }
286}298}
...@@ -296,6 +308,7 @@ function buildThreadStartParams(input: {...@@ -296,6 +308,7 @@ function buildThreadStartParams(input: {
296 cwd: input.cwd,308 cwd: input.cwd,
297 approvalPolicy: config.approvalPolicy,309 approvalPolicy: config.approvalPolicy,
298 sandbox: config.sandbox,310 sandbox: config.sandbox,
311 approvalsReviewer: config.approvalsReviewer,
299 ...(input.model ? { model: input.model } : {}),312 ...(input.model ? { model: input.model } : {}),
300 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),313 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
301 };314 };
...@@ -310,6 +323,7 @@ function runtimeModeToTurnSandboxPolicy(...@@ -310,6 +323,7 @@ function runtimeModeToTurnSandboxPolicy(
310 type: "readOnly",323 type: "readOnly",
311 };324 };
312 case "auto-accept-edits":325 case "auto-accept-edits":
326 case "auto":
313 return {327 return {
314 type: "workspaceWrite",328 type: "workspaceWrite",
315 };329 };
...@@ -382,6 +396,7 @@ export function buildTurnStartParams(input: {...@@ -382,6 +396,7 @@ export function buildTurnStartParams(input: {
382 threadId: input.threadId,396 threadId: input.threadId,
383 input: turnInput,397 input: turnInput,
384 approvalPolicy: config.approvalPolicy,398 approvalPolicy: config.approvalPolicy,
399 approvalsReviewer: config.approvalsReviewer,
385 sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode),400 sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode),
386 ...(input.model ? { model: input.model } : {}),401 ...(input.model ? { model: input.model } : {}),
387 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),402 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
apps/web/src/components/chat/ChatComposer.tsx+6
...@@ -106,6 +106,7 @@ import {...@@ -106,6 +106,7 @@ import {
106 LockIcon,106 LockIcon,
107 LockOpenIcon,107 LockOpenIcon,
108 PenLineIcon,108 PenLineIcon,
109 SparklesIcon,
109 XIcon,110 XIcon,
110} from "lucide-react";111} from "lucide-react";
111import { proposedPlanTitle } from "../../proposedPlan";112import { proposedPlanTitle } from "../../proposedPlan";
...@@ -149,6 +150,11 @@ const runtimeModeConfig: Record<...@@ -149,6 +150,11 @@ const runtimeModeConfig: Record<
149 description: "Auto-approve edits, ask before other actions.",150 description: "Auto-approve edits, ask before other actions.",
150 icon: PenLineIcon,151 icon: PenLineIcon,
151 },152 },
153 auto: {
154 label: "Auto",
155 description: "An AI reviewer approves routine actions; risky ones still ask.",
156 icon: SparklesIcon,
157 },
152 "full-access": {158 "full-access": {
153 label: "Full access",159 label: "Full access",
154 description: "Allow commands and edits without prompts.",160 description: "Allow commands and edits without prompts.",
apps/web/src/components/chat/CompactComposerControlsMenu.tsx+1
...@@ -71,6 +71,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls...@@ -71,6 +71,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
71 >71 >
72 <MenuRadioItem value="approval-required">Supervised</MenuRadioItem>72 <MenuRadioItem value="approval-required">Supervised</MenuRadioItem>
73 <MenuRadioItem value="auto-accept-edits">Auto-accept edits</MenuRadioItem>73 <MenuRadioItem value="auto-accept-edits">Auto-accept edits</MenuRadioItem>
74 <MenuRadioItem value="auto">Auto</MenuRadioItem>
74 <MenuRadioItem value="full-access">Full access</MenuRadioItem>75 <MenuRadioItem value="full-access">Full access</MenuRadioItem>
75 </MenuRadioGroup>76 </MenuRadioGroup>
76 {props.activePlan ? (77 {props.activePlan ? (
packages/contracts/src/orchestration.ts+1
...@@ -117,6 +117,7 @@ export type ModelSelection = typeof ModelSelection.Type;...@@ -117,6 +117,7 @@ export type ModelSelection = typeof ModelSelection.Type;
117export const RuntimeMode = Schema.Literals([117export const RuntimeMode = Schema.Literals([
118 "approval-required",118 "approval-required",
119 "auto-accept-edits",119 "auto-accept-edits",
120 "auto",
120 "full-access",121 "full-access",
121]);122]);
122export type RuntimeMode = typeof RuntimeMode.Type;123export type RuntimeMode = typeof RuntimeMode.Type;