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", () => {
171171 text: "hello reactor",
172172 attachments: [],
173173 },
174 approvalPolicy: "on-request",
175 sandboxMode: "workspace-write",
174176 createdAt: now,
175177 }),
176178 );
......@@ -181,6 +183,8 @@ describe("ProviderCommandReactor", () => {
181183 expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
182184 cwd: "/tmp/provider-project",
183185 model: "gpt-5-codex",
186 approvalPolicy: "on-request",
187 sandboxMode: "workspace-write",
184188 });
185189
186190 const readModel = await Effect.runPromise(harness.engine.getReadModel());
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+16-1
......@@ -3,7 +3,9 @@ import {
33 CommandId,
44 EventId,
55 type OrchestrationEvent,
6 type ProviderApprovalPolicy,
67 type ProviderKind,
8 type ProviderSandboxMode,
79 type OrchestrationSession,
810 type ThreadId,
911 type TurnId,
......@@ -128,6 +130,10 @@ const make = Effect.gen(function* () {
128130 const ensureSessionForThread = Effect.fnUntraced(function* (
129131 threadId: ThreadId,
130132 createdAt: string,
133 options?: {
134 readonly approvalPolicy?: ProviderApprovalPolicy;
135 readonly sandboxMode?: ProviderSandboxMode;
136 },
131137 ) {
132138 const readModel = yield* orchestrationEngine.getReadModel();
133139 const thread = readModel.threads.find((entry) => entry.id === threadId);
......@@ -153,6 +159,8 @@ const make = Effect.gen(function* () {
153159 ...(preferredProvider ? { provider: preferredProvider } : {}),
154160 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),
155161 ...(thread.model ? { model: thread.model } : {}),
162 ...(options?.approvalPolicy !== undefined ? { approvalPolicy: options.approvalPolicy } : {}),
163 ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}),
156164 });
157165
158166 yield* setThreadSession({
......@@ -180,13 +188,18 @@ const make = Effect.gen(function* () {
180188 readonly attachments?: ReadonlyArray<ChatAttachment>;
181189 readonly model?: string;
182190 readonly effort?: string;
191 readonly approvalPolicy?: ProviderApprovalPolicy;
192 readonly sandboxMode?: ProviderSandboxMode;
183193 readonly createdAt: string;
184194 }) {
185195 const thread = yield* resolveThread(input.threadId);
186196 if (!thread) {
187197 return;
188198 }
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 });
190203 const normalizedInput = toNonEmptyProviderInput(input.messageText);
191204 const normalizedAttachments = input.attachments ?? [];
192205
......@@ -232,6 +245,8 @@ const make = Effect.gen(function* () {
232245 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
233246 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),
234247 createdAt: event.payload.createdAt,
248 approvalPolicy: event.payload.approvalPolicy,
249 sandboxMode: event.payload.sandboxMode,
235250 });
236251 });
237252
apps/server/src/orchestration/decider.projectScripts.test.ts+10
......@@ -152,6 +152,8 @@ describe("decider project scripts", () => {
152152 },
153153 model: "gpt-5",
154154 effort: "high",
155 approvalPolicy: "on-request",
156 sandboxMode: "workspace-write",
155157 createdAt: now,
156158 },
157159 readModel,
......@@ -169,5 +171,13 @@ describe("decider project scripts", () => {
169171 return;
170172 }
171173 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 });
172182 });
173183});
apps/server/src/orchestration/decider.ts+4
......@@ -254,6 +254,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
254254 ...(command.model !== undefined ? { model: command.model } : {}),
255255 ...(command.effort !== undefined ? { effort: command.effort } : {}),
256256 assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
257 ...(command.approvalPolicy !== undefined
258 ? { approvalPolicy: command.approvalPolicy }
259 : {}),
260 ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}),
257261 createdAt: command.createdAt,
258262 },
259263 };
apps/web/src/components/ChatView.tsx+5
......@@ -1522,6 +1522,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
15221522 ),
15231523 );
15241524 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";
15251528 await api.orchestration.dispatchCommand({
15261529 type: "thread.turn.start",
15271530 commandId: newCommandId(),
......@@ -1535,6 +1538,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
15351538 model: selectedModel || undefined,
15361539 effort: selectedEffort || undefined,
15371540 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
1541 approvalPolicy,
1542 sandboxMode,
15381543 createdAt: messageCreatedAt,
15391544 });
15401545 } catch (err) {
packages/contracts/src/orchestration.ts+4
......@@ -282,6 +282,8 @@ export const ThreadTurnStartCommand = Schema.Struct({
282282 model: Schema.optional(TrimmedNonEmptyString),
283283 effort: Schema.optional(TrimmedNonEmptyString),
284284 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
285 approvalPolicy: Schema.optional(ProviderApprovalPolicy),
286 sandboxMode: Schema.optional(ProviderSandboxMode),
285287 createdAt: IsoDateTime,
286288});
287289
......@@ -496,6 +498,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({
496498 model: Schema.optional(TrimmedNonEmptyString),
497499 effort: Schema.optional(TrimmedNonEmptyString),
498500 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
501 approvalPolicy: Schema.optional(ProviderApprovalPolicy),
502 sandboxMode: Schema.optional(ProviderSandboxMode),
499503 createdAt: IsoDateTime,
500504});
501505