History

Persist provider runtime mode settings

Store approval policy and sandbox mode in provider session runtime records, carry them through session recovery, and cover the behavior with provider layer tests.

Julius Marminge committed 8 months ago

Commit de5270e1 parent d48b42c9 files · +111 −2Browse files

Unified · Split

9 files
  1. apps/server/src/persistence/Layers/ProviderSessionRuntime.ts +10 −0
  2. apps/server/src/persistence/Migrations.ts +2 −0
  3. apps/server/src/persistence/Migrations/008_ProviderSessionRuntimeModeColumns.ts +31 −0
  4. apps/server/src/persistence/Services/ProviderSessionRuntime.ts +4 −0
  5. apps/server/src/provider/Layers/ProviderService.test.ts +12 −0
  6. apps/server/src/provider/Layers/ProviderService.ts +26 −1
  7. apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts +6 −0
  8. apps/server/src/provider/Layers/ProviderSessionDirectory.ts +16 −1
  9. apps/server/src/provider/Services/ProviderSessionDirectory.ts +4 −0
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts+10−0
@@ -50,6 +50,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 49 unmodified lines
5050 adapter_key,
5151 provider_thread_id,
5252 status,
53+ approval_policy,
54+ sandbox_mode,
5355 last_seen_at,
5456 resume_cursor_json,
5557 runtime_payload_json
@@ -61,6 +63,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 5 unmodified lines
6163 ${runtime.adapterKey},
6264 ${runtime.providerThreadId},
6365 ${runtime.status},
66+ ${runtime.approvalPolicy},
67+ ${runtime.sandboxMode},
6468 ${runtime.lastSeenAt},
6569 ${runtime.resumeCursor},
6670 ${runtime.runtimePayload}
@@ -72,6 +76,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 5 unmodified lines
7276 adapter_key = excluded.adapter_key,
7377 provider_thread_id = excluded.provider_thread_id,
7478 status = excluded.status,
79+ approval_policy = excluded.approval_policy,
80+ sandbox_mode = excluded.sandbox_mode,
7581 last_seen_at = excluded.last_seen_at,
7682 resume_cursor_json = excluded.resume_cursor_json,
7783 runtime_payload_json = excluded.runtime_payload_json
@@ -90,6 +96,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 12 unmodified lines
9096 adapter_key AS "adapterKey",
9197 provider_thread_id AS "providerThreadId",
9298 status,
99+ approval_policy AS "approvalPolicy",
100+ sandbox_mode AS "sandboxMode",
93101 last_seen_at AS "lastSeenAt",
94102 resume_cursor_json AS "resumeCursor",
95103 runtime_payload_json AS "runtimePayload"
@@ -110,6 +118,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 14 unmodified lines
110118 adapter_key AS "adapterKey",
111119 provider_thread_id AS "providerThreadId",
112120 status,
121+ approval_policy AS "approvalPolicy",
122+ sandbox_mode AS "sandboxMode",
113123 last_seen_at AS "lastSeenAt",
114124 resume_cursor_json AS "resumeCursor",
115125 runtime_payload_json AS "runtimePayload"
apps/server/src/persistence/Migrations.ts+2−0
@@ -19,6 +19,7 @@ import Migration0004 from "./Migrations/004_ProviderSessionRuntime.ts"; · 18 unmodified lines
1919 import Migration0005 from "./Migrations/005_Projections.ts";
2020 import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts";
2121 import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts";
22+import Migration0008 from "./Migrations/008_ProviderSessionRuntimeModeColumns.ts";
2223
2324 /**
2425 * Migration loader with all migrations defined inline.
@@ -38,6 +39,7 @@ const loader = Migrator.fromRecord({ · 13 unmodified lines
3839 "5_Projections": Migration0005,
3940 "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006,
4041 "7_ProjectionThreadMessageAttachments": Migration0007,
42+ "8_ProviderSessionRuntimeModeColumns": Migration0008,
4143 });
4244
4345 /**
apps/server/src/persistence/Migrations/008_ProviderSessionRuntimeModeColumns.ts+31−0
@@ -0,0 +1,31 @@
1+import * as SqlClient from "effect/unstable/sql/SqlClient";
2+import * as Effect from "effect/Effect";
3+
4+const DEFAULT_APPROVAL_POLICY = "never";
5+const DEFAULT_SANDBOX_MODE = "workspace-write";
6+
7+export default Effect.gen(function* () {
8+ const sql = yield* SqlClient.SqlClient;
9+
10+ yield* sql`
11+ ALTER TABLE provider_session_runtime
12+ ADD COLUMN approval_policy TEXT NOT NULL DEFAULT 'never'
13+ `;
14+
15+ yield* sql`
16+ ALTER TABLE provider_session_runtime
17+ ADD COLUMN sandbox_mode TEXT NOT NULL DEFAULT 'workspace-write'
18+ `;
19+
20+ yield* sql`
21+ UPDATE provider_session_runtime
22+ SET approval_policy = ${DEFAULT_APPROVAL_POLICY}
23+ WHERE approval_policy IS NULL
24+ `;
25+
26+ yield* sql`
27+ UPDATE provider_session_runtime
28+ SET sandbox_mode = ${DEFAULT_SANDBOX_MODE}
29+ WHERE sandbox_mode IS NULL
30+ `;
31+});
apps/server/src/persistence/Services/ProviderSessionRuntime.ts+4−0
@@ -7,6 +7,8 @@ · 6 unmodified lines
77 */
88 import {
99 IsoDateTime,
10+ ProviderApprovalPolicy,
11+ ProviderSandboxMode,
1012 ProviderSessionId,
1113 ProviderSessionRuntimeStatus,
1214 ProviderThreadId,
@@ -24,6 +26,8 @@ export const ProviderSessionRuntime = Schema.Struct({ · 11 unmodified lines
2426 adapterKey: Schema.String,
2527 providerThreadId: Schema.NullOr(ProviderThreadId),
2628 status: ProviderSessionRuntimeStatus,
29+ approvalPolicy: ProviderApprovalPolicy,
30+ sandboxMode: ProviderSandboxMode,
2731 lastSeenAt: IsoDateTime,
2832 resumeCursor: Schema.NullOr(Schema.Unknown),
2933 runtimePayload: Schema.NullOr(Schema.Unknown),
apps/server/src/provider/Layers/ProviderService.test.ts+12−0
@@ -327,6 +327,8 @@ it.effect( · 326 unmodified lines
327327 return yield* provider.startSession(asThreadId("thread-1"), {
328328 provider: "codex",
329329 cwd: "/tmp/project",
330+ approvalPolicy: "never",
331+ sandboxMode: "danger-full-access",
330332 });
331333 }).pipe(Effect.provide(firstProviderLayer));
332334
@@ -380,6 +382,8 @@ it.effect( · 47 unmodified lines
380382 provider: "codex",
381383 cwd: "/tmp/project",
382384 resumeThreadId: startedSession.threadId,
385+ approvalPolicy: "never",
386+ sandboxMode: "danger-full-access",
383387 },
384388 ],
385389 ]);
@@ -452,6 +456,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 66 unmodified lines
452456 const initial = yield* provider.startSession(asThreadId("thread-1"), {
453457 provider: "codex",
454458 cwd: "/tmp/project",
459+ approvalPolicy: "never",
460+ sandboxMode: "danger-full-access",
455461 });
456462 yield* routing.codex.stopSession(initial.sessionId);
457463 routing.codex.startSession.mockClear();
@@ -468,6 +474,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 10 unmodified lines
468474 provider: "codex",
469475 cwd: "/tmp/project",
470476 resumeThreadId: initial.threadId,
477+ approvalPolicy: "never",
478+ sandboxMode: "danger-full-access",
471479 },
472480 ],
473481 ]);
@@ -484,6 +492,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 10 unmodified lines
484492 const initial = yield* provider.startSession(asThreadId("thread-1"), {
485493 provider: "codex",
486494 cwd: "/tmp/project-send-turn",
495+ approvalPolicy: "never",
496+ sandboxMode: "danger-full-access",
487497 });
488498
489499 yield* provider.stopAll();
@@ -502,6 +512,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 12 unmodified lines
502512 provider: "codex",
503513 cwd: "/tmp/project-send-turn",
504514 resumeThreadId: initial.threadId,
515+ approvalPolicy: "never",
516+ sandboxMode: "danger-full-access",
505517 },
506518 ],
507519 ]);
apps/server/src/provider/Layers/ProviderService.ts+26−1
@@ -18,6 +18,8 @@ import { · 17 unmodified lines
1818 ProviderSendTurnInput,
1919 ProviderSessionStartInput,
2020 ProviderStopSessionInput,
21+ type ProviderApprovalPolicy,
22+ type ProviderSandboxMode,
2123 type ProviderRuntimeEvent,
2224 type ProviderSession,
2325 } from "@t3tools/contracts";
@@ -155,6 +157,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 131 unmodified lines
155157 session: ProviderSession,
156158 operation: string,
157159 threadId: ThreadId,
160+ options?: {
161+ readonly approvalPolicy?: ProviderApprovalPolicy;
162+ readonly sandboxMode?: ProviderSandboxMode;
163+ },
158164 ) =>
159165 Effect.gen(function* () {
160166 const providerThreadId = session.threadId;
@@ -171,6 +177,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 10 unmodified lines
171177 threadId,
172178 providerThreadId,
173179 status: toRuntimeStatus(session),
180+ ...(options?.approvalPolicy !== undefined
181+ ? { approvalPolicy: options.approvalPolicy }
182+ : {}),
183+ ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}),
174184 ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}),
175185 runtimePayload: toRuntimePayloadFromSession(session),
176186 });
@@ -243,6 +253,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 66 unmodified lines
243253 const resumeThreadId = input.binding.providerThreadId ?? undefined;
244254 const hasResumeCursor =
245255 input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined;
256+ const persistedApprovalPolicy = input.binding.approvalPolicy;
257+ const persistedSandboxMode = input.binding.sandboxMode;
246258 const existing =
247259 resumeThreadId === undefined
248260 ? undefined
@@ -252,6 +264,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 3 unmodified lines
252264 existing,
253265 `${input.operation}:upsertExistingSession`,
254266 input.binding.threadId,
267+ {
268+ ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
269+ ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
270+ },
255271 );
256272 yield* directory.upsert({
257273 sessionId: input.staleSessionId,
@@ -285,6 +301,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 27 unmodified lines
285301 ...(persistedCwd ? { cwd: persistedCwd } : {}),
286302 ...(resumeThreadId ? { resumeThreadId } : {}),
287303 ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}),
304+ ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
305+ ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
288306 });
289307 if (resumed.provider !== adapter.provider) {
290308 return yield* toValidationError(
@@ -297,6 +315,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 6 unmodified lines
297315 resumed,
298316 `${input.operation}:upsertRecoveredSession`,
299317 input.binding.threadId,
318+ {
319+ ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
320+ ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
321+ },
300322 );
301323
302324 yield* directory.upsert({
@@ -416,7 +438,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 113 unmodified lines
416438 );
417439 }
418440
419− yield* upsertSessionBinding(session, "ProviderService.startSession", threadId);
441+ yield* upsertSessionBinding(session, "ProviderService.startSession", threadId, {
442+ approvalPolicy: input.approvalPolicy,
443+ sandboxMode: input.sandboxMode,
444+ });
420445
421446 return session;
422447 });
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+6−0
@@ -118,6 +118,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 117 unmodified lines
118118 threadId,
119119 providerThreadId,
120120 status: "starting",
121+ approvalPolicy: "never",
122+ sandboxMode: "danger-full-access",
121123 resumeCursor: {
122124 resumeThreadId: "provider-thread-runtime",
123125 },
@@ -131,6 +133,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 7 unmodified lines
131133 sessionId,
132134 provider: "codex",
133135 status: "running",
136+ approvalPolicy: "on-request",
137+ sandboxMode: "workspace-write",
134138 runtimePayload: {
135139 activeTurnId: "turn-1",
136140 },
@@ -144,6 +148,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 7 unmodified lines
144148 assert.equal(runtime.value.threadId, threadId);
145149 assert.equal(runtime.value.providerThreadId, providerThreadId);
146150 assert.equal(runtime.value.status, "running");
151+ assert.equal(runtime.value.approvalPolicy, "on-request");
152+ assert.equal(runtime.value.sandboxMode, "workspace-write");
147153 assert.deepEqual(runtime.value.resumeCursor, {
148154 resumeThreadId: providerThreadId,
149155 });
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+16−1
@@ -1,4 +1,9 @@
1−import { ProviderSessionId, type ProviderKind } from "@t3tools/contracts";
1+import {
2+ ProviderSessionId,
3+ type ProviderApprovalPolicy,
4+ type ProviderKind,
5+ type ProviderSandboxMode,
6+} from "@t3tools/contracts";
27 import { Effect, Layer, Option } from "effect";
38
49 import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
@@ -13,6 +18,9 @@ import { · 8 unmodified lines
1318 type ProviderSessionDirectoryShape,
1419 } from "../Services/ProviderSessionDirectory.ts";
1520
21+const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never";
22+const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write";
23+
1624 function toPersistenceError(operation: string) {
1725 return (cause: unknown) =>
1826 new ProviderSessionDirectoryPersistenceError({
@@ -76,6 +84,8 @@ const makeProviderSessionDirectory = Effect.gen(function* () { · 57 unmodified lines
7684 adapterKey: value.adapterKey,
7785 providerThreadId: value.providerThreadId,
7886 status: value.status,
87+ approvalPolicy: value.approvalPolicy,
88+ sandboxMode: value.sandboxMode,
7989 resumeCursor: value.resumeCursor,
8090 runtimePayload: value.runtimePayload,
8191 }),
@@ -113,6 +123,11 @@ const makeProviderSessionDirectory = Effect.gen(function* () { · 31 unmodified lines
113123 ? binding.providerThreadId
114124 : (existingRuntime?.providerThreadId ?? null),
115125 status: binding.status ?? existingRuntime?.status ?? "running",
126+ approvalPolicy:
127+ binding.approvalPolicy ??
128+ existingRuntime?.approvalPolicy ??
129+ DEFAULT_APPROVAL_POLICY,
130+ sandboxMode: binding.sandboxMode ?? existingRuntime?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
116131 lastSeenAt: now,
117132 resumeCursor:
118133 binding.resumeCursor !== undefined
apps/server/src/provider/Services/ProviderSessionDirectory.ts+4−0
@@ -8,7 +8,9 @@ · 7 unmodified lines
88 * @module ProviderSessionDirectory
99 */
1010 import type {
11+ ProviderApprovalPolicy,
1112 ProviderKind,
13+ ProviderSandboxMode,
1214 ProviderSessionId,
1315 ProviderSessionRuntimeStatus,
1416 ProviderThreadId,
@@ -30,6 +32,8 @@ export interface ProviderSessionBinding { · 15 unmodified lines
3032 readonly adapterKey?: string;
3133 readonly providerThreadId?: ProviderThreadId | null;
3234 readonly status?: ProviderSessionRuntimeStatus;
35+ readonly approvalPolicy?: ProviderApprovalPolicy;
36+ readonly sandboxMode?: ProviderSandboxMode;
3337 readonly resumeCursor?: unknown | null;
3438 readonly runtimePayload?: unknown | null;
3539 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t