| author | |
| committer | |
| log | 57dd091e05904dfe07721254863249305932c0b1 |
| tree | fec6784d9367a9eda20f323e3ba391c2b7b2d3f5 |
| parent | a1f208f1e6bb44e90ff97221fe839a4eeee26fdf |
- 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 updated6 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 | }); |
| 185 | 189 | ||
| 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 | }); |
| 157 | 165 | ||
| 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 ?? []; |
| 192 | 205 | ||
| ... | @@ -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 | }); |
| 237 | 252 |
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 | }); |
| 287 | 289 | ||
| ... | @@ -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 | }); |
| 501 | 505 |