authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 18:39:28-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 21:26:35-08:00
log57dd091e05904dfe07721254863249305932c0b1
treefec6784d9367a9eda20f323e3ba391c2b7b2d3f5
parenta1f208f1e6bb44e90ff97221fe839a4eeee26fdf

Propagate runtime approval and sandbox settings to turn startup

- Add `approvalPolicy` and `sandboxMode` to turn-start command and requested payload contracts - Pass runtime mode-derived values from `ChatView` when dispatching `thread.turn.start` - Thread provider options through decider/reactor into session startup, with tests updated

6 files changed, 43 insertions(+), 1 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+4
...@@ -171,6 +171,8 @@ describe("ProviderCommandReactor", () => {...@@ -171,6 +171,8 @@ describe("ProviderCommandReactor", () => {
171 text: "hello reactor",171 text: "hello reactor",
172 attachments: [],172 attachments: [],
173 },173 },
174 approvalPolicy: "on-request",
175 sandboxMode: "workspace-write",
174 createdAt: now,176 createdAt: now,
175 }),177 }),
176 );178 );
...@@ -181,6 +183,8 @@ describe("ProviderCommandReactor", () => {...@@ -181,6 +183,8 @@ describe("ProviderCommandReactor", () => {
181 expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({183 expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
182 cwd: "/tmp/provider-project",184 cwd: "/tmp/provider-project",
183 model: "gpt-5-codex",185 model: "gpt-5-codex",
186 approvalPolicy: "on-request",
187 sandboxMode: "workspace-write",
184 });188 });
185189
186 const readModel = await Effect.runPromise(harness.engine.getReadModel());190 const readModel = await Effect.runPromise(harness.engine.getReadModel());
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+16-1
...@@ -3,7 +3,9 @@ import {...@@ -3,7 +3,9 @@ import {
3 CommandId,3 CommandId,
4 EventId,4 EventId,
5 type OrchestrationEvent,5 type OrchestrationEvent,
6 type ProviderApprovalPolicy,
6 type ProviderKind,7 type ProviderKind,
8 type ProviderSandboxMode,
7 type OrchestrationSession,9 type OrchestrationSession,
8 type ThreadId,10 type ThreadId,
9 type TurnId,11 type TurnId,
...@@ -128,6 +130,10 @@ const make = Effect.gen(function* () {...@@ -128,6 +130,10 @@ const make = Effect.gen(function* () {
128 const ensureSessionForThread = Effect.fnUntraced(function* (130 const ensureSessionForThread = Effect.fnUntraced(function* (
129 threadId: ThreadId,131 threadId: ThreadId,
130 createdAt: string,132 createdAt: string,
133 options?: {
134 readonly approvalPolicy?: ProviderApprovalPolicy;
135 readonly sandboxMode?: ProviderSandboxMode;
136 },
131 ) {137 ) {
132 const readModel = yield* orchestrationEngine.getReadModel();138 const readModel = yield* orchestrationEngine.getReadModel();
133 const thread = readModel.threads.find((entry) => entry.id === threadId);139 const thread = readModel.threads.find((entry) => entry.id === threadId);
...@@ -153,6 +159,8 @@ const make = Effect.gen(function* () {...@@ -153,6 +159,8 @@ const make = Effect.gen(function* () {
153 ...(preferredProvider ? { provider: preferredProvider } : {}),159 ...(preferredProvider ? { provider: preferredProvider } : {}),
154 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),160 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),
155 ...(thread.model ? { model: thread.model } : {}),161 ...(thread.model ? { model: thread.model } : {}),
162 ...(options?.approvalPolicy !== undefined ? { approvalPolicy: options.approvalPolicy } : {}),
163 ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}),
156 });164 });
157165
158 yield* setThreadSession({166 yield* setThreadSession({
...@@ -180,13 +188,18 @@ const make = Effect.gen(function* () {...@@ -180,13 +188,18 @@ const make = Effect.gen(function* () {
180 readonly attachments?: ReadonlyArray<ChatAttachment>;188 readonly attachments?: ReadonlyArray<ChatAttachment>;
181 readonly model?: string;189 readonly model?: string;
182 readonly effort?: string;190 readonly effort?: string;
191 readonly approvalPolicy?: ProviderApprovalPolicy;
192 readonly sandboxMode?: ProviderSandboxMode;
183 readonly createdAt: string;193 readonly createdAt: string;
184 }) {194 }) {
185 const thread = yield* resolveThread(input.threadId);195 const thread = yield* resolveThread(input.threadId);
186 if (!thread) {196 if (!thread) {
187 return;197 return;
188 }198 }
189 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt);199 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, {
200 ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}),
201 ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}),
202 });
190 const normalizedInput = toNonEmptyProviderInput(input.messageText);203 const normalizedInput = toNonEmptyProviderInput(input.messageText);
191 const normalizedAttachments = input.attachments ?? [];204 const normalizedAttachments = input.attachments ?? [];
192205
...@@ -232,6 +245,8 @@ const make = Effect.gen(function* () {...@@ -232,6 +245,8 @@ const make = Effect.gen(function* () {
232 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),245 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
233 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),246 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),
234 createdAt: event.payload.createdAt,247 createdAt: event.payload.createdAt,
248 approvalPolicy: event.payload.approvalPolicy,
249 sandboxMode: event.payload.sandboxMode,
235 });250 });
236 });251 });
237252
apps/server/src/orchestration/decider.projectScripts.test.ts+10
...@@ -152,6 +152,8 @@ describe("decider project scripts", () => {...@@ -152,6 +152,8 @@ describe("decider project scripts", () => {
152 },152 },
153 model: "gpt-5",153 model: "gpt-5",
154 effort: "high",154 effort: "high",
155 approvalPolicy: "on-request",
156 sandboxMode: "workspace-write",
155 createdAt: now,157 createdAt: now,
156 },158 },
157 readModel,159 readModel,
...@@ -169,5 +171,13 @@ describe("decider project scripts", () => {...@@ -169,5 +171,13 @@ describe("decider project scripts", () => {
169 return;171 return;
170 }172 }
171 expect(turnStartEvent.payload.assistantDeliveryMode).toBe("buffered");173 expect(turnStartEvent.payload.assistantDeliveryMode).toBe("buffered");
174 expect(turnStartEvent.payload).toMatchObject({
175 threadId: ThreadId.makeUnsafe("thread-1"),
176 messageId: asMessageId("message-user-1"),
177 model: "gpt-5",
178 effort: "high",
179 approvalPolicy: "on-request",
180 sandboxMode: "workspace-write",
181 });
172 });182 });
173});183});
apps/server/src/orchestration/decider.ts+4
...@@ -254,6 +254,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -254,6 +254,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
254 ...(command.model !== undefined ? { model: command.model } : {}),254 ...(command.model !== undefined ? { model: command.model } : {}),
255 ...(command.effort !== undefined ? { effort: command.effort } : {}),255 ...(command.effort !== undefined ? { effort: command.effort } : {}),
256 assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,256 assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
257 ...(command.approvalPolicy !== undefined
258 ? { approvalPolicy: command.approvalPolicy }
259 : {}),
260 ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}),
257 createdAt: command.createdAt,261 createdAt: command.createdAt,
258 },262 },
259 };263 };
apps/web/src/components/ChatView.tsx+5
...@@ -1522,6 +1522,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1522,6 +1522,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
1522 ),1522 ),
1523 );1523 );
1524 attemptedTurnStart = true;1524 attemptedTurnStart = true;
1525 const approvalPolicy = state.runtimeMode === "full-access" ? "never" : "on-request";
1526 const sandboxMode =
1527 state.runtimeMode === "full-access" ? "danger-full-access" : "workspace-write";
1525 await api.orchestration.dispatchCommand({1528 await api.orchestration.dispatchCommand({
1526 type: "thread.turn.start",1529 type: "thread.turn.start",
1527 commandId: newCommandId(),1530 commandId: newCommandId(),
...@@ -1535,6 +1538,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1535,6 +1538,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
1535 model: selectedModel || undefined,1538 model: selectedModel || undefined,
1536 effort: selectedEffort || undefined,1539 effort: selectedEffort || undefined,
1537 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",1540 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
1541 approvalPolicy,
1542 sandboxMode,
1538 createdAt: messageCreatedAt,1543 createdAt: messageCreatedAt,
1539 });1544 });
1540 } catch (err) {1545 } catch (err) {
packages/contracts/src/orchestration.ts+4
...@@ -282,6 +282,8 @@ export const ThreadTurnStartCommand = Schema.Struct({...@@ -282,6 +282,8 @@ export const ThreadTurnStartCommand = Schema.Struct({
282 model: Schema.optional(TrimmedNonEmptyString),282 model: Schema.optional(TrimmedNonEmptyString),
283 effort: Schema.optional(TrimmedNonEmptyString),283 effort: Schema.optional(TrimmedNonEmptyString),
284 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),284 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
285 approvalPolicy: Schema.optional(ProviderApprovalPolicy),
286 sandboxMode: Schema.optional(ProviderSandboxMode),
285 createdAt: IsoDateTime,287 createdAt: IsoDateTime,
286});288});
287289
...@@ -496,6 +498,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({...@@ -496,6 +498,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({
496 model: Schema.optional(TrimmedNonEmptyString),498 model: Schema.optional(TrimmedNonEmptyString),
497 effort: Schema.optional(TrimmedNonEmptyString),499 effort: Schema.optional(TrimmedNonEmptyString),
498 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),500 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
501 approvalPolicy: Schema.optional(ProviderApprovalPolicy),
502 sandboxMode: Schema.optional(ProviderSandboxMode),
499 createdAt: IsoDateTime,503 createdAt: IsoDateTime,
500});504});
501505