authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 21:08:38-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 21:26:35-08:00
log8ef653fcc0270855a7e39140664e584083e759b4
tree4b109cf58599e2baaa20c060855fe0d93048fe0a
parentfb8d66b8d321ce58a52768560cdc3349d7db878d

Persist required approval policy and sandbox mode for sessions

- Make `approvalPolicy` and `sandboxMode` required in orchestration contracts and turn-start flow - Add defaults in provider reactors/ingestion to guarantee values are always set - Store and hydrate both fields in projection persistence, snapshots, and related tests

19 files changed, 106 insertions(+), 36 deletions(-)

apps/server/integration/orchestrationEngine.integration.test.ts+2
...@@ -136,6 +136,8 @@ const startTurn = (input: {...@@ -136,6 +136,8 @@ const startTurn = (input: {
136 text: input.text,136 text: input.text,
137 attachments: [],137 attachments: [],
138 },138 },
139 approvalPolicy: "on-request",
140 sandboxMode: "workspace-write",
139 createdAt: nowIso(),141 createdAt: nowIso(),
140 });142 });
141143
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+16
...@@ -317,6 +317,8 @@ describe("CheckpointReactor", () => {...@@ -317,6 +317,8 @@ describe("CheckpointReactor", () => {
317 providerName: "codex",317 providerName: "codex",
318 providerSessionId: asSessionId("sess-1"),318 providerSessionId: asSessionId("sess-1"),
319 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),319 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
320 approvalPolicy: "on-request",
321 sandboxMode: "workspace-write",
320 activeTurnId: null,322 activeTurnId: null,
321 lastError: null,323 lastError: null,
322 updatedAt: createdAt,324 updatedAt: createdAt,
...@@ -394,6 +396,8 @@ describe("CheckpointReactor", () => {...@@ -394,6 +396,8 @@ describe("CheckpointReactor", () => {
394 providerName: "codex",396 providerName: "codex",
395 providerSessionId: asSessionId("sess-1"),397 providerSessionId: asSessionId("sess-1"),
396 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),398 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
399 approvalPolicy: "on-request",
400 sandboxMode: "workspace-write",
397 activeTurnId: null,401 activeTurnId: null,
398 lastError: null,402 lastError: null,
399 updatedAt: createdAt,403 updatedAt: createdAt,
...@@ -445,6 +449,8 @@ describe("CheckpointReactor", () => {...@@ -445,6 +449,8 @@ describe("CheckpointReactor", () => {
445 text: "start turn",449 text: "start turn",
446 attachments: [],450 attachments: [],
447 },451 },
452 approvalPolicy: "on-request",
453 sandboxMode: "workspace-write",
448 createdAt: new Date().toISOString(),454 createdAt: new Date().toISOString(),
449 }),455 }),
450 );456 );
...@@ -481,6 +487,8 @@ describe("CheckpointReactor", () => {...@@ -481,6 +487,8 @@ describe("CheckpointReactor", () => {
481 providerName: "codex",487 providerName: "codex",
482 providerSessionId: asSessionId("sess-missing"),488 providerSessionId: asSessionId("sess-missing"),
483 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"),489 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"),
490 approvalPolicy: "on-request",
491 sandboxMode: "workspace-write",
484 activeTurnId: asTurnId("turn-missing-cwd"),492 activeTurnId: asTurnId("turn-missing-cwd"),
485 lastError: null,493 lastError: null,
486 updatedAt: createdAt,494 updatedAt: createdAt,
...@@ -529,6 +537,8 @@ describe("CheckpointReactor", () => {...@@ -529,6 +537,8 @@ describe("CheckpointReactor", () => {
529 providerName: "codex",537 providerName: "codex",
530 providerSessionId: asSessionId("sess-1"),538 providerSessionId: asSessionId("sess-1"),
531 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),539 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
540 approvalPolicy: "on-request",
541 sandboxMode: "workspace-write",
532 activeTurnId: null,542 activeTurnId: null,
533 lastError: null,543 lastError: null,
534 updatedAt: createdAt,544 updatedAt: createdAt,
...@@ -580,6 +590,8 @@ describe("CheckpointReactor", () => {...@@ -580,6 +590,8 @@ describe("CheckpointReactor", () => {
580 providerName: "codex",590 providerName: "codex",
581 providerSessionId: asSessionId("sess-1"),591 providerSessionId: asSessionId("sess-1"),
582 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),592 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
593 approvalPolicy: "on-request",
594 sandboxMode: "workspace-write",
583 activeTurnId: null,595 activeTurnId: null,
584 lastError: null,596 lastError: null,
585 updatedAt: createdAt,597 updatedAt: createdAt,
...@@ -634,6 +646,8 @@ describe("CheckpointReactor", () => {...@@ -634,6 +646,8 @@ describe("CheckpointReactor", () => {
634 providerName: "codex",646 providerName: "codex",
635 providerSessionId: asSessionId("sess-1"),647 providerSessionId: asSessionId("sess-1"),
636 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),648 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
649 approvalPolicy: "on-request",
650 sandboxMode: "workspace-write",
637 activeTurnId: null,651 activeTurnId: null,
638 lastError: null,652 lastError: null,
639 updatedAt: createdAt,653 updatedAt: createdAt,
...@@ -713,6 +727,8 @@ describe("CheckpointReactor", () => {...@@ -713,6 +727,8 @@ describe("CheckpointReactor", () => {
713 providerName: "codex",727 providerName: "codex",
714 providerSessionId: asSessionId("sess-1"),728 providerSessionId: asSessionId("sess-1"),
715 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),729 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
730 approvalPolicy: "on-request",
731 sandboxMode: "workspace-write",
716 activeTurnId: null,732 activeTurnId: null,
717 lastError: null,733 lastError: null,
718 updatedAt: createdAt,734 updatedAt: createdAt,
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+6
...@@ -92,6 +92,8 @@ describe("OrchestrationEngine", () => {...@@ -92,6 +92,8 @@ describe("OrchestrationEngine", () => {
92 text: "hello",92 text: "hello",
93 attachments: [],93 attachments: [],
94 },94 },
95 approvalPolicy: "on-request",
96 sandboxMode: "workspace-write",
95 createdAt,97 createdAt,
96 }),98 }),
97 );99 );
...@@ -428,6 +430,8 @@ describe("OrchestrationEngine", () => {...@@ -428,6 +430,8 @@ describe("OrchestrationEngine", () => {
428 text: "hello",430 text: "hello",
429 attachments: [],431 attachments: [],
430 },432 },
433 approvalPolicy: "on-request" as const,
434 sandboxMode: "workspace-write" as const,
431 createdAt,435 createdAt,
432 };436 };
433437
...@@ -586,6 +590,8 @@ describe("OrchestrationEngine", () => {...@@ -586,6 +590,8 @@ describe("OrchestrationEngine", () => {
586 text: "hello",590 text: "hello",
587 attachments: [],591 attachments: [],
588 },592 },
593 approvalPolicy: "on-request",
594 sandboxMode: "workspace-write",
589 createdAt: now(),595 createdAt: now(),
590 }),596 }),
591 ),597 ),
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+4
...@@ -794,6 +794,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta...@@ -794,6 +794,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta
794 payload: {794 payload: {
795 threadId,795 threadId,
796 messageId,796 messageId,
797 approvalPolicy: "on-request",
798 sandboxMode: "workspace-write",
797 createdAt: turnStartedAt,799 createdAt: turnStartedAt,
798 },800 },
799 });801 });
...@@ -827,6 +829,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta...@@ -827,6 +829,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta
827 providerName: "codex",829 providerName: "codex",
828 providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"),830 providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"),
829 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"),831 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"),
832 approvalPolicy: "on-request",
833 sandboxMode: "workspace-write",
830 activeTurnId: turnId,834 activeTurnId: turnId,
831 lastError: null,835 lastError: null,
832 updatedAt: sessionSetAt,836 updatedAt: sessionSetAt,
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+2
...@@ -473,6 +473,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {...@@ -473,6 +473,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
473 providerName: event.payload.session.providerName,473 providerName: event.payload.session.providerName,
474 providerSessionId: event.payload.session.providerSessionId,474 providerSessionId: event.payload.session.providerSessionId,
475 providerThreadId: event.payload.session.providerThreadId,475 providerThreadId: event.payload.session.providerThreadId,
476 approvalPolicy: event.payload.session.approvalPolicy,
477 sandboxMode: event.payload.session.sandboxMode,
476 activeTurnId: event.payload.session.activeTurnId,478 activeTurnId: event.payload.session.activeTurnId,
477 lastError: event.payload.session.lastError,479 lastError: event.payload.session.lastError,
478 updatedAt: event.payload.session.updatedAt,480 updatedAt: event.payload.session.updatedAt,
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+6
...@@ -143,6 +143,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {...@@ -143,6 +143,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
143 provider_name,143 provider_name,
144 provider_session_id,144 provider_session_id,
145 provider_thread_id,145 provider_thread_id,
146 approval_policy,
147 sandbox_mode,
146 active_turn_id,148 active_turn_id,
147 last_error,149 last_error,
148 updated_at150 updated_at
...@@ -153,6 +155,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {...@@ -153,6 +155,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
153 'codex',155 'codex',
154 'provider-session-1',156 'provider-session-1',
155 'provider-thread-1',157 'provider-thread-1',
158 'on-request',
159 'workspace-write',
156 'turn-1',160 'turn-1',
157 NULL,161 NULL,
158 '2026-02-24T00:00:07.000Z'162 '2026-02-24T00:00:07.000Z'
...@@ -282,6 +286,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {...@@ -282,6 +286,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
282 providerName: "codex",286 providerName: "codex",
283 providerSessionId: asProviderSessionId("provider-session-1"),287 providerSessionId: asProviderSessionId("provider-session-1"),
284 providerThreadId: asProviderThreadId("provider-thread-1"),288 providerThreadId: asProviderThreadId("provider-thread-1"),
289 approvalPolicy: "on-request",
290 sandboxMode: "workspace-write",
285 activeTurnId: asTurnId("turn-1"),291 activeTurnId: asTurnId("turn-1"),
286 lastError: null,292 lastError: null,
287 updatedAt: "2026-02-24T00:00:07.000Z",293 updatedAt: "2026-02-24T00:00:07.000Z",
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+4
...@@ -196,6 +196,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -196,6 +196,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
196 provider_name AS "providerName",196 provider_name AS "providerName",
197 provider_session_id AS "providerSessionId",197 provider_session_id AS "providerSessionId",
198 provider_thread_id AS "providerThreadId",198 provider_thread_id AS "providerThreadId",
199 approval_policy AS "approvalPolicy",
200 sandbox_mode AS "sandboxMode",
199 active_turn_id AS "activeTurnId",201 active_turn_id AS "activeTurnId",
200 last_error AS "lastError",202 last_error AS "lastError",
201 updated_at AS "updatedAt"203 updated_at AS "updatedAt"
...@@ -378,6 +380,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -378,6 +380,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
378 providerName: row.providerName,380 providerName: row.providerName,
379 providerSessionId: row.providerSessionId,381 providerSessionId: row.providerSessionId,
380 providerThreadId: row.providerThreadId,382 providerThreadId: row.providerThreadId,
383 approvalPolicy: row.approvalPolicy,
384 sandboxMode: row.sandboxMode,
381 activeTurnId: row.activeTurnId,385 activeTurnId: row.activeTurnId,
382 lastError: row.lastError,386 lastError: row.lastError,
383 updatedAt: row.updatedAt,387 updatedAt: row.updatedAt,
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+6
...@@ -319,6 +319,8 @@ describe("ProviderCommandReactor", () => {...@@ -319,6 +319,8 @@ describe("ProviderCommandReactor", () => {
319 providerName: "codex",319 providerName: "codex",
320 providerSessionId: asSessionId("sess-1"),320 providerSessionId: asSessionId("sess-1"),
321 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),321 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
322 approvalPolicy: "on-request",
323 sandboxMode: "workspace-write",
322 activeTurnId: asTurnId("turn-1"),324 activeTurnId: asTurnId("turn-1"),
323 lastError: null,325 lastError: null,
324 updatedAt: now,326 updatedAt: now,
...@@ -358,6 +360,8 @@ describe("ProviderCommandReactor", () => {...@@ -358,6 +360,8 @@ describe("ProviderCommandReactor", () => {
358 providerName: "codex",360 providerName: "codex",
359 providerSessionId: asSessionId("sess-1"),361 providerSessionId: asSessionId("sess-1"),
360 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),362 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
363 approvalPolicy: "on-request",
364 sandboxMode: "workspace-write",
361 activeTurnId: null,365 activeTurnId: null,
362 lastError: null,366 lastError: null,
363 updatedAt: now,367 updatedAt: now,
...@@ -400,6 +404,8 @@ describe("ProviderCommandReactor", () => {...@@ -400,6 +404,8 @@ describe("ProviderCommandReactor", () => {
400 providerName: "codex",404 providerName: "codex",
401 providerSessionId: asSessionId("sess-1"),405 providerSessionId: asSessionId("sess-1"),
402 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),406 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
407 approvalPolicy: "on-request",
408 sandboxMode: "workspace-write",
403 activeTurnId: null,409 activeTurnId: null,
404 lastError: null,410 lastError: null,
405 updatedAt: now,411 updatedAt: now,
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+18-14
...@@ -64,6 +64,8 @@ const serverCommandId = (tag: string): CommandId =>...@@ -64,6 +64,8 @@ const serverCommandId = (tag: string): CommandId =>
6464
65const HANDLED_TURN_START_KEY_MAX = 10_000;65const HANDLED_TURN_START_KEY_MAX = 10_000;
66const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);66const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
67const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never";
68const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write";
6769
68const make = Effect.gen(function* () {70const make = Effect.gen(function* () {
69 const orchestrationEngine = yield* OrchestrationEngineService;71 const orchestrationEngine = yield* OrchestrationEngineService;
...@@ -143,8 +145,10 @@ const make = Effect.gen(function* () {...@@ -143,8 +145,10 @@ const make = Effect.gen(function* () {
143 return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`));145 return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`));
144 }146 }
145147
146 const desiredApprovalPolicy = options?.approvalPolicy ?? thread.session?.approvalPolicy;148 const desiredApprovalPolicy =
147 const desiredSandboxMode = options?.sandboxMode ?? thread.session?.sandboxMode;149 options?.approvalPolicy ?? thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY;
150 const desiredSandboxMode =
151 options?.sandboxMode ?? thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE;
148 const preferredProvider: ProviderKind | undefined =152 const preferredProvider: ProviderKind | undefined =
149 thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode"153 thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode"
150 ? thread.session.providerName154 ? thread.session.providerName
...@@ -160,8 +164,8 @@ const make = Effect.gen(function* () {...@@ -160,8 +164,8 @@ const make = Effect.gen(function* () {
160 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),164 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),
161 ...(thread.model ? { model: thread.model } : {}),165 ...(thread.model ? { model: thread.model } : {}),
162 ...(resumeThreadId ? { resumeThreadId } : {}),166 ...(resumeThreadId ? { resumeThreadId } : {}),
163 ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}),167 approvalPolicy: desiredApprovalPolicy,
164 ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}),168 sandboxMode: desiredSandboxMode,
165 });169 });
166170
167 const bindSessionToThread = (session: ProviderSession) =>171 const bindSessionToThread = (session: ProviderSession) =>
...@@ -173,8 +177,8 @@ const make = Effect.gen(function* () {...@@ -173,8 +177,8 @@ const make = Effect.gen(function* () {
173 providerName: session.provider,177 providerName: session.provider,
174 providerSessionId: session.sessionId,178 providerSessionId: session.sessionId,
175 providerThreadId: session.threadId ?? null,179 providerThreadId: session.threadId ?? null,
176 ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}),180 approvalPolicy: desiredApprovalPolicy,
177 ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}),181 sandboxMode: desiredSandboxMode,
178 // Provider turn ids are not orchestration turn ids.182 // Provider turn ids are not orchestration turn ids.
179 activeTurnId: null,183 activeTurnId: null,
180 lastError: session.lastError ?? null,184 lastError: session.lastError ?? null,
...@@ -211,8 +215,8 @@ const make = Effect.gen(function* () {...@@ -211,8 +215,8 @@ const make = Effect.gen(function* () {
211 readonly attachments?: ReadonlyArray<ChatAttachment>;215 readonly attachments?: ReadonlyArray<ChatAttachment>;
212 readonly model?: string;216 readonly model?: string;
213 readonly effort?: string;217 readonly effort?: string;
214 readonly approvalPolicy?: ProviderApprovalPolicy;218 readonly approvalPolicy: ProviderApprovalPolicy;
215 readonly sandboxMode?: ProviderSandboxMode;219 readonly sandboxMode: ProviderSandboxMode;
216 readonly createdAt: string;220 readonly createdAt: string;
217 }) {221 }) {
218 const thread = yield* resolveThread(input.threadId);222 const thread = yield* resolveThread(input.threadId);
...@@ -220,8 +224,8 @@ const make = Effect.gen(function* () {...@@ -220,8 +224,8 @@ const make = Effect.gen(function* () {
220 return;224 return;
221 }225 }
222 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, {226 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, {
223 ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}),227 approvalPolicy: input.approvalPolicy,
224 ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}),228 sandboxMode: input.sandboxMode,
225 });229 });
226 const normalizedInput = toNonEmptyProviderInput(input.messageText);230 const normalizedInput = toNonEmptyProviderInput(input.messageText);
227 const normalizedAttachments = input.attachments ?? [];231 const normalizedAttachments = input.attachments ?? [];
...@@ -267,10 +271,8 @@ const make = Effect.gen(function* () {...@@ -267,10 +271,8 @@ const make = Effect.gen(function* () {
267 ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),271 ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
268 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),272 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
269 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),273 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),
270 ...(event.payload.approvalPolicy !== undefined274 approvalPolicy: event.payload.approvalPolicy,
271 ? { approvalPolicy: event.payload.approvalPolicy }275 sandboxMode: event.payload.sandboxMode,
272 : {}),
273 ...(event.payload.sandboxMode !== undefined ? { sandboxMode: event.payload.sandboxMode } : {}),
274 createdAt: event.payload.createdAt,276 createdAt: event.payload.createdAt,
275 });277 });
276 });278 });
...@@ -347,6 +349,8 @@ const make = Effect.gen(function* () {...@@ -347,6 +349,8 @@ const make = Effect.gen(function* () {
347 providerName: thread.session?.providerName ?? null,349 providerName: thread.session?.providerName ?? null,
348 providerSessionId: null,350 providerSessionId: null,
349 providerThreadId: null,351 providerThreadId: null,
352 approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
353 sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
350 activeTurnId: null,354 activeTurnId: null,
351 lastError: thread.session?.lastError ?? null,355 lastError: thread.session?.lastError ?? null,
352 updatedAt: now,356 updatedAt: now,
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+4
...@@ -164,6 +164,8 @@ describe("ProviderRuntimeIngestion", () => {...@@ -164,6 +164,8 @@ describe("ProviderRuntimeIngestion", () => {
164 providerName: "codex",164 providerName: "codex",
165 providerSessionId: asSessionId("sess-1"),165 providerSessionId: asSessionId("sess-1"),
166 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),166 providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
167 approvalPolicy: "on-request",
168 sandboxMode: "workspace-write",
167 activeTurnId: null,169 activeTurnId: null,
168 updatedAt: createdAt,170 updatedAt: createdAt,
169 lastError: null,171 lastError: null,
...@@ -332,6 +334,8 @@ describe("ProviderRuntimeIngestion", () => {...@@ -332,6 +334,8 @@ describe("ProviderRuntimeIngestion", () => {
332 attachments: [],334 attachments: [],
333 },335 },
334 assistantDeliveryMode: "streaming",336 assistantDeliveryMode: "streaming",
337 approvalPolicy: "on-request",
338 sandboxMode: "workspace-write",
335 createdAt: now,339 createdAt: now,
336 }),340 }),
337 );341 );
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+8-12
...@@ -3,6 +3,8 @@ import {...@@ -3,6 +3,8 @@ import {
3 CommandId,3 CommandId,
4 MessageId,4 MessageId,
5 type OrchestrationEvent,5 type OrchestrationEvent,
6 type ProviderApprovalPolicy,
7 type ProviderSandboxMode,
6 ProviderThreadId,8 ProviderThreadId,
7 type ThreadId,9 type ThreadId,
8 TurnId,10 TurnId,
...@@ -24,6 +26,8 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId...@@ -24,6 +26,8 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId
24 CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`);26 CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`);
2527
26const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered";28const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered";
29const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never";
30const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write";
27const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000;31const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000;
28const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120);32const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120);
29const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;33const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
...@@ -371,12 +375,8 @@ const make = Effect.gen(function* () {...@@ -371,12 +375,8 @@ const make = Effect.gen(function* () {
371 providerName: event.provider,375 providerName: event.provider,
372 providerSessionId: event.sessionId,376 providerSessionId: event.sessionId,
373 providerThreadId,377 providerThreadId,
374 ...(thread.session?.approvalPolicy !== undefined378 approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
375 ? { approvalPolicy: thread.session.approvalPolicy }379 sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
376 : {}),
377 ...(thread.session?.sandboxMode !== undefined
378 ? { sandboxMode: thread.session.sandboxMode }
379 : {}),
380 activeTurnId,380 activeTurnId,
381 lastError,381 lastError,
382 updatedAt: now,382 updatedAt: now,
...@@ -485,12 +485,8 @@ const make = Effect.gen(function* () {...@@ -485,12 +485,8 @@ const make = Effect.gen(function* () {
485 providerName: event.provider,485 providerName: event.provider,
486 providerSessionId: event.sessionId,486 providerSessionId: event.sessionId,
487 providerThreadId,487 providerThreadId,
488 ...(thread.session?.approvalPolicy !== undefined488 approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
489 ? { approvalPolicy: thread.session.approvalPolicy }489 sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
490 : {}),
491 ...(thread.session?.sandboxMode !== undefined
492 ? { sandboxMode: thread.session.sandboxMode }
493 : {}),
494 activeTurnId: toTurnId(event.turnId) ?? null,490 activeTurnId: toTurnId(event.turnId) ?? null,
495 lastError: event.message,491 lastError: event.message,
496 updatedAt: now,492 updatedAt: now,
apps/server/src/orchestration/commandInvariants.test.ts+2
...@@ -90,6 +90,8 @@ const messageSendCommand: OrchestrationCommand = {...@@ -90,6 +90,8 @@ const messageSendCommand: OrchestrationCommand = {
90 text: "hello",90 text: "hello",
91 attachments: [],91 attachments: [],
92 },92 },
93 approvalPolicy: "on-request",
94 sandboxMode: "workspace-write",
93 createdAt: now,95 createdAt: now,
94};96};
9597
apps/server/src/orchestration/decider.ts+2-4
...@@ -254,10 +254,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -254,10 +254,8 @@ 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 !== undefined257 approvalPolicy: command.approvalPolicy,
258 ? { approvalPolicy: command.approvalPolicy }258 sandboxMode: command.sandboxMode,
259 : {}),
260 ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}),
261 createdAt: command.createdAt,259 createdAt: command.createdAt,
262 },260 },
263 };261 };
apps/server/src/orchestration/projector.test.ts+4
...@@ -135,6 +135,8 @@ describe("orchestration projector", () => {...@@ -135,6 +135,8 @@ describe("orchestration projector", () => {
135 payload: {135 payload: {
136 threadId: "thread-1",136 threadId: "thread-1",
137 messageId: "message-1",137 messageId: "message-1",
138 approvalPolicy: "on-request",
139 sandboxMode: "workspace-write",
138 createdAt: "2026-01-01T00:00:00.000Z",140 createdAt: "2026-01-01T00:00:00.000Z",
139 },141 },
140 }),142 }),
...@@ -193,6 +195,8 @@ describe("orchestration projector", () => {...@@ -193,6 +195,8 @@ describe("orchestration projector", () => {
193 providerName: "codex",195 providerName: "codex",
194 providerSessionId: "session-1",196 providerSessionId: "session-1",
195 providerThreadId: "provider-thread-1",197 providerThreadId: "provider-thread-1",
198 approvalPolicy: "on-request",
199 sandboxMode: "workspace-write",
196 activeTurnId: "turn-1",200 activeTurnId: "turn-1",
197 lastError: null,201 lastError: null,
198 updatedAt: startedAt,202 updatedAt: startedAt,
apps/server/src/persistence/Layers/ProjectionThreadSessions.ts+8
...@@ -25,6 +25,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {...@@ -25,6 +25,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {
25 provider_name,25 provider_name,
26 provider_session_id,26 provider_session_id,
27 provider_thread_id,27 provider_thread_id,
28 approval_policy,
29 sandbox_mode,
28 active_turn_id,30 active_turn_id,
29 last_error,31 last_error,
30 updated_at32 updated_at
...@@ -35,6 +37,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {...@@ -35,6 +37,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {
35 ${row.providerName},37 ${row.providerName},
36 ${row.providerSessionId},38 ${row.providerSessionId},
37 ${row.providerThreadId},39 ${row.providerThreadId},
40 ${row.approvalPolicy},
41 ${row.sandboxMode},
38 ${row.activeTurnId},42 ${row.activeTurnId},
39 ${row.lastError},43 ${row.lastError},
40 ${row.updatedAt}44 ${row.updatedAt}
...@@ -45,6 +49,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {...@@ -45,6 +49,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {
45 provider_name = excluded.provider_name,49 provider_name = excluded.provider_name,
46 provider_session_id = excluded.provider_session_id,50 provider_session_id = excluded.provider_session_id,
47 provider_thread_id = excluded.provider_thread_id,51 provider_thread_id = excluded.provider_thread_id,
52 approval_policy = excluded.approval_policy,
53 sandbox_mode = excluded.sandbox_mode,
48 active_turn_id = excluded.active_turn_id,54 active_turn_id = excluded.active_turn_id,
49 last_error = excluded.last_error,55 last_error = excluded.last_error,
50 updated_at = excluded.updated_at56 updated_at = excluded.updated_at
...@@ -62,6 +68,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {...@@ -62,6 +68,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () {
62 provider_name AS "providerName",68 provider_name AS "providerName",
63 provider_session_id AS "providerSessionId",69 provider_session_id AS "providerSessionId",
64 provider_thread_id AS "providerThreadId",70 provider_thread_id AS "providerThreadId",
71 approval_policy AS "approvalPolicy",
72 sandbox_mode AS "sandboxMode",
65 active_turn_id AS "activeTurnId",73 active_turn_id AS "activeTurnId",
66 last_error AS "lastError",74 last_error AS "lastError",
67 updated_at AS "updatedAt"75 updated_at AS "updatedAt"
apps/server/src/persistence/Migrations/005_Projections.ts+2
...@@ -65,6 +65,8 @@ export default Effect.gen(function* () {...@@ -65,6 +65,8 @@ export default Effect.gen(function* () {
65 provider_name TEXT,65 provider_name TEXT,
66 provider_session_id TEXT,66 provider_session_id TEXT,
67 provider_thread_id TEXT,67 provider_thread_id TEXT,
68 approval_policy TEXT NOT NULL,
69 sandbox_mode TEXT NOT NULL,
68 active_turn_id TEXT,70 active_turn_id TEXT,
69 last_error TEXT,71 last_error TEXT,
70 updated_at TEXT NOT NULL72 updated_at TEXT NOT NULL
apps/server/src/persistence/Services/ProjectionThreadSessions.ts+4
...@@ -9,6 +9,8 @@...@@ -9,6 +9,8 @@
9import {9import {
10 IsoDateTime,10 IsoDateTime,
11 OrchestrationSessionStatus,11 OrchestrationSessionStatus,
12 ProviderApprovalPolicy,
13 ProviderSandboxMode,
12 ProviderSessionId,14 ProviderSessionId,
13 ProviderThreadId,15 ProviderThreadId,
14 ThreadId,16 ThreadId,
...@@ -25,6 +27,8 @@ export const ProjectionThreadSession = Schema.Struct({...@@ -25,6 +27,8 @@ export const ProjectionThreadSession = Schema.Struct({
25 providerName: Schema.NullOr(Schema.String),27 providerName: Schema.NullOr(Schema.String),
26 providerSessionId: Schema.NullOr(ProviderSessionId),28 providerSessionId: Schema.NullOr(ProviderSessionId),
27 providerThreadId: Schema.NullOr(ProviderThreadId),29 providerThreadId: Schema.NullOr(ProviderThreadId),
30 approvalPolicy: ProviderApprovalPolicy,
31 sandboxMode: ProviderSandboxMode,
28 activeTurnId: Schema.NullOr(TurnId),32 activeTurnId: Schema.NullOr(TurnId),
29 lastError: Schema.NullOr(Schema.String),33 lastError: Schema.NullOr(Schema.String),
30 updatedAt: IsoDateTime,34 updatedAt: IsoDateTime,
apps/server/src/wsServer.test.ts+2
...@@ -1055,6 +1055,8 @@ describe("WebSocket Server", () => {...@@ -1055,6 +1055,8 @@ describe("WebSocket Server", () => {
1055 attachments: [],1055 attachments: [],
1056 },1056 },
1057 assistantDeliveryMode: "streaming",1057 assistantDeliveryMode: "streaming",
1058 approvalPolicy: "on-request",
1059 sandboxMode: "workspace-write",
1058 createdAt,1060 createdAt,
1059 });1061 });
1060 expect(startTurnResponse.error).toBeUndefined();1062 expect(startTurnResponse.error).toBeUndefined();
packages/contracts/src/orchestration.ts+6-6
...@@ -142,8 +142,8 @@ export const OrchestrationSession = Schema.Struct({...@@ -142,8 +142,8 @@ export const OrchestrationSession = Schema.Struct({
142 providerName: Schema.NullOr(TrimmedNonEmptyString),142 providerName: Schema.NullOr(TrimmedNonEmptyString),
143 providerSessionId: Schema.NullOr(ProviderSessionId),143 providerSessionId: Schema.NullOr(ProviderSessionId),
144 providerThreadId: Schema.NullOr(ProviderThreadId),144 providerThreadId: Schema.NullOr(ProviderThreadId),
145 approvalPolicy: Schema.optional(ProviderApprovalPolicy),145 approvalPolicy: ProviderApprovalPolicy,
146 sandboxMode: Schema.optional(ProviderSandboxMode),146 sandboxMode: ProviderSandboxMode,
147 activeTurnId: Schema.NullOr(TurnId),147 activeTurnId: Schema.NullOr(TurnId),
148 lastError: Schema.NullOr(TrimmedNonEmptyString),148 lastError: Schema.NullOr(TrimmedNonEmptyString),
149 updatedAt: IsoDateTime,149 updatedAt: IsoDateTime,
...@@ -284,8 +284,8 @@ export const ThreadTurnStartCommand = Schema.Struct({...@@ -284,8 +284,8 @@ export const ThreadTurnStartCommand = Schema.Struct({
284 model: Schema.optional(TrimmedNonEmptyString),284 model: Schema.optional(TrimmedNonEmptyString),
285 effort: Schema.optional(TrimmedNonEmptyString),285 effort: Schema.optional(TrimmedNonEmptyString),
286 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),286 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
287 approvalPolicy: Schema.optional(ProviderApprovalPolicy),287 approvalPolicy: ProviderApprovalPolicy,
288 sandboxMode: Schema.optional(ProviderSandboxMode),288 sandboxMode: ProviderSandboxMode,
289 createdAt: IsoDateTime,289 createdAt: IsoDateTime,
290});290});
291291
...@@ -500,8 +500,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({...@@ -500,8 +500,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({
500 model: Schema.optional(TrimmedNonEmptyString),500 model: Schema.optional(TrimmedNonEmptyString),
501 effort: Schema.optional(TrimmedNonEmptyString),501 effort: Schema.optional(TrimmedNonEmptyString),
502 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),502 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
503 approvalPolicy: Schema.optional(ProviderApprovalPolicy),503 approvalPolicy: ProviderApprovalPolicy,
504 sandboxMode: Schema.optional(ProviderSandboxMode),504 sandboxMode: ProviderSandboxMode,
505 createdAt: IsoDateTime,505 createdAt: IsoDateTime,
506});506});
507507