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
50 adapter_key,50 adapter_key,
51 provider_thread_id,51 provider_thread_id,
52 status,52 status,
53 approval_policy,
54 sandbox_mode,
53 last_seen_at,55 last_seen_at,
54 resume_cursor_json,56 resume_cursor_json,
55 runtime_payload_json57 runtime_payload_json
@@ -61,6 +63,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 5 unmodified lines
61 ${runtime.adapterKey},63 ${runtime.adapterKey},
62 ${runtime.providerThreadId},64 ${runtime.providerThreadId},
63 ${runtime.status},65 ${runtime.status},
66 ${runtime.approvalPolicy},
67 ${runtime.sandboxMode},
64 ${runtime.lastSeenAt},68 ${runtime.lastSeenAt},
65 ${runtime.resumeCursor},69 ${runtime.resumeCursor},
66 ${runtime.runtimePayload}70 ${runtime.runtimePayload}
@@ -72,6 +76,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 5 unmodified lines
72 adapter_key = excluded.adapter_key,76 adapter_key = excluded.adapter_key,
73 provider_thread_id = excluded.provider_thread_id,77 provider_thread_id = excluded.provider_thread_id,
74 status = excluded.status,78 status = excluded.status,
79 approval_policy = excluded.approval_policy,
80 sandbox_mode = excluded.sandbox_mode,
75 last_seen_at = excluded.last_seen_at,81 last_seen_at = excluded.last_seen_at,
76 resume_cursor_json = excluded.resume_cursor_json,82 resume_cursor_json = excluded.resume_cursor_json,
77 runtime_payload_json = excluded.runtime_payload_json83 runtime_payload_json = excluded.runtime_payload_json
@@ -90,6 +96,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 12 unmodified lines
90 adapter_key AS "adapterKey",96 adapter_key AS "adapterKey",
91 provider_thread_id AS "providerThreadId",97 provider_thread_id AS "providerThreadId",
92 status,98 status,
99 approval_policy AS "approvalPolicy",
100 sandbox_mode AS "sandboxMode",
93 last_seen_at AS "lastSeenAt",101 last_seen_at AS "lastSeenAt",
94 resume_cursor_json AS "resumeCursor",102 resume_cursor_json AS "resumeCursor",
95 runtime_payload_json AS "runtimePayload"103 runtime_payload_json AS "runtimePayload"
@@ -110,6 +118,8 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { · 14 unmodified lines
110 adapter_key AS "adapterKey",118 adapter_key AS "adapterKey",
111 provider_thread_id AS "providerThreadId",119 provider_thread_id AS "providerThreadId",
112 status,120 status,
121 approval_policy AS "approvalPolicy",
122 sandbox_mode AS "sandboxMode",
113 last_seen_at AS "lastSeenAt",123 last_seen_at AS "lastSeenAt",
114 resume_cursor_json AS "resumeCursor",124 resume_cursor_json AS "resumeCursor",
115 runtime_payload_json AS "runtimePayload"125 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
19import Migration0005 from "./Migrations/005_Projections.ts";19import Migration0005 from "./Migrations/005_Projections.ts";
20import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts";20import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts";
21import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts";21import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts";
22import Migration0008 from "./Migrations/008_ProviderSessionRuntimeModeColumns.ts";
22 23
23/**24/**
24 * Migration loader with all migrations defined inline.25 * Migration loader with all migrations defined inline.
@@ -38,6 +39,7 @@ const loader = Migrator.fromRecord({ · 13 unmodified lines
38 "5_Projections": Migration0005,39 "5_Projections": Migration0005,
39 "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006,40 "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006,
40 "7_ProjectionThreadMessageAttachments": Migration0007,41 "7_ProjectionThreadMessageAttachments": Migration0007,
42 "8_ProviderSessionRuntimeModeColumns": Migration0008,
41});43});
42 44
43/**45/**
apps/server/src/persistence/Migrations/008_ProviderSessionRuntimeModeColumns.ts+31−0
@@ -0,0 +1,31 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as Effect from "effect/Effect";
3
4const DEFAULT_APPROVAL_POLICY = "never";
5const DEFAULT_SANDBOX_MODE = "workspace-write";
6
7export 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
7 */7 */
8import {8import {
9 IsoDateTime,9 IsoDateTime,
10 ProviderApprovalPolicy,
11 ProviderSandboxMode,
10 ProviderSessionId,12 ProviderSessionId,
11 ProviderSessionRuntimeStatus,13 ProviderSessionRuntimeStatus,
12 ProviderThreadId,14 ProviderThreadId,
@@ -24,6 +26,8 @@ export const ProviderSessionRuntime = Schema.Struct({ · 11 unmodified lines
24 adapterKey: Schema.String,26 adapterKey: Schema.String,
25 providerThreadId: Schema.NullOr(ProviderThreadId),27 providerThreadId: Schema.NullOr(ProviderThreadId),
26 status: ProviderSessionRuntimeStatus,28 status: ProviderSessionRuntimeStatus,
29 approvalPolicy: ProviderApprovalPolicy,
30 sandboxMode: ProviderSandboxMode,
27 lastSeenAt: IsoDateTime,31 lastSeenAt: IsoDateTime,
28 resumeCursor: Schema.NullOr(Schema.Unknown),32 resumeCursor: Schema.NullOr(Schema.Unknown),
29 runtimePayload: Schema.NullOr(Schema.Unknown),33 runtimePayload: Schema.NullOr(Schema.Unknown),
apps/server/src/provider/Layers/ProviderService.test.ts+12−0
@@ -327,6 +327,8 @@ it.effect( · 326 unmodified lines
327 return yield* provider.startSession(asThreadId("thread-1"), {327 return yield* provider.startSession(asThreadId("thread-1"), {
328 provider: "codex",328 provider: "codex",
329 cwd: "/tmp/project",329 cwd: "/tmp/project",
330 approvalPolicy: "never",
331 sandboxMode: "danger-full-access",
330 });332 });
331 }).pipe(Effect.provide(firstProviderLayer));333 }).pipe(Effect.provide(firstProviderLayer));
332 334
@@ -380,6 +382,8 @@ it.effect( · 47 unmodified lines
380 provider: "codex",382 provider: "codex",
381 cwd: "/tmp/project",383 cwd: "/tmp/project",
382 resumeThreadId: startedSession.threadId,384 resumeThreadId: startedSession.threadId,
385 approvalPolicy: "never",
386 sandboxMode: "danger-full-access",
383 },387 },
384 ],388 ],
385 ]);389 ]);
@@ -452,6 +456,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 66 unmodified lines
452 const initial = yield* provider.startSession(asThreadId("thread-1"), {456 const initial = yield* provider.startSession(asThreadId("thread-1"), {
453 provider: "codex",457 provider: "codex",
454 cwd: "/tmp/project",458 cwd: "/tmp/project",
459 approvalPolicy: "never",
460 sandboxMode: "danger-full-access",
455 });461 });
456 yield* routing.codex.stopSession(initial.sessionId);462 yield* routing.codex.stopSession(initial.sessionId);
457 routing.codex.startSession.mockClear();463 routing.codex.startSession.mockClear();
@@ -468,6 +474,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 10 unmodified lines
468 provider: "codex",474 provider: "codex",
469 cwd: "/tmp/project",475 cwd: "/tmp/project",
470 resumeThreadId: initial.threadId,476 resumeThreadId: initial.threadId,
477 approvalPolicy: "never",
478 sandboxMode: "danger-full-access",
471 },479 },
472 ],480 ],
473 ]);481 ]);
@@ -484,6 +492,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 10 unmodified lines
484 const initial = yield* provider.startSession(asThreadId("thread-1"), {492 const initial = yield* provider.startSession(asThreadId("thread-1"), {
485 provider: "codex",493 provider: "codex",
486 cwd: "/tmp/project-send-turn",494 cwd: "/tmp/project-send-turn",
495 approvalPolicy: "never",
496 sandboxMode: "danger-full-access",
487 });497 });
488 498
489 yield* provider.stopAll();499 yield* provider.stopAll();
@@ -502,6 +512,8 @@ routing.layer("ProviderServiceLive routing", (it) => { · 12 unmodified lines
502 provider: "codex",512 provider: "codex",
503 cwd: "/tmp/project-send-turn",513 cwd: "/tmp/project-send-turn",
504 resumeThreadId: initial.threadId,514 resumeThreadId: initial.threadId,
515 approvalPolicy: "never",
516 sandboxMode: "danger-full-access",
505 },517 },
506 ],518 ],
507 ]);519 ]);
apps/server/src/provider/Layers/ProviderService.ts+26−1
@@ -18,6 +18,8 @@ import { · 17 unmodified lines
18 ProviderSendTurnInput,18 ProviderSendTurnInput,
19 ProviderSessionStartInput,19 ProviderSessionStartInput,
20 ProviderStopSessionInput,20 ProviderStopSessionInput,
21 type ProviderApprovalPolicy,
22 type ProviderSandboxMode,
21 type ProviderRuntimeEvent,23 type ProviderRuntimeEvent,
22 type ProviderSession,24 type ProviderSession,
23} from "@t3tools/contracts";25} from "@t3tools/contracts";
@@ -155,6 +157,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 131 unmodified lines
155 session: ProviderSession,157 session: ProviderSession,
156 operation: string,158 operation: string,
157 threadId: ThreadId,159 threadId: ThreadId,
160 options?: {
161 readonly approvalPolicy?: ProviderApprovalPolicy;
162 readonly sandboxMode?: ProviderSandboxMode;
163 },
158 ) =>164 ) =>
159 Effect.gen(function* () {165 Effect.gen(function* () {
160 const providerThreadId = session.threadId;166 const providerThreadId = session.threadId;
@@ -171,6 +177,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 10 unmodified lines
171 threadId,177 threadId,
172 providerThreadId,178 providerThreadId,
173 status: toRuntimeStatus(session),179 status: toRuntimeStatus(session),
180 ...(options?.approvalPolicy !== undefined
181 ? { approvalPolicy: options.approvalPolicy }
182 : {}),
183 ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}),
174 ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}),184 ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}),
175 runtimePayload: toRuntimePayloadFromSession(session),185 runtimePayload: toRuntimePayloadFromSession(session),
176 });186 });
@@ -243,6 +253,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 66 unmodified lines
243 const resumeThreadId = input.binding.providerThreadId ?? undefined;253 const resumeThreadId = input.binding.providerThreadId ?? undefined;
244 const hasResumeCursor =254 const hasResumeCursor =
245 input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined;255 input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined;
256 const persistedApprovalPolicy = input.binding.approvalPolicy;
257 const persistedSandboxMode = input.binding.sandboxMode;
246 const existing =258 const existing =
247 resumeThreadId === undefined259 resumeThreadId === undefined
248 ? undefined260 ? undefined
@@ -252,6 +264,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 3 unmodified lines
252 existing,264 existing,
253 `${input.operation}:upsertExistingSession`,265 `${input.operation}:upsertExistingSession`,
254 input.binding.threadId,266 input.binding.threadId,
267 {
268 ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
269 ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
270 },
255 );271 );
256 yield* directory.upsert({272 yield* directory.upsert({
257 sessionId: input.staleSessionId,273 sessionId: input.staleSessionId,
@@ -285,6 +301,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 27 unmodified lines
285 ...(persistedCwd ? { cwd: persistedCwd } : {}),301 ...(persistedCwd ? { cwd: persistedCwd } : {}),
286 ...(resumeThreadId ? { resumeThreadId } : {}),302 ...(resumeThreadId ? { resumeThreadId } : {}),
287 ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}),303 ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}),
304 ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
305 ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
288 });306 });
289 if (resumed.provider !== adapter.provider) {307 if (resumed.provider !== adapter.provider) {
290 return yield* toValidationError(308 return yield* toValidationError(
@@ -297,6 +315,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 6 unmodified lines
297 resumed,315 resumed,
298 `${input.operation}:upsertRecoveredSession`,316 `${input.operation}:upsertRecoveredSession`,
299 input.binding.threadId,317 input.binding.threadId,
318 {
319 ...(persistedApprovalPolicy ? { approvalPolicy: persistedApprovalPolicy } : {}),
320 ...(persistedSandboxMode ? { sandboxMode: persistedSandboxMode } : {}),
321 },
300 );322 );
301 323
302 yield* directory.upsert({324 yield* directory.upsert({
@@ -416,7 +438,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 113 unmodified lines
416 );438 );
417 }439 }
418 440
419 yield* upsertSessionBinding(session, "ProviderService.startSession", threadId);441 yield* upsertSessionBinding(session, "ProviderService.startSession", threadId, {
442 approvalPolicy: input.approvalPolicy,
443 sandboxMode: input.sandboxMode,
444 });
420 445
421 return session;446 return session;
422 });447 });
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+6−0
@@ -118,6 +118,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 117 unmodified lines
118 threadId,118 threadId,
119 providerThreadId,119 providerThreadId,
120 status: "starting",120 status: "starting",
121 approvalPolicy: "never",
122 sandboxMode: "danger-full-access",
121 resumeCursor: {123 resumeCursor: {
122 resumeThreadId: "provider-thread-runtime",124 resumeThreadId: "provider-thread-runtime",
123 },125 },
@@ -131,6 +133,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 7 unmodified lines
131 sessionId,133 sessionId,
132 provider: "codex",134 provider: "codex",
133 status: "running",135 status: "running",
136 approvalPolicy: "on-request",
137 sandboxMode: "workspace-write",
134 runtimePayload: {138 runtimePayload: {
135 activeTurnId: "turn-1",139 activeTurnId: "turn-1",
136 },140 },
@@ -144,6 +148,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL · 7 unmodified lines
144 assert.equal(runtime.value.threadId, threadId);148 assert.equal(runtime.value.threadId, threadId);
145 assert.equal(runtime.value.providerThreadId, providerThreadId);149 assert.equal(runtime.value.providerThreadId, providerThreadId);
146 assert.equal(runtime.value.status, "running");150 assert.equal(runtime.value.status, "running");
151 assert.equal(runtime.value.approvalPolicy, "on-request");
152 assert.equal(runtime.value.sandboxMode, "workspace-write");
147 assert.deepEqual(runtime.value.resumeCursor, {153 assert.deepEqual(runtime.value.resumeCursor, {
148 resumeThreadId: providerThreadId,154 resumeThreadId: providerThreadId,
149 });155 });
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+16−1
@@ -1,4 +1,9 @@
1import { ProviderSessionId, type ProviderKind } from "@t3tools/contracts";1import {
2 ProviderSessionId,
3 type ProviderApprovalPolicy,
4 type ProviderKind,
5 type ProviderSandboxMode,
6} from "@t3tools/contracts";
2import { Effect, Layer, Option } from "effect";7import { Effect, Layer, Option } from "effect";
3 8
4import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";9import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
@@ -13,6 +18,9 @@ import { · 8 unmodified lines
13 type ProviderSessionDirectoryShape,18 type ProviderSessionDirectoryShape,
14} from "../Services/ProviderSessionDirectory.ts";19} from "../Services/ProviderSessionDirectory.ts";
15 20
21const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never";
22const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write";
23
16function toPersistenceError(operation: string) {24function toPersistenceError(operation: string) {
17 return (cause: unknown) =>25 return (cause: unknown) =>
18 new ProviderSessionDirectoryPersistenceError({26 new ProviderSessionDirectoryPersistenceError({
@@ -76,6 +84,8 @@ const makeProviderSessionDirectory = Effect.gen(function* () { · 57 unmodified lines
76 adapterKey: value.adapterKey,84 adapterKey: value.adapterKey,
77 providerThreadId: value.providerThreadId,85 providerThreadId: value.providerThreadId,
78 status: value.status,86 status: value.status,
87 approvalPolicy: value.approvalPolicy,
88 sandboxMode: value.sandboxMode,
79 resumeCursor: value.resumeCursor,89 resumeCursor: value.resumeCursor,
80 runtimePayload: value.runtimePayload,90 runtimePayload: value.runtimePayload,
81 }),91 }),
@@ -113,6 +123,11 @@ const makeProviderSessionDirectory = Effect.gen(function* () { · 31 unmodified lines
113 ? binding.providerThreadId123 ? binding.providerThreadId
114 : (existingRuntime?.providerThreadId ?? null),124 : (existingRuntime?.providerThreadId ?? null),
115 status: binding.status ?? existingRuntime?.status ?? "running",125 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,
116 lastSeenAt: now,131 lastSeenAt: now,
117 resumeCursor:132 resumeCursor:
118 binding.resumeCursor !== undefined133 binding.resumeCursor !== undefined
apps/server/src/provider/Services/ProviderSessionDirectory.ts+4−0
@@ -8,7 +8,9 @@ · 7 unmodified lines
8 * @module ProviderSessionDirectory8 * @module ProviderSessionDirectory
9 */9 */
10import type {10import type {
11 ProviderApprovalPolicy,
11 ProviderKind,12 ProviderKind,
13 ProviderSandboxMode,
12 ProviderSessionId,14 ProviderSessionId,
13 ProviderSessionRuntimeStatus,15 ProviderSessionRuntimeStatus,
14 ProviderThreadId,16 ProviderThreadId,
@@ -30,6 +32,8 @@ export interface ProviderSessionBinding { · 15 unmodified lines
30 readonly adapterKey?: string;32 readonly adapterKey?: string;
31 readonly providerThreadId?: ProviderThreadId | null;33 readonly providerThreadId?: ProviderThreadId | null;
32 readonly status?: ProviderSessionRuntimeStatus;34 readonly status?: ProviderSessionRuntimeStatus;
35 readonly approvalPolicy?: ProviderApprovalPolicy;
36 readonly sandboxMode?: ProviderSandboxMode;
33 readonly resumeCursor?: unknown | null;37 readonly resumeCursor?: unknown | null;
34 readonly runtimePayload?: unknown | null;38 readonly runtimePayload?: unknown | null;
35}39}

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