diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 3c59e54cba866c2f25cede4f0974e09485214cb5..c41ce614aea8c97d3433d1c1e22b610d835d8b0e 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -171,6 +171,8 @@ describe("ProviderCommandReactor", () => { text: "hello reactor", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }), ); @@ -181,6 +183,8 @@ describe("ProviderCommandReactor", () => { expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ cwd: "/tmp/provider-project", model: "gpt-5-codex", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", }); const readModel = await Effect.runPromise(harness.engine.getReadModel()); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index dc08566aefd9330b54bdf24efe2c2514ce3b603b..ecba45185183d300da5f959556c0261d59ac90f2 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -3,7 +3,9 @@ import { CommandId, EventId, type OrchestrationEvent, + type ProviderApprovalPolicy, type ProviderKind, + type ProviderSandboxMode, type OrchestrationSession, type ThreadId, type TurnId, @@ -128,6 +130,10 @@ const make = Effect.gen(function* () { const ensureSessionForThread = Effect.fnUntraced(function* ( threadId: ThreadId, createdAt: string, + options?: { + readonly approvalPolicy?: ProviderApprovalPolicy; + readonly sandboxMode?: ProviderSandboxMode; + }, ) { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); @@ -153,6 +159,8 @@ const make = Effect.gen(function* () { ...(preferredProvider ? { provider: preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), ...(thread.model ? { model: thread.model } : {}), + ...(options?.approvalPolicy !== undefined ? { approvalPolicy: options.approvalPolicy } : {}), + ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}), }); yield* setThreadSession({ @@ -180,13 +188,18 @@ const make = Effect.gen(function* () { readonly attachments?: ReadonlyArray; readonly model?: string; readonly effort?: string; + readonly approvalPolicy?: ProviderApprovalPolicy; + readonly sandboxMode?: ProviderSandboxMode; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); if (!thread) { return; } - const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt); + const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { + ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}), + ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}), + }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; @@ -232,6 +245,8 @@ const make = Effect.gen(function* () { ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), createdAt: event.payload.createdAt, + approvalPolicy: event.payload.approvalPolicy, + sandboxMode: event.payload.sandboxMode, }); }); diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 9973219f629f6494fe54569746e279b5f255ac09..6d528614de5d738e82708e4f4321c555bd5cd5ff 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -152,6 +152,8 @@ describe("decider project scripts", () => { }, model: "gpt-5", effort: "high", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }, readModel, @@ -169,5 +171,13 @@ describe("decider project scripts", () => { return; } expect(turnStartEvent.payload.assistantDeliveryMode).toBe("buffered"); + expect(turnStartEvent.payload).toMatchObject({ + threadId: ThreadId.makeUnsafe("thread-1"), + messageId: asMessageId("message-user-1"), + model: "gpt-5", + effort: "high", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + }); }); }); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 91e7d756e9e8913d99b4f2667061d0e29e68bb6c..9599b4c05f80bec5dc74b5bf3f4dbccd8adfea92 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -254,6 +254,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.effort !== undefined ? { effort: command.effort } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, + ...(command.approvalPolicy !== undefined + ? { approvalPolicy: command.approvalPolicy } + : {}), + ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}), createdAt: command.createdAt, }, }; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 64ac9e1bd3e70151893bd672c5e4b83810f39038..7ae68e5371000bb5d3946e2d57250e519f8ebb3e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1522,6 +1522,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ), ); attemptedTurnStart = true; + const approvalPolicy = state.runtimeMode === "full-access" ? "never" : "on-request"; + const sandboxMode = + state.runtimeMode === "full-access" ? "danger-full-access" : "workspace-write"; await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), @@ -1535,6 +1538,8 @@ export default function ChatView({ threadId }: ChatViewProps) { model: selectedModel || undefined, effort: selectedEffort || undefined, assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", + approvalPolicy, + sandboxMode, createdAt: messageCreatedAt, }); } catch (err) { diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index c2f25d4e02b27fe263520642212042c3aa791b50..cf69a8db844f31e7644b85347e20376afd411bad 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -282,6 +282,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), + approvalPolicy: Schema.optional(ProviderApprovalPolicy), + sandboxMode: Schema.optional(ProviderSandboxMode), createdAt: IsoDateTime, }); @@ -496,6 +498,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), + approvalPolicy: Schema.optional(ProviderApprovalPolicy), + sandboxMode: Schema.optional(ProviderSandboxMode), createdAt: IsoDateTime, });