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: {
582582 ? "Approve actions"
583583 : flow.runtimeMode === "auto-accept-edits"
584584 ? "Auto-accept edits"
585 : "Full access",
585 : flow.runtimeMode === "auto"
586 ? "Auto"
587 : "Full access",
586588 subactions: [
587589 { id: "options:runtime:approval-required", title: "Approve actions" },
588590 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },
591 { id: "options:runtime:auto", title: "Auto" },
589592 { id: "options:runtime:full-access", title: "Full access" },
590593 ].map((option) => {
591594 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
638638 ? "Approve actions"
639639 : currentRuntimeMode === "auto-accept-edits"
640640 ? "Auto-accept edits"
641 : "Full access",
641 : currentRuntimeMode === "auto"
642 ? "Auto"
643 : "Full access",
642644 subactions: [
643645 { id: "options:runtime:approval-required", title: "Approve actions" },
644646 { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },
647 { id: "options:runtime:auto", title: "Auto" },
645648 { id: "options:runtime:full-access", title: "Full access" },
646649 ].map((option) => {
647650 const value = option.id.replace("options:runtime:", "");
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+19
......@@ -356,6 +356,25 @@ describe("ClaudeAdapterLive", () => {
356356 );
357357 });
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
359378 it.effect("loads Claude filesystem settings sources for SDK sessions", () => {
360379 const harness = makeHarness();
361380 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeAdapter.ts+1
......@@ -3509,6 +3509,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
35093509 const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);
35103510 const runtimeModeToPermission: Record<string, PermissionMode> = {
35113511 "auto-accept-edits": "acceptEdits",
3512 auto: "auto",
35123513 "full-access": "bypassPermissions",
35133514 };
35143515 const permissionMode = runtimeModeToPermission[input.runtimeMode];
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+28
......@@ -104,6 +104,7 @@ describe("buildTurnStartParams", () => {
104104 NodeAssert.deepStrictEqual(params, {
105105 threadId: "provider-thread-1",
106106 approvalPolicy: "never",
107 approvalsReviewer: "user",
107108 sandboxPolicy: {
108109 type: "dangerFullAccess",
109110 },
......@@ -149,6 +150,7 @@ describe("buildTurnStartParams", () => {
149150 NodeAssert.deepStrictEqual(params, {
150151 threadId: "provider-thread-1",
151152 approvalPolicy: "on-request",
153 approvalsReviewer: "user",
152154 sandboxPolicy: {
153155 type: "workspaceWrite",
154156 },
......@@ -193,6 +195,31 @@ describe("buildTurnStartParams", () => {
193195 NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));
194196 });
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
196223 it("omits collaboration mode when interaction mode is absent", () => {
197224 const params = Effect.runSync(
198225 buildTurnStartParams({
......@@ -205,6 +232,7 @@ describe("buildTurnStartParams", () => {
205232 NodeAssert.deepStrictEqual(params, {
206233 threadId: "provider-thread-1",
207234 approvalPolicy: "untrusted",
235 approvalsReviewer: "user",
208236 sandboxPolicy: {
209237 type: "readOnly",
210238 },
apps/server/src/provider/Layers/CodexSessionRuntime.ts+15
......@@ -264,23 +264,35 @@ function readResumeCursorThreadId(
264264function runtimeModeToThreadConfig(input: RuntimeMode): {
265265 readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval;
266266 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;
267270} {
268271 switch (input) {
269272 case "approval-required":
270273 return {
271274 approvalPolicy: "untrusted",
272275 sandbox: "read-only",
276 approvalsReviewer: "user",
273277 };
274278 case "auto-accept-edits":
275279 return {
276280 approvalPolicy: "on-request",
277281 sandbox: "workspace-write",
282 approvalsReviewer: "user",
283 };
284 case "auto":
285 return {
286 approvalPolicy: "on-request",
287 sandbox: "workspace-write",
288 approvalsReviewer: "auto_review",
278289 };
279290 case "full-access":
280291 default:
281292 return {
282293 approvalPolicy: "never",
283294 sandbox: "danger-full-access",
295 approvalsReviewer: "user",
284296 };
285297 }
286298}
......@@ -296,6 +308,7 @@ function buildThreadStartParams(input: {
296308 cwd: input.cwd,
297309 approvalPolicy: config.approvalPolicy,
298310 sandbox: config.sandbox,
311 approvalsReviewer: config.approvalsReviewer,
299312 ...(input.model ? { model: input.model } : {}),
300313 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
301314 };
......@@ -310,6 +323,7 @@ function runtimeModeToTurnSandboxPolicy(
310323 type: "readOnly",
311324 };
312325 case "auto-accept-edits":
326 case "auto":
313327 return {
314328 type: "workspaceWrite",
315329 };
......@@ -382,6 +396,7 @@ export function buildTurnStartParams(input: {
382396 threadId: input.threadId,
383397 input: turnInput,
384398 approvalPolicy: config.approvalPolicy,
399 approvalsReviewer: config.approvalsReviewer,
385400 sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode),
386401 ...(input.model ? { model: input.model } : {}),
387402 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
apps/web/src/components/chat/ChatComposer.tsx+6
......@@ -106,6 +106,7 @@ import {
106106 LockIcon,
107107 LockOpenIcon,
108108 PenLineIcon,
109 SparklesIcon,
109110 XIcon,
110111} from "lucide-react";
111112import { proposedPlanTitle } from "../../proposedPlan";
......@@ -149,6 +150,11 @@ const runtimeModeConfig: Record<
149150 description: "Auto-approve edits, ask before other actions.",
150151 icon: PenLineIcon,
151152 },
153 auto: {
154 label: "Auto",
155 description: "An AI reviewer approves routine actions; risky ones still ask.",
156 icon: SparklesIcon,
157 },
152158 "full-access": {
153159 label: "Full access",
154160 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
7171 >
7272 <MenuRadioItem value="approval-required">Supervised</MenuRadioItem>
7373 <MenuRadioItem value="auto-accept-edits">Auto-accept edits</MenuRadioItem>
74 <MenuRadioItem value="auto">Auto</MenuRadioItem>
7475 <MenuRadioItem value="full-access">Full access</MenuRadioItem>
7576 </MenuRadioGroup>
7677 {props.activePlan ? (
packages/contracts/src/orchestration.ts+1
......@@ -117,6 +117,7 @@ export type ModelSelection = typeof ModelSelection.Type;
117117export const RuntimeMode = Schema.Literals([
118118 "approval-required",
119119 "auto-accept-edits",
120 "auto",
120121 "full-access",
121122]);
122123export type RuntimeMode = typeof RuntimeMode.Type;