| author | |
| committer | |
| log | fbd77420f27c1322d49726dd927861b8b5903494 |
| tree | ce1574cdb761d8f722efd97df2a814e975591592 |
| parent | 16491a84b9bb2842ad6db41635d087647fc2df5b |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | }); |
| 358 | 358 | ||
| 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 | }); |
| 195 | 197 | ||
| 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( |
| 264 | function runtimeModeToThreadConfig(input: RuntimeMode): { | 264 | function 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"; |
| 111 | import { proposedPlanTitle } from "../../proposedPlan"; | 112 | import { 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; |
| 117 | export const RuntimeMode = Schema.Literals([ | 117 | export 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 | ]); |
| 122 | export type RuntimeMode = typeof RuntimeMode.Type; | 123 | export type RuntimeMode = typeof RuntimeMode.Type; |