History

Refactor control-plane runtime services

Rhys Sullivan committed 8 months ago

Commit 88b34681 parent 0cf170c33 files · +1602 −784Browse files

Unified · Split

33 files
  1. apps/executor/src/server/server.test.ts +1 −1
  2. packages/control-plane/src/api/local/http.ts +21 −10
  3. packages/control-plane/src/runtime/control-plane-runtime.test.ts +53 −30
  4. packages/control-plane/src/runtime/execution-service.ts +15 −5
  5. packages/control-plane/src/runtime/executor-tools.ts +14 −12
  6. packages/control-plane/src/runtime/index.ts +90 −75
  7. packages/control-plane/src/runtime/live-execution.ts +5 −0
  8. packages/control-plane/src/runtime/local-config.test.ts +4 −3
  9. packages/control-plane/src/runtime/local-config.ts +22 −23
  10. packages/control-plane/src/runtime/local-control-plane-store.ts +4 −42
  11. packages/control-plane/src/runtime/local-installation.test.ts +2 −1
  12. packages/control-plane/src/runtime/local-runtime-context.ts +13 −0
  13. packages/control-plane/src/runtime/local-source-artifacts.ts +10 −19
  14. packages/control-plane/src/runtime/local-storage.ts +85 −14
  15. packages/control-plane/src/runtime/local-tools.ts +88 −65
  16. packages/control-plane/src/runtime/local-workspace-state.ts +10 −16
  17. packages/control-plane/src/runtime/local-workspace-sync.ts +3 −2
  18. packages/control-plane/src/runtime/secret-material-providers.ts +108 −0
  19. packages/control-plane/src/runtime/source-adapters/google-discovery.ts +8 −0
  20. packages/control-plane/src/runtime/source-adapters/graphql.ts +2 −0
  21. packages/control-plane/src/runtime/source-adapters/mcp.ts +2 −0
  22. packages/control-plane/src/runtime/source-adapters/openapi.ts +2 −0
  23. packages/control-plane/src/runtime/source-adapters/types.ts +0 −2
  24. packages/control-plane/src/runtime/source-auth-material.test.ts +94 −88
  25. packages/control-plane/src/runtime/source-auth-material.ts +43 −2
  26. packages/control-plane/src/runtime/source-auth-service.ts +87 −90
  27. packages/control-plane/src/runtime/source-inspection.ts +5 −9
  28. packages/control-plane/src/runtime/source-materialization.ts +165 −43
  29. packages/control-plane/src/runtime/source-recipes-runtime.ts +255 −67
  30. packages/control-plane/src/runtime/source-store.ts +306 −55
  31. packages/control-plane/src/runtime/sources-operations.ts +22 −20
  32. packages/control-plane/src/runtime/store.ts +0 −5
  33. packages/control-plane/src/runtime/workspace-execution-environment.ts +63 −85
apps/executor/src/server/server.test.ts+1−1
@@ -171,7 +171,7 @@ const writeConfiguredLocalMcpSource = (input: { · 170 unmodified lines
171171 materialization,
172172 }),
173173 });
174− });
174+ }).pipe(Effect.provide(NodeFileSystem.layer));
175175
176176 const makeServer = createIsolatedLocalExecutorServer({
177177 port: 0,
packages/control-plane/src/api/local/http.ts+21−10
@@ -9,12 +9,14 @@ import { SecretMaterialIdSchema } from "#schema"; · 8 unmodified lines
99 import {
1010 getLocalInstallation,
1111 } from "../../runtime/local-operations";
12+import { requireRuntimeLocalWorkspace } from "../../runtime/local-runtime-context";
1213 import {
1314 ENV_SECRET_PROVIDER_ID,
1415 KEYCHAIN_SECRET_PROVIDER_ID,
1516 LOCAL_SECRET_PROVIDER_ID,
1617 parseSecretStoreProviderId,
1718 } from "../../runtime/secret-material-providers";
19+import { RuntimeSourceStoreService } from "../../runtime/source-store";
1820 import { ControlPlaneStore } from "../../runtime/store";
1921 import type {
2022 CreateSecretResult,
@@ -84,16 +86,25 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 63 unmodified lines
8486 .handle("config", () =>
8587 getInstanceConfig(),
8688 )
87− .handle("listSecrets", () =>
88− Effect.gen(function* () {
89− const store = yield* ControlPlaneStore;
90− const rows = yield* store.secretMaterials.listAll().pipe(
91− Effect.mapError(() => storageError("Failed listing secrets.")),
92− );
93− const linkedSourcesMap = yield* store.secretMaterials.listLinkedSources().pipe(
94− Effect.mapError(() => storageError("Failed loading linked sources.")),
95− );
96− return rows.map((row) => ({
89+ .handle("listSecrets", () =>
90+ Effect.gen(function* () {
91+ const store = yield* ControlPlaneStore;
92+ const sourceStore = yield* RuntimeSourceStoreService;
93+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace().pipe(
94+ Effect.mapError(() => storageError("Failed resolving local workspace.")),
95+ );
96+ const rows = yield* store.secretMaterials.listAll().pipe(
97+ Effect.mapError(() => storageError("Failed listing secrets.")),
98+ );
99+ const linkedSourcesMap = yield* sourceStore.listLinkedSecretSourcesInWorkspace(
100+ runtimeLocalWorkspace.installation.workspaceId,
101+ {
102+ actorAccountId: runtimeLocalWorkspace.installation.accountId,
103+ },
104+ ).pipe(
105+ Effect.mapError(() => storageError("Failed loading linked sources.")),
106+ );
107+ return rows.map((row) => ({
97108 ...row,
98109 linkedSources: linkedSourcesMap.get(row.id) ?? [],
99110 }));
packages/control-plane/src/runtime/control-plane-runtime.test.ts+53−30
@@ -1,8 +1,9 @@
11 import { createServer } from "node:http";
2−import { mkdtempSync, readFileSync } from "node:fs";
3−import { tmpdir } from "node:os";
2+import { readFileSync } from "node:fs";
43 import { join } from "node:path";
54
5+import { FileSystem } from "@effect/platform";
6+import { NodeFileSystem } from "@effect/platform-node";
67 import { describe, expect, it } from "@effect/vitest";
78 import { assertTrue } from "@effect/vitest/utils";
89 import * as Effect from "effect/Effect";
@@ -13,6 +14,7 @@ import type { AccountId } from "#schema"; · 4 unmodified lines
1314 import {
1415 ExecutionIdSchema,
1516 ExecutionInteractionIdSchema,
17+ type ExecutionInteraction,
1618 SecretMaterialIdSchema,
1719 SourceIdSchema,
1820 } from "#schema";
@@ -21,19 +23,24 @@ import type { ToolPath } from "@executor/codemode-core"; · 2 unmodified lines
2123 import {
2224 createControlPlaneRuntime,
2325 LiveExecutionManagerService,
26+ provideControlPlaneRuntime,
2427 } from "./index";
2528 import { createSourceFromPayload } from "./source-definitions";
2629 import { decodeSourceCredentialSelectionContent } from "./source-credential-interactions";
2730 import { persistSource } from "./source-store";
2831 import { withControlPlaneClient } from "./test-http-client";
2932
30−const makeRuntime = Effect.acquireRelease(
31− createControlPlaneRuntime({
32− localDataDir: ":memory:",
33− workspaceRoot: mkdtempSync(join(tmpdir(), "executor-control-plane-runtime-")),
34− }),
35− (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
36−);
33+const makeRuntime = Effect.gen(function* () {
34+ const fs = yield* FileSystem.FileSystem;
35+ const workspaceRoot = yield* fs.makeTempDirectoryScoped({
36+ prefix: "executor-control-plane-runtime-",
37+ });
38+
39+ return yield* Effect.acquireRelease(
40+ createControlPlaneRuntime({ workspaceRoot }),
41+ (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
42+ );
43+}).pipe(Effect.provide(NodeFileSystem.layer));
3744
3845 type OpenApiSpecServer = {
3946 baseUrl: string;
@@ -121,10 +128,12 @@ const expectLeft = <A, E>(effect: Effect.Effect<A, E, never>) => · 81 unmodified lines
121128 describe("control-plane-runtime", () => {
122129 it.scoped("writes local source changes through executor.jsonc", () =>
123130 Effect.gen(function* () {
124− const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-local-config-runtime-"));
131+ const fs = yield* FileSystem.FileSystem;
132+ const workspaceRoot = yield* fs.makeTempDirectoryScoped({
133+ prefix: "executor-local-config-runtime-",
134+ });
125135 const runtime = yield* Effect.acquireRelease(
126136 createControlPlaneRuntime({
127− localDataDir: ":memory:",
128137 workspaceRoot,
129138 }),
130139 (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie),
@@ -174,7 +183,7 @@ describe("control-plane-runtime", () => { · 43 unmodified lines
174183 sources?: Record<string, unknown>;
175184 };
176185 expect(removedConfig.sources?.github).toBeUndefined();
177− }),
186+ }).pipe(Effect.provide(NodeFileSystem.layer)),
178187 60_000,
179188 );
180189
@@ -228,10 +237,7 @@ describe("control-plane-runtime", () => { · 47 unmodified lines
228237 }).pipe(Effect.orDie);
229238 yield* persistSource(runtime.persistence.rows, localSource, {
230239 actorAccountId: installation.accountId,
231− }).pipe(
232− Effect.provide(runtime.runtimeLayer),
233− Effect.orDie,
234− );
240+ }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie);
235241
236242 const interactionFiber = yield* Effect.gen(function* () {
237243 const liveExecutionManager = yield* LiveExecutionManagerService;
@@ -260,14 +266,24 @@ describe("control-plane-runtime", () => { · 22 unmodified lines
260266 },
261267 });
262268 }).pipe(
263− Effect.provide(runtime.runtimeLayer),
269+ (effect) => provideControlPlaneRuntime(effect, runtime),
264270 Effect.fork,
265271 );
266272
267− yield* Effect.yieldNow();
268−
269− const pendingInteraction = yield* runtime.persistence.rows.executionInteractions
270− .getPendingByExecutionId(executionId);
273+ const waitForPendingInteraction = (
274+ remaining: number,
275+ ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> =>
276+ runtime.persistence.rows.executionInteractions.getPendingByExecutionId(executionId).pipe(
277+ Effect.flatMap((pendingInteraction) =>
278+ Option.isSome(pendingInteraction) || remaining <= 0
279+ ? Effect.succeed(pendingInteraction)
280+ : Effect.yieldNow().pipe(
281+ Effect.zipRight(waitForPendingInteraction(remaining - 1)),
282+ ),
283+ ),
284+ );
285+
286+ const pendingInteraction = yield* waitForPendingInteraction(20);
271287 assertTrue(Option.isSome(pendingInteraction));
272288 expect(pendingInteraction.value.id).toBe(interactionId);
273289
@@ -391,10 +407,7 @@ describe("control-plane-runtime", () => { · 117 unmodified lines
391407 }).pipe(Effect.orDie);
392408 yield* persistSource(runtime.persistence.rows, localSource, {
393409 actorAccountId: installation.accountId,
394− }).pipe(
395− Effect.provide(runtime.runtimeLayer),
396− Effect.orDie,
397− );
410+ }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie);
398411
399412 const interactionFiber = yield* Effect.gen(function* () {
400413 const liveExecutionManager = yield* LiveExecutionManagerService;
@@ -423,14 +436,24 @@ describe("control-plane-runtime", () => { · 22 unmodified lines
423436 },
424437 });
425438 }).pipe(
426− Effect.provide(runtime.runtimeLayer),
439+ (effect) => provideControlPlaneRuntime(effect, runtime),
427440 Effect.fork,
428441 );
429442
430− yield* Effect.yieldNow();
431−
432− const pendingInteraction = yield* runtime.persistence.rows.executionInteractions
433− .getPendingByExecutionId(executionId);
443+ const waitForPendingInteraction = (
444+ remaining: number,
445+ ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> =>
446+ runtime.persistence.rows.executionInteractions.getPendingByExecutionId(executionId).pipe(
447+ Effect.flatMap((pendingInteraction) =>
448+ Option.isSome(pendingInteraction) || remaining <= 0
449+ ? Effect.succeed(pendingInteraction)
450+ : Effect.yieldNow().pipe(
451+ Effect.zipRight(waitForPendingInteraction(remaining - 1)),
452+ ),
453+ ),
454+ );
455+
456+ const pendingInteraction = yield* waitForPendingInteraction(20);
434457 assertTrue(Option.isSome(pendingInteraction));
435458
436459 const submittedPage = yield* withControlPlaneClient(
packages/control-plane/src/runtime/execution-service.ts+15−5
@@ -34,6 +34,10 @@ import { · 33 unmodified lines
3434 sanitizePersistedElicitationResponse,
3535 type LiveExecutionManager,
3636 } from "./live-execution";
37+import {
38+ getRuntimeLocalWorkspaceOption,
39+ provideOptionalRuntimeLocalWorkspace,
40+} from "./local-runtime-context";
3741 import {
3842 asOperationErrors,
3943 operationErrors,
@@ -658,16 +662,22 @@ const forkExecutionAttemptWithDependencies = ( · 618 unmodified lines
658662 execution: Execution,
659663 interactionMode: InteractionMode,
660664 ) =>
661− Effect.sync(() => {
662− Effect.runFork(
663− runExecutionAttemptWithDependencies(
665+ Effect.gen(function* () {
666+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
667+
668+ yield* Effect.sync(() => {
669+ const effect = runExecutionAttemptWithDependencies(
664670 store,
665671 executionResolver,
666672 liveExecutionManager,
667673 execution,
668674 interactionMode,
669− ),
670− );
675+ );
676+
677+ Effect.runFork(
678+ provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace),
679+ );
680+ });
671681 });
672682
673683 const submitExecutionInteractionResponseWithDependencies = (
packages/control-plane/src/runtime/executor-tools.ts+14−12
@@ -18,33 +18,35 @@ import { · 17 unmodified lines
1818 import * as Effect from "effect/Effect";
1919 import * as Cause from "effect/Cause";
2020 import * as Exit from "effect/Exit";
21−import * as Layer from "effect/Layer";
2221 import * as Schema from "effect/Schema";
2322 import type { RuntimeLocalWorkspaceState } from "./local-runtime-context";
2423 import {
25− InstallationStore,
2624 type LocalStorageServices,
2725 LocalInstallationStore,
2826 LocalSourceArtifactStore,
2927 LocalWorkspaceConfigStore,
3028 LocalWorkspaceStateStore,
31− SourceArtifactStore,
32− WorkspaceConfigStore,
33− WorkspaceStateStore,
29+ makeLocalStorageLayer,
3430 } from "./local-storage";
31+import { provideOptionalRuntimeLocalWorkspace } from "./local-runtime-context";
3532
3633 /** Run an Effect as a Promise, preserving the original error (not FiberFailure). */
3734 const runEffect = async <A>(
3835 effect: Effect.Effect<A, unknown, LocalStorageServices>,
39− _runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null = null,
36+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null = null,
4037 ): Promise<A> => {
41− const baseLayer = Layer.mergeAll(
42− Layer.succeed(InstallationStore, LocalInstallationStore),
43− Layer.succeed(WorkspaceConfigStore, LocalWorkspaceConfigStore),
44− Layer.succeed(WorkspaceStateStore, LocalWorkspaceStateStore),
45− Layer.succeed(SourceArtifactStore, LocalSourceArtifactStore),
38+ const baseLayer = makeLocalStorageLayer({
39+ installationStore: LocalInstallationStore,
40+ workspaceConfigStore: LocalWorkspaceConfigStore,
41+ workspaceStateStore: LocalWorkspaceStateStore,
42+ sourceArtifactStore: LocalSourceArtifactStore,
43+ });
44+ const exit = await Effect.runPromiseExit(
45+ provideOptionalRuntimeLocalWorkspace(
46+ effect.pipe(Effect.provide(baseLayer)),
47+ runtimeLocalWorkspace,
48+ ),
4649 );
47− const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(baseLayer)));
4850 if (Exit.isSuccess(exit)) return exit.value;
4951 throw Cause.squash(exit.cause);
5052 };
packages/control-plane/src/runtime/index.ts+90−75
@@ -1,7 +1,9 @@
11 import * as Effect from "effect/Effect";
22 import * as Exit from "effect/Exit";
33 import * as Layer from "effect/Layer";
4+import * as ManagedRuntime from "effect/ManagedRuntime";
45 import * as Scope from "effect/Scope";
6+import { NodeFileSystem } from "@effect/platform-node";
57
68 import { type ControlPlaneApiRuntimeContext } from "#api";
79 import type { LocalInstallation } from "#schema";
@@ -9,7 +11,6 @@ import type { LocalInstallation } from "#schema"; · 1 unmodified line
911 import { type ResolveExecutionEnvironment } from "./execution-state";
1012 import {
1113 createLiveExecutionManager,
12− LiveExecutionManagerLive,
1314 LiveExecutionManagerService,
1415 } from "./live-execution";
1516 import {
@@ -20,36 +21,28 @@ import { · 4 unmodified lines
2021 resolveLocalWorkspaceContext,
2122 } from "./local-config";
2223 import {
23− InstallationStore,
24+ LocalStorageLive,
2425 LocalInstallationStore,
25− LocalInstallationStoreLive,
26− LocalSourceArtifactStore,
27− LocalSourceArtifactStoreLive,
28− SourceArtifactStore,
2926 LocalWorkspaceConfigStore,
30− LocalWorkspaceConfigStoreLive,
31− LocalWorkspaceStateStore,
32− LocalWorkspaceStateStoreLive,
33− WorkspaceConfigStore,
34− WorkspaceStateStore,
3527 } from "./local-storage";
3628 import {
3729 type RuntimeLocalWorkspaceState,
38− RuntimeLocalWorkspaceService,
30+ RuntimeLocalWorkspaceLive,
3931 } from "./local-runtime-context";
32+import { LocalToolRuntimeLoaderLive } from "./local-tools";
4033 import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";
4134 import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
35+import { RuntimeSourceStoreLive } from "./source-store";
36+import { RuntimeSourceRecipeStoreLive } from "./source-recipes-runtime";
37+import { RuntimeSourceAuthMaterialLive } from "./source-auth-material";
38+import { RuntimeSourceMaterializationLive } from "./source-materialization";
4239 import {
43− createRuntimeSourceAuthService,
4440 RuntimeSourceAuthServiceLive,
45− RuntimeSourceAuthServiceTag,
4641 } from "./source-auth-service";
4742 import type { ResolveSecretMaterial } from "./secret-material-providers";
48−import { createDefaultSecretMaterialResolver } from "./secret-material-providers";
43+import { SecretMaterialLive } from "./secret-material-providers";
4944 import {
50− createWorkspaceExecutionEnvironmentResolver,
5145 RuntimeExecutionResolverLive,
52− RuntimeExecutionResolverService,
5346 } from "./workspace-execution-environment";
5447
5548 export * from "./execution-state";
@@ -101,34 +94,84 @@ export const createRuntimeControlPlaneLayer = ( · 45 unmodified lines
10194 input: RuntimeControlPlaneOptions & {
10295 store: ControlPlaneStoreShape;
10396 localWorkspaceState: RuntimeLocalWorkspaceState;
97+ liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;
10498 },
10599 ) => {
100+ const platformLayer = NodeFileSystem.layer;
101+ const storageLayer = LocalStorageLive.pipe(
102+ Layer.provide(platformLayer),
103+ );
104+ const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe(
105+ Layer.provide(platformLayer),
106+ );
107+
106108 const baseLayer = Layer.mergeAll(
109+ platformLayer,
107110 Layer.succeed(ControlPlaneStore, input.store),
108− Layer.succeed(RuntimeLocalWorkspaceService, input.localWorkspaceState),
109− LocalInstallationStoreLive,
110− LocalWorkspaceConfigStoreLive,
111− LocalWorkspaceStateStoreLive,
112− LocalSourceArtifactStoreLive,
113− LiveExecutionManagerLive,
111+ RuntimeLocalWorkspaceLive(input.localWorkspaceState),
112+ storageLayer,
113+ Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),
114+ );
115+
116+ const secretMaterialLayer = SecretMaterialLive({
117+ resolveSecretMaterial: input.resolveSecretMaterial,
118+ }).pipe(Layer.provide(baseLayer));
119+
120+ const sourceStoreLayer = RuntimeSourceStoreLive.pipe(
121+ Layer.provide(baseLayer),
122+ );
123+
124+ const sourceRecipeStoreLayer = RuntimeSourceRecipeStoreLive.pipe(
125+ Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)),
126+ );
127+
128+ const sourceAuthMaterialLayer = RuntimeSourceAuthMaterialLive.pipe(
129+ Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)),
130+ );
131+
132+ const sourceMaterializationLayer = RuntimeSourceMaterializationLive.pipe(
133+ Layer.provide(
134+ Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer),
135+ ),
114136 );
115137
116138 const sourceAuthLayer = RuntimeSourceAuthServiceLive({
117139 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
118− localConfig: input.localWorkspaceState.loadedConfig.config,
119− workspaceRoot: input.localWorkspaceState.context.workspaceRoot,
120− localWorkspaceState: input.localWorkspaceState,
121− }).pipe(Layer.provide(baseLayer));
140+ }).pipe(
141+ Layer.provide(
142+ Layer.mergeAll(
143+ baseLayer,
144+ secretMaterialLayer,
145+ sourceStoreLayer,
146+ sourceMaterializationLayer,
147+ ),
148+ ),
149+ );
122150
123151 const executionResolverLayer = RuntimeExecutionResolverLive({
124152 executionResolver: input.executionResolver,
125− resolveSecretMaterial: input.resolveSecretMaterial,
126153 }).pipe(
127− Layer.provide(Layer.mergeAll(baseLayer, sourceAuthLayer)),
154+ Layer.provide(
155+ Layer.mergeAll(
156+ baseLayer,
157+ secretMaterialLayer,
158+ sourceAuthMaterialLayer,
159+ sourceMaterializationLayer,
160+ sourceAuthLayer,
161+ sourceRecipeStoreLayer,
162+ localToolRuntimeLayer,
163+ ),
164+ ),
128165 );
129166
130167 return Layer.mergeAll(
131168 baseLayer,
169+ secretMaterialLayer,
170+ sourceStoreLayer,
171+ sourceAuthMaterialLayer,
172+ sourceMaterializationLayer,
173+ sourceRecipeStoreLayer,
174+ localToolRuntimeLayer,
132175 sourceAuthLayer,
133176 executionResolverLayer,
134177 ) as RuntimeControlPlaneLayer;
@@ -137,12 +180,19 @@ export const createRuntimeControlPlaneLayer = ( · 2 unmodified lines
137180 export type ControlPlaneRuntime = {
138181 persistence: LocalControlPlanePersistence;
139182 localInstallation: LocalInstallation;
183+ managedRuntime: ManagedRuntime.ManagedRuntime<ControlPlaneApiRuntimeContext, never>;
140184 runtimeLayer: RuntimeControlPlaneLayer;
141185 close: () => Promise<void>;
142186 };
143187
144188 export type CreateControlPlaneRuntimeOptions = RuntimeControlPlaneOptions;
145189
190+export const provideControlPlaneRuntime = <A, E, R>(
191+ effect: Effect.Effect<A, E, R>,
192+ runtime: ControlPlaneRuntime,
193+): Effect.Effect<A, E | never, Exclude<R, ControlPlaneApiRuntimeContext>> =>
194+ effect.pipe(Effect.provide(runtime.managedRuntime));
195+
146196 export const createControlPlaneRuntime = (
147197 options: CreateControlPlaneRuntimeOptions,
148198 ): Effect.Effect<ControlPlaneRuntime, Error> =>
@@ -160,8 +210,6 @@ export const createControlPlaneRuntime = ( · 11 unmodified lines
160210
161211 const installationStore = LocalInstallationStore;
162212 const workspaceConfigStore = LocalWorkspaceConfigStore;
163− const workspaceStateStore = LocalWorkspaceStateStore;
164− const sourceArtifactStore = LocalSourceArtifactStore;
165213
166214 const localInstallation = yield* installationStore.getOrProvision({
167215 context: localWorkspaceContext,
@@ -194,34 +242,6 @@ export const createControlPlaneRuntime = ( · 26 unmodified lines
194242 ),
195243 );
196244
197− const resolveSecretMaterial =
198− options.resolveSecretMaterial ??
199− createDefaultSecretMaterialResolver({
200− rows,
201− localConfig: effectiveLocalConfig,
202− workspaceRoot: localWorkspaceContext.workspaceRoot,
203− });
204−
205− const liveExecutionManager = createLiveExecutionManager();
206− const sourceAuthService = createRuntimeSourceAuthService({
207− rows,
208− liveExecutionManager,
209− getLocalServerBaseUrl: options.getLocalServerBaseUrl,
210− localConfig: effectiveLocalConfig,
211− workspaceRoot: localWorkspaceContext.workspaceRoot,
212− localWorkspaceState: {
213− context: localWorkspaceContext,
214− installation: {
215− workspaceId: localInstallation.workspaceId,
216− accountId: localInstallation.accountId,
217− },
218− loadedConfig: {
219− ...loadedLocalConfig,
220− config: effectiveLocalConfig,
221− },
222− },
223− });
224−
225245 const runtimeLocalWorkspaceState: RuntimeLocalWorkspaceState = {
226246 context: localWorkspaceContext,
227247 installation: {
@@ -233,30 +253,25 @@ export const createControlPlaneRuntime = ( · 5 unmodified lines
233253 config: effectiveLocalConfig,
234254 },
235255 };
236−
237− const executionResolver =
238− options.executionResolver ??
239− createWorkspaceExecutionEnvironmentResolver({
240− rows,
241− sourceAuthService,
242− resolveSecretMaterial,
243− workspaceConfigStore,
244− workspaceStateStore,
245− sourceArtifactStore,
246− });
256+ const liveExecutionManager = createLiveExecutionManager();
247257
248258 const concreteRuntimeLayer = createRuntimeControlPlaneLayer({
249259 ...options,
250260 store: rows,
251261 localWorkspaceState: runtimeLocalWorkspaceState,
252− resolveSecretMaterial,
253− executionResolver,
262+ liveExecutionManager,
254263 });
264+ const managedRuntime = ManagedRuntime.make(concreteRuntimeLayer);
265+ const runtime = yield* managedRuntime.runtimeEffect;
266+ const runtimeLayer = Layer.succeedContext(
267+ runtime.context,
268+ ) as RuntimeControlPlaneLayer;
255269
256270 return {
257271 persistence,
258272 localInstallation,
259− runtimeLayer: concreteRuntimeLayer,
260− close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}),
273+ managedRuntime,
274+ runtimeLayer: runtimeLayer as RuntimeControlPlaneLayer,
275+ close: () => managedRuntime.dispose().catch(() => {}),
261276 };
262− });
277+ }).pipe(Effect.provide(NodeFileSystem.layer));
packages/control-plane/src/runtime/live-execution.ts+5−0
@@ -15,6 +15,7 @@ import * as Layer from "effect/Layer"; · 14 unmodified lines
1515 import type { ControlPlaneStoreShape } from "./store";
1616
1717 type VisibleExecutionState =
18+ | "running"
1819 | "waiting_for_interaction"
1920 | "completed"
2021 | "failed";
@@ -209,6 +210,10 @@ export const createLiveExecutionManager = () => { · 188 unmodified lines
209210 status: "running",
210211 updatedAt: resolvedAt,
211212 });
213+ yield* publishState({
214+ executionId,
215+ state: "running",
216+ });
212217
213218 run.currentInteraction = null;
214219 return resolved;
packages/control-plane/src/runtime/local-config.test.ts+4−3
@@ -2,6 +2,7 @@ import { mkdtempSync } from "node:fs"; · 1 unmodified line
22 import { mkdir, writeFile } from "node:fs/promises";
33 import { tmpdir } from "node:os";
44 import { join } from "node:path";
5+import { NodeFileSystem } from "@effect/platform-node";
56 import { describe, expect, it } from "@effect/vitest";
67 import * as Effect from "effect/Effect";
78
@@ -50,7 +51,7 @@ describe("local-config", () => { · 42 unmodified lines
5051 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(
5152 "https://api.github.com",
5253 );
53− }),
54+ }).pipe(Effect.provide(NodeFileSystem.layer)),
5455 );
5556
5657 it.effect("reports jsonc syntax errors with line and column details", () =>
@@ -81,7 +82,7 @@ describe("local-config", () => { · 24 unmodified lines
8182
8283 expect(failure.message).toContain("Invalid executor config");
8384 expect(failure.message).toContain("line 5, column 7");
84− }),
85+ }).pipe(Effect.provide(NodeFileSystem.layer)),
8586 );
8687
8788 it("uses platform-standard home config candidates", () => {
@@ -123,6 +124,6 @@ describe("local-config", () => { · 35 unmodified lines
123124 });
124125
125126 expect(resolvedPath).toBe(join(legacyConfigDirectory, "executor.jsonc"));
126− }),
127+ }).pipe(Effect.provide(NodeFileSystem.layer)),
127128 );
128129 });
packages/control-plane/src/runtime/local-config.ts+22−23
@@ -1,7 +1,6 @@
11 import { homedir } from "node:os";
22 import { basename, dirname, isAbsolute, join, resolve } from "node:path";
33 import { FileSystem } from "@effect/platform";
4−import { NodeFileSystem } from "@effect/platform-node";
54 import {
65 type ParseError as JsoncParseError,
76 parse as parseJsoncDocument,
@@ -30,15 +29,6 @@ const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json"; · 22 unmodified lines
3029 const PROJECT_CONFIG_DIRECTORY = ".executor";
3130 const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR";
3231
33−const provideNodeFileSystem = <A, E, R>(
34− effect: Effect.Effect<A, E, R | FileSystem.FileSystem>,
35−): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> =>
36− effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
37− A,
38− E,
39− Exclude<R, FileSystem.FileSystem>
40− >;
41−
4232 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
4333 new LocalFileSystemError({
4434 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
@@ -114,8 +104,8 @@ export const resolveHomeConfigPath = (input: { · 69 unmodified lines
114104 env?: NodeJS.ProcessEnv;
115105 platform?: NodeJS.Platform;
116106 homeDirectory?: string;
117−} = {}): Effect.Effect<string, LocalFileSystemError> =>
118− provideNodeFileSystem(Effect.gen(function* () {
107+} = {}): Effect.Effect<string, LocalFileSystemError, FileSystem.FileSystem> =>
108+ Effect.gen(function* () {
119109 const fs = yield* FileSystem.FileSystem;
120110 const candidates = resolveDefaultHomeConfigCandidates(input);
121111
@@ -129,7 +119,7 @@ export const resolveHomeConfigPath = (input: { · 7 unmodified lines
129119 }
130120
131121 return candidates[0]!;
132− }));
122+ });
133123
134124 const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {
135125 const lines = content.split("\n");
@@ -338,8 +328,12 @@ export type LoadedLocalExecutorConfig = { · 202 unmodified lines
338328 export const resolveLocalWorkspaceContext = (input: {
339329 cwd?: string;
340330 workspaceRoot?: string;
341−} = {}): Effect.Effect<ResolvedLocalWorkspaceContext, LocalFileSystemError> =>
342− provideNodeFileSystem(Effect.gen(function* () {
331+} = {}): Effect.Effect<
332+ ResolvedLocalWorkspaceContext,
333+ LocalFileSystemError,
334+ FileSystem.FileSystem
335+> =>
336+ Effect.gen(function* () {
343337 const cwd = resolve(input.cwd ?? process.cwd());
344338 const workspaceRoot = resolve(
345339 input.workspaceRoot ?? (yield* resolveWorkspaceRootFromCwdEffect(cwd)),
@@ -358,12 +352,16 @@ export const resolveLocalWorkspaceContext = (input: { · 12 unmodified lines
358352 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),
359353 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),
360354 };
361− }));
355+ });
362356
363357 export const readOptionalLocalExecutorConfig = (
364358 path: string,
365−): Effect.Effect<LocalExecutorConfig | null, LocalFileSystemError | LocalExecutorConfigDecodeError> =>
366− provideNodeFileSystem(Effect.gen(function* () {
359+): Effect.Effect<
360+ LocalExecutorConfig | null,
361+ LocalFileSystemError | LocalExecutorConfigDecodeError,
362+ FileSystem.FileSystem
363+> =>
364+ Effect.gen(function* () {
367365 const fs = yield* FileSystem.FileSystem;
368366 const exists = yield* fs.exists(path).pipe(
369367 Effect.mapError(mapFileSystemError(path, "check config path")),
@@ -386,13 +384,14 @@ export const readOptionalLocalExecutorConfig = ( · 16 unmodified lines
386384 details: unknownLocalErrorDetails(cause),
387385 }),
388386 });
389− }));
387+ });
390388
391389 export const loadLocalExecutorConfig = (
392390 context: ResolvedLocalWorkspaceContext,
393391 ): Effect.Effect<
394392 LoadedLocalExecutorConfig,
395− LocalFileSystemError | LocalExecutorConfigDecodeError
393+ LocalFileSystemError | LocalExecutorConfigDecodeError,
394+ FileSystem.FileSystem
396395 > =>
397396 Effect.gen(function* () {
398397 const [homeConfig, projectConfig] = yield* Effect.all([
@@ -415,8 +414,8 @@ export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string = · 16 unmodified lines
415414 export const writeProjectLocalExecutorConfig = (input: {
416415 context: ResolvedLocalWorkspaceContext;
417416 config: LocalExecutorConfig;
418−}): Effect.Effect<void, LocalFileSystemError> =>
419− provideNodeFileSystem(Effect.gen(function* () {
417+}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
418+ Effect.gen(function* () {
420419 const fs = yield* FileSystem.FileSystem;
421420 yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe(
422421 Effect.mapError(mapFileSystemError(input.context.configDirectory, "create config directory")),
@@ -427,7 +426,7 @@ export const writeProjectLocalExecutorConfig = (input: { · 4 unmodified lines
427426 ).pipe(
428427 Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")),
429428 );
430− }));
429+ });
431430
432431 export const resolveConfigRelativePath = (input: {
433432 path: string;
packages/control-plane/src/runtime/local-control-plane-store.ts+4−42
@@ -25,9 +25,7 @@ import * as Effect from "effect/Effect"; · 24 unmodified lines
2525 import * as Option from "effect/Option";
2626 import * as Schema from "effect/Schema";
2727
28−import { authArtifactSecretMaterialRefs } from "./auth-artifacts";
2928 import type { ResolvedLocalWorkspaceContext } from "./local-config";
30−import { loadLocalExecutorConfig } from "./local-config";
3129 import {
3230 LocalFileSystemError,
3331 unknownLocalErrorDetails,
@@ -303,7 +301,10 @@ const createStateManager = (context: ResolvedLocalWorkspaceContext) => { · 269 unmodified lines
303301 operation: (state: LocalControlPlaneState) => A | Promise<A>,
304302 ): Effect.Effect<A, LocalFileSystemError> =>
305303 Effect.tryPromise({
306− try: async () => operation(cloneValue(await ensureLoaded())),
304+ try: async () => {
305+ await mutationQueue;
306+ return operation(cloneValue(await ensureLoaded()));
307+ },
307308 catch: mapFileSystemError(
308309 localControlPlaneStatePath(context),
309310 "read control plane state",
@@ -772,45 +773,6 @@ export const createLocalControlPlaneStore = ( · 462 unmodified lines
772773 value: nextMaterials.length !== state.secretMaterials.length,
773774 } satisfies StateMutationResult<boolean>;
774775 }),
775−
776− listLinkedSources: () =>
777− Effect.gen(function* () {
778− const [state, loadedConfig] = yield* Effect.all([
779− stateManager.read((current) => current),
780− loadLocalExecutorConfig(context),
781− ]);
782−
783− const sourceNames = new Map<string, string>();
784− for (const [sourceId, source] of Object.entries(
785− loadedConfig.config?.sources ?? {},
786− )) {
787− sourceNames.set(sourceId, source.name ?? sourceId);
788− }
789−
790− const materialIds = new Set(
791− state.secretMaterials.map((material) => String(material.id)),
792− );
793− const linkedSources = new Map<string, Array<{ sourceId: string; sourceName: string }>>();
794−
795− for (const artifact of state.authArtifacts) {
796− for (const ref of authArtifactSecretMaterialRefs(artifact)) {
797− if (!materialIds.has(ref.handle)) {
798− continue;
799− }
800−
801− const existing = linkedSources.get(ref.handle) ?? [];
802− if (!existing.some((link) => link.sourceId === artifact.sourceId)) {
803− existing.push({
804− sourceId: artifact.sourceId,
805− sourceName: sourceNames.get(artifact.sourceId) ?? artifact.sourceId,
806− });
807− linkedSources.set(ref.handle, existing);
808− }
809− }
810− }
811−
812− return linkedSources;
813− }),
814776 },
815777
816778 executions: {
packages/control-plane/src/runtime/local-installation.test.ts+2−1
@@ -1,6 +1,7 @@
11 import { existsSync, mkdtempSync } from "node:fs";
22 import { tmpdir } from "node:os";
33 import { join } from "node:path";
4+import { NodeFileSystem } from "@effect/platform-node";
45 import { describe, expect, it } from "@effect/vitest";
56 import * as Effect from "effect/Effect";
67
@@ -45,6 +46,6 @@ describe("local-installation", () => { · 38 unmodified lines
4546
4647 expect(second.accountId).toBe(first.accountId);
4748 expect(second.workspaceId).toBe(first.workspaceId);
48− }),
49+ }).pipe(Effect.provide(NodeFileSystem.layer)),
4950 );
5051 });
packages/control-plane/src/runtime/local-runtime-context.ts+13−0
@@ -1,5 +1,6 @@
11 import * as Context from "effect/Context";
22 import * as Effect from "effect/Effect";
3+import * as Layer from "effect/Layer";
34 import * as Option from "effect/Option";
45
56 import type { AccountId, WorkspaceId } from "#schema";
@@ -25,6 +26,18 @@ export class RuntimeLocalWorkspaceService extends Context.Tag( · 19 unmodified lines
2526 "#runtime/RuntimeLocalWorkspaceService",
2627 )<RuntimeLocalWorkspaceService, RuntimeLocalWorkspaceState>() {}
2728
29+export const RuntimeLocalWorkspaceLive = (
30+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
31+) => Layer.succeed(RuntimeLocalWorkspaceService, runtimeLocalWorkspace);
32+
33+export const provideOptionalRuntimeLocalWorkspace = <A, E, R>(
34+ effect: Effect.Effect<A, E, R>,
35+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null | undefined,
36+): Effect.Effect<A, E, R> =>
37+ runtimeLocalWorkspace === null || runtimeLocalWorkspace === undefined
38+ ? effect
39+ : effect.pipe(Effect.provide(RuntimeLocalWorkspaceLive(runtimeLocalWorkspace)));
40+
2841 export const getRuntimeLocalWorkspaceOption = () =>
2942 Effect.contextWith((context) =>
3043 Context.getOption(context, RuntimeLocalWorkspaceService),
packages/control-plane/src/runtime/local-source-artifacts.ts+10−19
@@ -1,6 +1,5 @@
11 import { join } from "node:path";
22 import { FileSystem } from "@effect/platform";
3−import { NodeFileSystem } from "@effect/platform-node";
43
54 import {
65 SourceIdSchema,
@@ -48,15 +47,6 @@ export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; · 41 unmodified lines
4847
4948 const decodeLocalSourceArtifact = Schema.decodeUnknownSync(LocalSourceArtifactSchema);
5049
51−const provideNodeFileSystem = <A, E, R>(
52− effect: Effect.Effect<A, E, R | FileSystem.FileSystem>,
53−): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> =>
54− effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
55− A,
56− E,
57− Exclude<R, FileSystem.FileSystem>
58− >;
59−
6050 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
6151 new LocalFileSystemError({
6252 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
@@ -167,9 +157,10 @@ export const readLocalSourceArtifact = (input: { · 104 unmodified lines
167157 sourceId: string;
168158 }): Effect.Effect<
169159 LocalSourceArtifact | null,
170− LocalFileSystemError | LocalSourceArtifactDecodeError
160+ LocalFileSystemError | LocalSourceArtifactDecodeError,
161+ FileSystem.FileSystem
171162 > =>
172− provideNodeFileSystem(Effect.gen(function* () {
163+ Effect.gen(function* () {
173164 const fs = yield* FileSystem.FileSystem;
174165 const path = localSourceArtifactPath(input);
175166 const exists = yield* fs.exists(path).pipe(
@@ -192,14 +183,14 @@ export const readLocalSourceArtifact = (input: { · 16 unmodified lines
192183 });
193184 },
194185 });
195− }));
186+ });
196187
197188 export const writeLocalSourceArtifact = (input: {
198189 context: ResolvedLocalWorkspaceContext;
199190 sourceId: string;
200191 artifact: LocalSourceArtifact;
201−}): Effect.Effect<void, LocalFileSystemError> =>
202− provideNodeFileSystem(Effect.gen(function* () {
192+}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
193+ Effect.gen(function* () {
203194 const fs = yield* FileSystem.FileSystem;
204195 const directory = join(input.context.artifactsDirectory, "sources");
205196 const path = localSourceArtifactPath(input);
@@ -209,13 +200,13 @@ export const writeLocalSourceArtifact = (input: { · 3 unmodified lines
209200 yield* fs.writeFileString(path, `${JSON.stringify(input.artifact, null, 2)}\n`).pipe(
210201 Effect.mapError(mapFileSystemError(path, "write source artifact")),
211202 );
212− }));
203+ });
213204
214205 export const removeLocalSourceArtifact = (input: {
215206 context: ResolvedLocalWorkspaceContext;
216207 sourceId: string;
217−}): Effect.Effect<void, LocalFileSystemError> =>
218− provideNodeFileSystem(Effect.gen(function* () {
208+}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
209+ Effect.gen(function* () {
219210 const fs = yield* FileSystem.FileSystem;
220211 const path = localSourceArtifactPath(input);
221212 const exists = yield* fs.exists(path).pipe(
@@ -227,4 +218,4 @@ export const removeLocalSourceArtifact = (input: { · 5 unmodified lines
227218 yield* fs.remove(path).pipe(
228219 Effect.mapError(mapFileSystemError(path, "remove source artifact")),
229220 );
230− }));
221+ });
packages/control-plane/src/runtime/local-storage.ts+85−14
@@ -1,6 +1,8 @@
1+import { FileSystem } from "@effect/platform";
2+import { NodeFileSystem } from "@effect/platform-node";
13 import type { LocalInstallation, LocalExecutorConfig } from "#schema";
24 import * as Context from "effect/Context";
3−import type * as Effect from "effect/Effect";
5+import * as Effect from "effect/Effect";
46 import * as Layer from "effect/Layer";
57
68 import type {
@@ -120,35 +122,104 @@ export const LocalInstallationStoreLive = Layer.succeed( · 113 unmodified lines
120122 LocalInstallationStore,
121123 );
122124
125+const bindFileSystem = <A, E>(
126+ fileSystem: FileSystem.FileSystem,
127+ effect: Effect.Effect<A, E, FileSystem.FileSystem>,
128+): Effect.Effect<A, E, never> =>
129+ effect.pipe(Effect.provideService(FileSystem.FileSystem, fileSystem));
130+
131+const bindNodeFileSystem = <A, E>(
132+ effect: Effect.Effect<A, E, FileSystem.FileSystem>,
133+): Effect.Effect<A, E, never> =>
134+ effect.pipe(Effect.provide(NodeFileSystem.layer));
135+
123136 export const LocalWorkspaceConfigStore: WorkspaceConfigStoreShape = {
124− load: loadLocalExecutorConfig,
125− writeProject: writeProjectLocalExecutorConfig,
137+ load: (context) => bindNodeFileSystem(loadLocalExecutorConfig(context)),
138+ writeProject: (input) => bindNodeFileSystem(writeProjectLocalExecutorConfig(input)),
126139 resolveRelativePath: resolveConfigRelativePath,
127140 };
128141
129−export const LocalWorkspaceConfigStoreLive = Layer.succeed(
142+export const LocalWorkspaceConfigStoreLive = Layer.effect(
130143 WorkspaceConfigStore,
131− LocalWorkspaceConfigStore,
144+ Effect.gen(function* () {
145+ const fileSystem = yield* FileSystem.FileSystem;
146+
147+ return WorkspaceConfigStore.of({
148+ load: (context) => bindFileSystem(fileSystem, loadLocalExecutorConfig(context)),
149+ writeProject: (input) =>
150+ bindFileSystem(fileSystem, writeProjectLocalExecutorConfig(input)),
151+ resolveRelativePath: resolveConfigRelativePath,
152+ });
153+ }),
132154 );
133155
134156 export const LocalWorkspaceStateStore: WorkspaceStateStoreShape = {
135− load: loadLocalWorkspaceState,
136− write: writeLocalWorkspaceState,
157+ load: (context) => bindNodeFileSystem(loadLocalWorkspaceState(context)),
158+ write: (input) => bindNodeFileSystem(writeLocalWorkspaceState(input)),
137159 };
138160
139−export const LocalWorkspaceStateStoreLive = Layer.succeed(
161+export const LocalWorkspaceStateStoreLive = Layer.effect(
140162 WorkspaceStateStore,
141− LocalWorkspaceStateStore,
163+ Effect.gen(function* () {
164+ const fileSystem = yield* FileSystem.FileSystem;
165+
166+ return WorkspaceStateStore.of({
167+ load: (context) => bindFileSystem(fileSystem, loadLocalWorkspaceState(context)),
168+ write: (input) => bindFileSystem(fileSystem, writeLocalWorkspaceState(input)),
169+ });
170+ }),
142171 );
143172
144173 export const LocalSourceArtifactStore: SourceArtifactStoreShape = {
145174 build: buildLocalSourceArtifact,
146− read: readLocalSourceArtifact,
147− write: writeLocalSourceArtifact,
148− remove: removeLocalSourceArtifact,
175+ read: (input) => bindNodeFileSystem(readLocalSourceArtifact(input)),
176+ write: (input) => bindNodeFileSystem(writeLocalSourceArtifact(input)),
177+ remove: (input) => bindNodeFileSystem(removeLocalSourceArtifact(input)),
149178 };
150179
151−export const LocalSourceArtifactStoreLive = Layer.succeed(
180+export const LocalSourceArtifactStoreLive = Layer.effect(
152181 SourceArtifactStore,
153− LocalSourceArtifactStore,
182+ Effect.gen(function* () {
183+ const fileSystem = yield* FileSystem.FileSystem;
184+
185+ return SourceArtifactStore.of({
186+ build: buildLocalSourceArtifact,
187+ read: (input) => bindFileSystem(fileSystem, readLocalSourceArtifact(input)),
188+ write: (input) => bindFileSystem(fileSystem, writeLocalSourceArtifact(input)),
189+ remove: (input) => bindFileSystem(fileSystem, removeLocalSourceArtifact(input)),
190+ });
191+ }),
192+);
193+
194+export const makeWorkspaceStorageLayer = (input: {
195+ workspaceConfigStore: WorkspaceConfigStoreShape;
196+ workspaceStateStore: WorkspaceStateStoreShape;
197+ sourceArtifactStore: SourceArtifactStoreShape;
198+}) =>
199+ Layer.mergeAll(
200+ Layer.succeed(WorkspaceConfigStore, input.workspaceConfigStore),
201+ Layer.succeed(WorkspaceStateStore, input.workspaceStateStore),
202+ Layer.succeed(SourceArtifactStore, input.sourceArtifactStore),
203+ );
204+
205+export const makeLocalStorageLayer = (input: {
206+ installationStore: InstallationStoreShape;
207+ workspaceConfigStore: WorkspaceConfigStoreShape;
208+ workspaceStateStore: WorkspaceStateStoreShape;
209+ sourceArtifactStore: SourceArtifactStoreShape;
210+}) =>
211+ Layer.mergeAll(
212+ Layer.succeed(InstallationStore, input.installationStore),
213+ makeWorkspaceStorageLayer(input),
214+ );
215+
216+export const WorkspaceStorageLive = Layer.mergeAll(
217+ LocalWorkspaceConfigStoreLive,
218+ LocalWorkspaceStateStoreLive,
219+ LocalSourceArtifactStoreLive,
220+);
221+
222+export const LocalStorageLive = Layer.mergeAll(
223+ LocalInstallationStoreLive,
224+ WorkspaceStorageLive,
154225 );
packages/control-plane/src/runtime/local-tools.ts+88−65
@@ -3,7 +3,6 @@ import { existsSync } from "node:fs"; · 2 unmodified lines
33 import { dirname, extname, join, relative, resolve, sep } from "node:path";
44 import { fileURLToPath, pathToFileURL } from "node:url";
55 import { FileSystem } from "@effect/platform";
6−import { NodeFileSystem } from "@effect/platform-node";
76 import {
87 createToolCatalogFromTools,
98 makeToolInvokerFromTools,
@@ -17,7 +16,9 @@ import { · 7 unmodified lines
1716 type ToolMetadata,
1817 type ToolPath,
1918 } from "@executor/codemode-core";
19+import * as Context from "effect/Context";
2020 import * as Effect from "effect/Effect";
21+import * as Layer from "effect/Layer";
2122 import * as ts from "typescript";
2223
2324 import type { ResolvedLocalWorkspaceContext } from "./local-config";
@@ -96,15 +97,6 @@ const emptyLocalToolRuntime = (): LocalToolRuntime => { · 72 unmodified lines
9697 };
9798 };
9899
99−const provideNodeFileSystem = <A, E, R>(
100− effect: Effect.Effect<A, E, R | FileSystem.FileSystem>,
101−): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> =>
102− effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
103− A,
104− E,
105− Exclude<R, FileSystem.FileSystem>
106− >;
107−
108100 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
109101 new LocalFileSystemError({
110102 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
@@ -589,66 +581,97 @@ export const loadLocalToolRuntime = ( · 478 unmodified lines
589581 | LocalToolDefinitionError
590582 | LocalToolImportError
591583 | LocalToolPathConflictError
592− | LocalToolTranspileError
584+ | LocalToolTranspileError,
585+ FileSystem.FileSystem
593586 > =>
594− provideNodeFileSystem(
595− Effect.gen(function* () {
596− const sourceDirectory = localToolsDirectory(context);
597− const sourceFiles = yield* collectLocalToolSourceFiles(sourceDirectory);
587+ Effect.gen(function* () {
588+ const sourceDirectory = localToolsDirectory(context);
589+ const sourceFiles = yield* collectLocalToolSourceFiles(sourceDirectory);
598590
599− if (sourceFiles.length === 0) {
600− return emptyLocalToolRuntime();
591+ if (sourceFiles.length === 0) {
592+ return emptyLocalToolRuntime();
593+ }
594+
595+ const artifactPaths = yield* buildLocalToolArtifacts({
596+ context,
597+ sourceDirectory,
598+ sourceFiles,
599+ });
600+ const candidateFiles = toolCandidateFiles(sourceDirectory, sourceFiles);
601+ const tools: ToolMap = {};
602+ const seenToolPaths = new Map<string, string>();
603+
604+ for (const sourcePath of candidateFiles) {
605+ const relativePath = relative(sourceDirectory, sourcePath);
606+ const toolPath = yield* toToolPath(relativePath);
607+ const existing = seenToolPaths.get(toolPath);
608+ if (existing) {
609+ return yield* Effect.fail(
610+ new LocalToolPathConflictError({
611+ message: `Local tool path conflict for ${toolPath}`,
612+ path: sourcePath,
613+ otherPath: existing,
614+ toolPath,
615+ }),
616+ );
617+ }
618+
619+ const artifactPath = artifactPaths.get(sourcePath);
620+ if (!artifactPath) {
621+ return yield* Effect.fail(
622+ new LocalToolImportError({
623+ message: `Missing compiled artifact for local tool ${sourcePath}`,
624+ path: sourcePath,
625+ details:
626+ "Expected a compiled local tool artifact, but none was produced.",
627+ }),
628+ );
601629 }
602630
603− const artifactPaths = yield* buildLocalToolArtifacts({
604− context,
605− sourceDirectory,
606− sourceFiles,
631+ tools[toolPath] = yield* importLocalToolModule({
632+ sourcePath,
633+ artifactPath,
634+ toolPath,
607635 });
608− const candidateFiles = toolCandidateFiles(sourceDirectory, sourceFiles);
609− const tools: ToolMap = {};
610− const seenToolPaths = new Map<string, string>();
611−
612− for (const sourcePath of candidateFiles) {
613− const relativePath = relative(sourceDirectory, sourcePath);
614− const toolPath = yield* toToolPath(relativePath);
615− const existing = seenToolPaths.get(toolPath);
616− if (existing) {
617− return yield* Effect.fail(
618− new LocalToolPathConflictError({
619− message: `Local tool path conflict for ${toolPath}`,
620− path: sourcePath,
621− otherPath: existing,
622− toolPath,
623− }),
624− );
625− }
636+ seenToolPaths.set(toolPath, sourcePath);
637+ }
626638
627− const artifactPath = artifactPaths.get(sourcePath);
628− if (!artifactPath) {
629− return yield* Effect.fail(
630− new LocalToolImportError({
631− message: `Missing compiled artifact for local tool ${sourcePath}`,
632− path: sourcePath,
633− details:
634− "Expected a compiled local tool artifact, but none was produced.",
635− }),
636− );
637− }
639+ return {
640+ tools,
641+ catalog: createToolCatalogFromTools({ tools }),
642+ toolInvoker: makeToolInvokerFromTools({ tools }),
643+ toolPaths: new Set(Object.keys(tools)),
644+ } satisfies LocalToolRuntime;
645+ });
638646
639− tools[toolPath] = yield* importLocalToolModule({
640− sourcePath,
641− artifactPath,
642− toolPath,
643− });
644− seenToolPaths.set(toolPath, sourcePath);
645− }
647+export type LocalToolRuntimeLoaderShape = {
648+ load: (
649+ context: ResolvedLocalWorkspaceContext,
650+ ) => Effect.Effect<
651+ LocalToolRuntime,
652+ | LocalFileSystemError
653+ | LocalToolDefinitionError
654+ | LocalToolImportError
655+ | LocalToolPathConflictError
656+ | LocalToolTranspileError,
657+ never
658+ >;
659+};
646660
647− return {
648− tools,
649− catalog: createToolCatalogFromTools({ tools }),
650− toolInvoker: makeToolInvokerFromTools({ tools }),
651− toolPaths: new Set(Object.keys(tools)),
652− } satisfies LocalToolRuntime;
653− }),
654− );
661+export class LocalToolRuntimeLoaderService extends Context.Tag(
662+ "#runtime/LocalToolRuntimeLoaderService",
663+)<LocalToolRuntimeLoaderService, LocalToolRuntimeLoaderShape>() {}
664+
665+export const LocalToolRuntimeLoaderLive = Layer.effect(
666+ LocalToolRuntimeLoaderService,
667+ Effect.gen(function* () {
668+ const fileSystem = yield* FileSystem.FileSystem;
669+
670+ return LocalToolRuntimeLoaderService.of({
671+ load: (context) =>
672+ loadLocalToolRuntime(context).pipe(
673+ Effect.provideService(FileSystem.FileSystem, fileSystem),
674+ ),
675+ });
676+ }),
677+);
packages/control-plane/src/runtime/local-workspace-state.ts+10−16
@@ -1,6 +1,5 @@
11 import { join } from "node:path";
22 import { FileSystem } from "@effect/platform";
3−import { NodeFileSystem } from "@effect/platform-node";
43
54 import {
65 PolicyIdSchema,
@@ -51,15 +50,6 @@ export type LocalWorkspaceState = typeof LocalWorkspaceStateSchema.Type; · 44 unmodified lines
5150
5251 const decodeLocalWorkspaceState = Schema.decodeUnknownSync(LocalWorkspaceStateSchema);
5352
54−const provideNodeFileSystem = <A, E, R>(
55− effect: Effect.Effect<A, E, R | FileSystem.FileSystem>,
56−): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> =>
57− effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
58− A,
59− E,
60− Exclude<R, FileSystem.FileSystem>
61− >;
62−
6353 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
6454 new LocalFileSystemError({
6555 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
@@ -80,8 +70,12 @@ export const localWorkspaceStatePath = ( · 14 unmodified lines
8070
8171 export const loadLocalWorkspaceState = (
8272 context: ResolvedLocalWorkspaceContext,
83−): Effect.Effect<LocalWorkspaceState, LocalFileSystemError | LocalWorkspaceStateDecodeError> =>
84− provideNodeFileSystem(Effect.gen(function* () {
73+): Effect.Effect<
74+ LocalWorkspaceState,
75+ LocalFileSystemError | LocalWorkspaceStateDecodeError,
76+ FileSystem.FileSystem
77+> =>
78+ Effect.gen(function* () {
8579 const fs = yield* FileSystem.FileSystem;
8680 const path = localWorkspaceStatePath(context);
8781 const exists = yield* fs.exists(path).pipe(
@@ -104,13 +98,13 @@ export const loadLocalWorkspaceState = ( · 16 unmodified lines
10498 });
10599 },
106100 });
107− }));
101+ });
108102
109103 export const writeLocalWorkspaceState = (input: {
110104 context: ResolvedLocalWorkspaceContext;
111105 state: LocalWorkspaceState;
112−}): Effect.Effect<void, LocalFileSystemError> =>
113− provideNodeFileSystem(Effect.gen(function* () {
106+}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
107+ Effect.gen(function* () {
114108 const fs = yield* FileSystem.FileSystem;
115109 yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe(
116110 Effect.mapError(mapFileSystemError(input.context.stateDirectory, "create state directory")),
@@ -121,4 +115,4 @@ export const writeLocalWorkspaceState = (input: { · 4 unmodified lines
121115 ).pipe(
122116 Effect.mapError(mapFileSystemError(localWorkspaceStatePath(input.context), "write workspace state")),
123117 );
124− }));
118+ });
packages/control-plane/src/runtime/local-workspace-sync.ts+3−2
@@ -1,3 +1,4 @@
1+import { FileSystem } from "@effect/platform";
12 import * as Effect from "effect/Effect";
23
34 import {
@@ -42,7 +43,7 @@ export const derivePolicyConfigKey = ( · 38 unmodified lines
4243 const pruneLocalWorkspaceState = (input: {
4344 context: ResolvedLocalWorkspaceContext;
4445 loadedConfig: LoadedLocalExecutorConfig;
45−}): Effect.Effect<LocalWorkspaceState, Error, never> =>
46+}): Effect.Effect<LocalWorkspaceState, Error, FileSystem.FileSystem> =>
4647 Effect.gen(function* () {
4748 const currentState = yield* loadLocalWorkspaceState(input.context);
4849
@@ -82,7 +83,7 @@ const pruneLocalWorkspaceState = (input: { · 33 unmodified lines
8283 export const synchronizeLocalWorkspaceState = (input: {
8384 context: ResolvedLocalWorkspaceContext;
8485 loadedConfig: LoadedLocalExecutorConfig;
85−}): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, never> =>
86+}): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, FileSystem.FileSystem> =>
8687 Effect.gen(function* () {
8788 yield* pruneLocalWorkspaceState({
8889 context: input.context,
packages/control-plane/src/runtime/secret-material-providers.ts+108−0
@@ -11,11 +11,15 @@ import { · 10 unmodified lines
1111 SecretMaterialIdSchema,
1212 type SecretRef,
1313 } from "#schema";
14+import * as Context from "effect/Context";
1415 import * as Effect from "effect/Effect";
16+import * as Layer from "effect/Layer";
1517 import * as Option from "effect/Option";
1618
1719 import { resolveConfigRelativePath } from "./local-config";
1820 import { fromConfigSecretProviderId } from "./local-config-secrets";
21+import { getRuntimeLocalWorkspaceOption } from "./local-runtime-context";
22+import { ControlPlaneStore } from "./store";
1923 import type { ControlPlaneStoreShape } from "./store";
2024
2125 export const ENV_SECRET_PROVIDER_ID = "env";
@@ -46,6 +50,18 @@ export type DeleteSecretMaterial = ( · 24 unmodified lines
4650 ref: SecretRef,
4751 ) => Effect.Effect<boolean, Error, never>;
4852
53+export class SecretMaterialResolverService extends Context.Tag(
54+ "#runtime/SecretMaterialResolverService",
55+)<SecretMaterialResolverService, ResolveSecretMaterial>() {}
56+
57+export class SecretMaterialStorerService extends Context.Tag(
58+ "#runtime/SecretMaterialStorerService",
59+)<SecretMaterialStorerService, StoreSecretMaterial>() {}
60+
61+export class SecretMaterialDeleterService extends Context.Tag(
62+ "#runtime/SecretMaterialDeleterService",
63+)<SecretMaterialDeleterService, DeleteSecretMaterial>() {}
64+
4965 type SecretMaterialProviderRuntime = {
5066 rows: ControlPlaneStoreShape;
5167 env: NodeJS.ProcessEnv;
@@ -740,3 +756,95 @@ export const createDefaultSecretMaterialDeleter = (input: { · 688 unmodified lines
740756 });
741757 });
742758 };
759+
760+const resolveRuntimeSecretMaterialConfig = (input: {
761+ localConfig?: LocalExecutorConfig | null;
762+ workspaceRoot?: string | null;
763+}) =>
764+ Effect.gen(function* () {
765+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
766+
767+ return {
768+ localConfig: input.localConfig
769+ ?? runtimeLocalWorkspace?.loadedConfig.config
770+ ?? null,
771+ workspaceRoot: input.workspaceRoot
772+ ?? runtimeLocalWorkspace?.context.workspaceRoot
773+ ?? null,
774+ };
775+ });
776+
777+export const SecretMaterialResolverLive = (input: {
778+ resolveSecretMaterial?: ResolveSecretMaterial;
779+ dangerouslyAllowEnvSecrets?: boolean;
780+ keychainServiceName?: string;
781+ localConfig?: LocalExecutorConfig | null;
782+ workspaceRoot?: string | null;
783+} = {}) =>
784+ input.resolveSecretMaterial
785+ ? Layer.succeed(SecretMaterialResolverService, input.resolveSecretMaterial)
786+ : Layer.effect(
787+ SecretMaterialResolverService,
788+ Effect.gen(function* () {
789+ const rows = yield* ControlPlaneStore;
790+ const runtimeConfig = yield* resolveRuntimeSecretMaterialConfig(input);
791+
792+ return createDefaultSecretMaterialResolver({
793+ rows,
794+ dangerouslyAllowEnvSecrets: input.dangerouslyAllowEnvSecrets,
795+ keychainServiceName: input.keychainServiceName,
796+ localConfig: runtimeConfig.localConfig,
797+ workspaceRoot: runtimeConfig.workspaceRoot,
798+ });
799+ }),
800+ );
801+
802+export const SecretMaterialStorerLive = (input: {
803+ storeProviderId?: SecretStoreProviderId;
804+ dangerouslyAllowEnvSecrets?: boolean;
805+ keychainServiceName?: string;
806+} = {}) =>
807+ Layer.effect(
808+ SecretMaterialStorerService,
809+ Effect.gen(function* () {
810+ const rows = yield* ControlPlaneStore;
811+
812+ return createDefaultSecretMaterialStorer({
813+ rows,
814+ storeProviderId: input.storeProviderId,
815+ dangerouslyAllowEnvSecrets: input.dangerouslyAllowEnvSecrets,
816+ keychainServiceName: input.keychainServiceName,
817+ });
818+ }),
819+ );
820+
821+export const SecretMaterialDeleterLive = (input: {
822+ dangerouslyAllowEnvSecrets?: boolean;
823+ keychainServiceName?: string;
824+} = {}) =>
825+ Layer.effect(
826+ SecretMaterialDeleterService,
827+ Effect.gen(function* () {
828+ const rows = yield* ControlPlaneStore;
829+
830+ return createDefaultSecretMaterialDeleter({
831+ rows,
832+ dangerouslyAllowEnvSecrets: input.dangerouslyAllowEnvSecrets,
833+ keychainServiceName: input.keychainServiceName,
834+ });
835+ }),
836+ );
837+
838+export const SecretMaterialLive = (input: {
839+ resolveSecretMaterial?: ResolveSecretMaterial;
840+ storeProviderId?: SecretStoreProviderId;
841+ dangerouslyAllowEnvSecrets?: boolean;
842+ keychainServiceName?: string;
843+ localConfig?: LocalExecutorConfig | null;
844+ workspaceRoot?: string | null;
845+} = {}) =>
846+ Layer.mergeAll(
847+ SecretMaterialResolverLive(input),
848+ SecretMaterialStorerLive(input),
849+ SecretMaterialDeleterLive(input),
850+ );
packages/control-plane/src/runtime/source-adapters/google-discovery.ts+8−0
@@ -4,6 +4,7 @@ import { · 3 unmodified lines
44 HttpClientRequest,
55 } from "@effect/platform";
66 import {
7+ allowAllToolInteractions,
78 makeToolInvokerFromTools,
89 typeSignatureFromSchemaJson,
910 } from "@executor/codemode-core";
@@ -616,6 +617,13 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 606 unmodified lines
616617 tools: {
617618 [path]: tool,
618619 },
620+ // Authorization was already handled by authorizePersistedToolInvocation
621+ // at the workspace level, so skip the redundant tool-level interaction
622+ // gate. Without this, the default "required" interaction on write tools
623+ // would re-elicit approval and then merge the approval form content
624+ // (e.g. { approve: true }) into the tool args, breaking schemas that
625+ // use additionalProperties: false.
626+ onToolInteraction: allowAllToolInteractions,
619627 onElicitation,
620628 }).invoke({
621629 path,
packages/control-plane/src/runtime/source-adapters/graphql.ts+2−0
@@ -1,4 +1,5 @@
11 import {
2+ allowAllToolInteractions,
23 applyCookiePlacementsToHeaders,
34 applyHttpQueryPlacementsToUrl,
45 applyJsonBodyPlacements,
@@ -524,6 +525,7 @@ export const graphqlSourceAdapter: SourceAdapter = { · 519 unmodified lines
524525 tools: {
525526 [path]: tool,
526527 },
528+ onToolInteraction: allowAllToolInteractions,
527529 onElicitation,
528530 }).invoke({
529531 path,
packages/control-plane/src/runtime/source-adapters/mcp.ts+2−0
@@ -1,4 +1,5 @@
11 import {
2+ allowAllToolInteractions,
23 applyCookiePlacementsToHeaders,
34 makeToolInvokerFromTools,
45 } from "@executor/codemode-core";
@@ -409,6 +410,7 @@ export const mcpSourceAdapter: SourceAdapter = { · 404 unmodified lines
409410
410411 return yield* makeToolInvokerFromTools({
411412 tools,
413+ onToolInteraction: allowAllToolInteractions,
412414 onElicitation,
413415 }).invoke({
414416 path,
packages/control-plane/src/runtime/source-adapters/openapi.ts+2−0
@@ -4,6 +4,7 @@ import { · 3 unmodified lines
44 HttpClientRequest,
55 } from "@effect/platform";
66 import {
7+ allowAllToolInteractions,
78 applyCookiePlacementsToHeaders,
89 applyHttpQueryPlacementsToUrl,
910 makeToolInvokerFromTools,
@@ -563,6 +564,7 @@ export const openApiSourceAdapter: SourceAdapter = { · 553 unmodified lines
563564 tools: {
564565 [path]: tool,
565566 },
567+ onToolInteraction: allowAllToolInteractions,
566568 onElicitation,
567569 }).invoke({
568570 path,
packages/control-plane/src/runtime/source-adapters/types.ts+0−2
@@ -24,7 +24,6 @@ import * as Schema from "effect/Schema"; · 23 unmodified lines
2424
2525 import type { ResolvedSourceAuthMaterial } from "../source-auth-material";
2626 import type { ResolveSecretMaterial } from "../secret-material-providers";
27−import type { ControlPlaneStoreShape } from "../store";
2827
2928 export type SourceAdapterFamily = "http_api" | "mcp" | "internal";
3029 export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;
@@ -82,7 +81,6 @@ export type SourceAdapterOauth2SetupConfig = { · 51 unmodified lines
8281 };
8382
8483 export type SourceAdapterInvokePersistedToolInput = {
85− rows: ControlPlaneStoreShape;
8684 workspaceId: Source["workspaceId"];
8785 accountId: AccountId;
8886 source: Source;
packages/control-plane/src/runtime/source-auth-material.test.ts+94−88
@@ -7,7 +7,7 @@ import { · 6 unmodified lines
77 type Source,
88 } from "#schema";
99
10−import { resolveSourceAuthMaterial } from "./source-auth-material";
10+import { resolveSourceAuthMaterialWithDeps } from "./source-auth-material";
1111
1212 const makeSource = (overrides: Partial<Source> = {}): Source => ({
1313 id: SourceIdSchema.make("src_tool_artifacts"),
@@ -36,113 +36,119 @@ const makeSource = (overrides: Partial<Source> = {}): Source => ({ · 22 unmodified lines
3636 describe("source-auth-material", () => {
3737 describe("resolveSourceAuthMaterial", () => {
3838 it("returns empty headers for auth.kind none", async () => {
39− const auth = await Effect.runPromise(resolveSourceAuthMaterial({
40− source: makeSource({
41− auth: { kind: "none" },
42− }),
43− resolveSecretMaterial: () => Effect.die("should not be called"),
44− }));
39+ await Effect.runPromise(Effect.gen(function* () {
40+ const auth = yield* resolveSourceAuthMaterialWithDeps({
41+ source: makeSource({
42+ auth: { kind: "none" },
43+ }),
44+ resolveSecretMaterial: () => Effect.die("should not be called"),
45+ });
4546
46− expect(auth).toEqual({
47− placements: [],
48− headers: {},
49− queryParams: {},
50− cookies: {},
51− bodyValues: {},
52− expiresAt: null,
53− refreshAfter: null,
54− });
47+ expect(auth).toEqual({
48+ placements: [],
49+ headers: {},
50+ queryParams: {},
51+ cookies: {},
52+ bodyValues: {},
53+ expiresAt: null,
54+ refreshAfter: null,
55+ });
56+ }));
5557 });
5658
5759 it("resolves bearer auth headers from the configured token ref", async () => {
5860 const calls: string[] = [];
5961
60− const auth = await Effect.runPromise(resolveSourceAuthMaterial({
61− source: makeSource({
62− auth: {
63− kind: "bearer",
64− headerName: "X-Api-Key",
65− prefix: "Token ",
66− token: {
67− providerId: "local",
68− handle: "sec_bearer",
62+ await Effect.runPromise(Effect.gen(function* () {
63+ const auth = yield* resolveSourceAuthMaterialWithDeps({
64+ source: makeSource({
65+ auth: {
66+ kind: "bearer",
67+ headerName: "X-Api-Key",
68+ prefix: "Token ",
69+ token: {
70+ providerId: "local",
71+ handle: "sec_bearer",
72+ },
6973 },
74+ }),
75+ resolveSecretMaterial: ({ ref }) => {
76+ calls.push(`${ref.providerId}:${ref.handle}`);
77+ return Effect.succeed("resolved-bearer-token");
7078 },
71− }),
72− resolveSecretMaterial: ({ ref }) => {
73− calls.push(`${ref.providerId}:${ref.handle}`);
74− return Effect.succeed("resolved-bearer-token");
75− },
76− }));
79+ });
7780
78− expect(calls).toEqual(["local:sec_bearer"]);
79− expect(auth).toEqual({
80− placements: [
81− {
82− location: "header",
83− name: "X-Api-Key",
84− value: "Token resolved-bearer-token",
81+ expect(calls).toEqual(["local:sec_bearer"]);
82+ expect(auth).toEqual({
83+ placements: [
84+ {
85+ location: "header",
86+ name: "X-Api-Key",
87+ value: "Token resolved-bearer-token",
88+ },
89+ ],
90+ headers: {
91+ "X-Api-Key": "Token resolved-bearer-token",
8592 },
86− ],
87− headers: {
88− "X-Api-Key": "Token resolved-bearer-token",
89− },
90− queryParams: {},
91− cookies: {},
92− bodyValues: {},
93− expiresAt: null,
94− refreshAfter: null,
95− });
93+ queryParams: {},
94+ cookies: {},
95+ bodyValues: {},
96+ expiresAt: null,
97+ refreshAfter: null,
98+ });
99+ }));
96100 });
97101
98102 it("uses the oauth access token ref and ignores the refresh token", async () => {
99103 const calls: string[] = [];
100104
101− const auth = await Effect.runPromise(resolveSourceAuthMaterial({
102− source: makeSource({
103− kind: "graphql",
104− endpoint: "https://example.com/graphql",
105− binding: {
106− defaultHeaders: null,
107− },
108− auth: {
109− kind: "oauth2",
110− headerName: "Authorization",
111− prefix: "Bearer ",
112− accessToken: {
113− providerId: "local",
114− handle: "sec_access",
105+ await Effect.runPromise(Effect.gen(function* () {
106+ const auth = yield* resolveSourceAuthMaterialWithDeps({
107+ source: makeSource({
108+ kind: "graphql",
109+ endpoint: "https://example.com/graphql",
110+ binding: {
111+ defaultHeaders: null,
115112 },
116− refreshToken: {
117− providerId: "local",
118− handle: "sec_refresh",
113+ auth: {
114+ kind: "oauth2",
115+ headerName: "Authorization",
116+ prefix: "Bearer ",
117+ accessToken: {
118+ providerId: "local",
119+ handle: "sec_access",
120+ },
121+ refreshToken: {
122+ providerId: "local",
123+ handle: "sec_refresh",
124+ },
119125 },
126+ }),
127+ resolveSecretMaterial: ({ ref }) => {
128+ calls.push(`${ref.providerId}:${ref.handle}`);
129+ return Effect.succeed("resolved-access-token");
120130 },
121− }),
122− resolveSecretMaterial: ({ ref }) => {
123− calls.push(`${ref.providerId}:${ref.handle}`);
124− return Effect.succeed("resolved-access-token");
125− },
126− }));
131+ });
127132
128− expect(calls).toEqual(["local:sec_access"]);
129− expect(auth).toEqual({
130− placements: [
131− {
132− location: "header",
133− name: "Authorization",
134− value: "Bearer resolved-access-token",
133+ expect(calls).toEqual(["local:sec_access"]);
134+ expect(auth).toEqual({
135+ placements: [
136+ {
137+ location: "header",
138+ name: "Authorization",
139+ value: "Bearer resolved-access-token",
140+ },
141+ ],
142+ headers: {
143+ Authorization: "Bearer resolved-access-token",
135144 },
136− ],
137− headers: {
138− Authorization: "Bearer resolved-access-token",
139− },
140− queryParams: {},
141− cookies: {},
142− bodyValues: {},
143− expiresAt: null,
144− refreshAfter: null,
145− });
145+ queryParams: {},
146+ cookies: {},
147+ bodyValues: {},
148+ expiresAt: null,
149+ refreshAfter: null,
150+ });
151+ }));
146152 });
147153 });
148154 });
packages/control-plane/src/runtime/source-auth-material.ts+43−2
@@ -4,10 +4,12 @@ import type { · 3 unmodified lines
44 Source,
55 SourceAuth,
66 } from "#schema";
7+import * as Context from "effect/Context";
78 import * as Effect from "effect/Effect";
9+import * as Layer from "effect/Layer";
810 import * as Option from "effect/Option";
911
10−import type { ControlPlaneStoreShape } from "./store";
12+import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
1113 import {
1214 authArtifactFromSourceAuth,
1315 resolveAuthArtifactMaterial,
@@ -18,6 +20,7 @@ import type { · 4 unmodified lines
1820 ResolveSecretMaterial,
1921 SecretMaterialResolveContext,
2022 } from "./secret-material-providers";
23+import { SecretMaterialResolverService } from "./secret-material-providers";
2124
2225 const authForSlot = (input: {
2326 source: Source;
@@ -38,7 +41,20 @@ const authForSlot = (input: { · 14 unmodified lines
3841 return input.source.importAuth;
3942 };
4043
41−export const resolveSourceAuthMaterial = (input: {
44+export type RuntimeSourceAuthMaterialShape = {
45+ resolve: (input: {
46+ source: Source;
47+ slot?: CredentialSlot;
48+ actorAccountId?: AccountId | null;
49+ context?: SecretMaterialResolveContext;
50+ }) => Effect.Effect<ResolvedSourceAuthMaterial, Error, never>;
51+};
52+
53+export class RuntimeSourceAuthMaterialService extends Context.Tag(
54+ "#runtime/RuntimeSourceAuthMaterialService",
55+)<RuntimeSourceAuthMaterialService, RuntimeSourceAuthMaterialShape>() {}
56+
57+export const resolveSourceAuthMaterialWithDeps = (input: {
4258 source: Source;
4359 slot?: CredentialSlot;
4460 actorAccountId?: AccountId | null;
@@ -106,4 +122,29 @@ export const resolveSourceAuthMaterial = (input: { · 61 unmodified lines
106122 });
107123 });
108124
125+export const resolveSourceAuthMaterial = (input: {
126+ source: Source;
127+ slot?: CredentialSlot;
128+ actorAccountId?: AccountId | null;
129+ context?: SecretMaterialResolveContext;
130+}): Effect.Effect<ResolvedSourceAuthMaterial, Error, RuntimeSourceAuthMaterialService> =>
131+ Effect.flatMap(RuntimeSourceAuthMaterialService, (service) => service.resolve(input));
132+
133+export const RuntimeSourceAuthMaterialLive = Layer.effect(
134+ RuntimeSourceAuthMaterialService,
135+ Effect.gen(function* () {
136+ const rows = yield* ControlPlaneStore;
137+ const resolveSecretMaterial = yield* SecretMaterialResolverService;
138+
139+ return RuntimeSourceAuthMaterialService.of({
140+ resolve: (input) =>
141+ resolveSourceAuthMaterialWithDeps({
142+ ...input,
143+ rows,
144+ resolveSecretMaterial,
145+ }),
146+ });
147+ }),
148+);
149+
109150 export type { ResolvedSourceAuthMaterial } from "./auth-artifacts";
packages/control-plane/src/runtime/source-auth-service.ts+87−90
@@ -7,7 +7,6 @@ import { · 6 unmodified lines
77 AccountId,
88 type CredentialSlot,
99 ExecutionIdSchema,
10− type LocalExecutorConfig,
1110 McpSourceAuthSessionDataJsonSchema,
1211 type McpSourceAuthSessionData,
1312 OAuth2PkceSourceAuthSessionDataJsonSchema,
@@ -42,7 +41,8 @@ import { · 28 unmodified lines
4241 type LiveExecutionManager,
4342 } from "./live-execution";
4443 import {
45− RuntimeLocalWorkspaceService,
44+ getRuntimeLocalWorkspaceOption,
45+ provideOptionalRuntimeLocalWorkspace,
4646 type RuntimeLocalWorkspaceState,
4747 } from "./local-runtime-context";
4848 import {
@@ -61,15 +61,15 @@ import { · 12 unmodified lines
6161 import { isSourceCredentialRequiredError } from "./source-adapters/shared";
6262 import {
6363 createDefaultSecretMaterialDeleter,
64− createDefaultSecretMaterialResolver,
65− createDefaultSecretMaterialStorer,
6664 type ResolveSecretMaterial,
65+ SecretMaterialResolverService,
66+ SecretMaterialStorerService,
6767 type StoreSecretMaterial,
6868 } from "./secret-material-providers";
6969 import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases";
7070 import {
71− persistMcpRecipeMaterializationFromManifest,
72− syncSourceMaterialization,
71+ RuntimeSourceMaterializationService,
72+ type RuntimeSourceMaterializationShape,
7373 } from "./source-materialization";
7474 import {
7575 buildOAuth2AuthorizationUrl,
@@ -81,6 +81,8 @@ import { · 5 unmodified lines
8181 loadSourceById,
8282 loadSourcesInWorkspace,
8383 persistSource,
84+ type RuntimeSourceStore,
85+ RuntimeSourceStoreService,
8486 } from "./source-store";
8587 import type { WorkspaceStorageServices } from "./local-storage";
8688 import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
@@ -358,7 +360,7 @@ const serializeJson = (value: unknown): string | null => { · 271 unmodified lines
358360 return JSON.stringify(value);
359361 };
360362
361−const updateSourceStatus = (rows: ControlPlaneStoreShape, source: Source, input: {
363+const updateSourceStatus = (sourceStore: RuntimeSourceStore, source: Source, input: {
362364 actorAccountId?: AccountId | null;
363365 status: Source["status"];
364366 lastError?: string | null;
@@ -366,13 +368,13 @@ const updateSourceStatus = (rows: ControlPlaneStoreShape, source: Source, input: · 1 unmodified line
366368 importAuth?: Source["importAuth"];
367369 }) =>
368370 Effect.gen(function* () {
369− const latest = yield* loadSourceById(rows, {
371+ const latest = yield* sourceStore.loadSourceById({
370372 workspaceId: source.workspaceId,
371373 sourceId: source.id,
372374 actorAccountId: input.actorAccountId,
373375 });
374376
375− return yield* persistSource(rows, {
377+ return yield* sourceStore.persistSource({
376378 ...latest,
377379 status: input.status,
378380 lastError: input.lastError ?? null,
@@ -857,6 +859,7 @@ const resolveExistingSourceOauthClient = (input: { · 478 unmodified lines
857859
858860 const startOauth2PkceSourceCredentialSetup = (input: {
859861 rows: ControlPlaneStoreShape;
862+ sourceStore: RuntimeSourceStore;
860863 source: Source;
861864 actorAccountId?: AccountId | null;
862865 executionId?: SourceAuthSession["executionId"];
@@ -951,7 +954,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 88 unmodified lines
951954 updatedAt: now,
952955 });
953956
954− const authRequiredSource = yield* updateSourceStatus(input.rows, input.source, {
957+ const authRequiredSource = yield* updateSourceStatus(input.sourceStore, input.source, {
955958 actorAccountId: input.actorAccountId,
956959 status: "auth_required",
957960 lastError: null,
@@ -974,6 +977,8 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 16 unmodified lines
974977
975978 const connectMcpSourceInternal = (input: {
976979 rows: ControlPlaneStoreShape;
980+ sourceStore: RuntimeSourceStore;
981+ sourceMaterialization: RuntimeSourceMaterializationShape;
977982 getLocalServerBaseUrl?: () => string | undefined;
978983 baseUrl?: string | null;
979984 workspaceId: WorkspaceId;
@@ -995,7 +1000,7 @@ const connectMcpSourceInternal = (input: { · 15 unmodified lines
9951000 const normalizedEndpoint = normalizeEndpoint(input.endpoint);
9961001 const existing = yield* (
9971002 input.sourceId
998− ? loadSourceById(input.rows, {
1003+ ? input.sourceStore.loadSourceById({
9991004 workspaceId: input.workspaceId,
10001005 sourceId: input.sourceId,
10011006 actorAccountId: input.actorAccountId,
@@ -1006,7 +1011,7 @@ const connectMcpSourceInternal = (input: { · 4 unmodified lines
10061011 : Effect.fail(new Error(`Expected MCP source, received ${source.kind}`)),
10071012 ),
10081013 )
1009− : loadSourcesInWorkspace(input.rows, input.workspaceId, {
1014+ : input.sourceStore.loadSourcesInWorkspace(input.workspaceId, {
10101015 actorAccountId: input.actorAccountId,
10111016 }).pipe(
10121017 Effect.map((sources) =>
@@ -1079,14 +1084,12 @@ const connectMcpSourceInternal = (input: { · 66 unmodified lines
10791084 now,
10801085 });
10811086
1082− const persistedDraft = yield* persistSource(input.rows, draftSource, {
1087+ const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {
10831088 actorAccountId: input.actorAccountId,
10841089 });
1085− yield* syncSourceMaterialization({
1086− rows: input.rows,
1090+ yield* input.sourceMaterialization.sync({
10871091 source: persistedDraft,
10881092 actorAccountId: input.actorAccountId,
1089− resolveSecretMaterial: input.resolveSecretMaterial,
10901093 });
10911094
10921095 const discovered = yield* Effect.either(
@@ -1100,15 +1103,14 @@ const connectMcpSourceInternal = (input: { · 7 unmodified lines
11001103 onLeft: () => Effect.succeed(null),
11011104 onRight: (result) =>
11021105 Effect.gen(function* () {
1103− const connected = yield* updateSourceStatus(input.rows, persistedDraft, {
1106+ const connected = yield* updateSourceStatus(input.sourceStore, persistedDraft, {
11041107 actorAccountId: input.actorAccountId,
11051108 status: "connected",
11061109 lastError: null,
11071110 auth: { kind: "none" },
11081111 });
11091112 const indexed = yield* Effect.either(
1110− persistMcpRecipeMaterializationFromManifest({
1111− rows: input.rows,
1113+ input.sourceMaterialization.persistMcpRecipeMaterializationFromManifest({
11121114 source: connected,
11131115 manifestEntries: result.manifest.tools,
11141116 }),
@@ -1116,7 +1118,7 @@ const connectMcpSourceInternal = (input: { · 1 unmodified line
11161118
11171119 return yield* Either.match(indexed, {
11181120 onLeft: (error) =>
1119− updateSourceStatus(input.rows, connected, {
1121+ updateSourceStatus(input.sourceStore, connected, {
11201122 actorAccountId: input.actorAccountId,
11211123 status: "error",
11221124 lastError: error.message,
@@ -1156,7 +1158,7 @@ const connectMcpSourceInternal = (input: { · 33 unmodified lines
11561158 state,
11571159 });
11581160
1159− const authRequiredSource = yield* updateSourceStatus(input.rows, persistedDraft, {
1161+ const authRequiredSource = yield* updateSourceStatus(input.sourceStore, persistedDraft, {
11601162 actorAccountId: input.actorAccountId,
11611163 status: "auth_required",
11621164 lastError: null,
@@ -1203,6 +1205,8 @@ const connectMcpSourceInternal = (input: { · 40 unmodified lines
12031205
12041206 const addExecutorHttpSource = (input: {
12051207 rows: ControlPlaneStoreShape;
1208+ sourceStore: RuntimeSourceStore;
1209+ sourceMaterialization: RuntimeSourceMaterializationShape;
12061210 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;
12071211 storeSecretMaterial: StoreSecretMaterial;
12081212 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1214,8 +1218,7 @@ const addExecutorHttpSource = (input: { · 5 unmodified lines
12141218 const normalizedSpecUrl = input.sourceInput.kind === "openapi"
12151219 ? normalizeEndpoint(input.sourceInput.specUrl)
12161220 : null;
1217− const existingSources = yield* loadSourcesInWorkspace(
1218− input.rows,
1221+ const existingSources = yield* input.sourceStore.loadSourcesInWorkspace(
12191222 input.sourceInput.workspaceId,
12201223 {
12211224 actorAccountId: input.sourceInput.actorAccountId,
@@ -1313,7 +1316,7 @@ const addExecutorHttpSource = (input: { · 91 unmodified lines
13131316 now,
13141317 });
13151318
1316− const persistedDraft = yield* persistSource(input.rows, draftSource, {
1319+ const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {
13171320 actorAccountId: input.sourceInput.actorAccountId,
13181321 });
13191322
@@ -1327,6 +1330,7 @@ const addExecutorHttpSource = (input: { · 7 unmodified lines
13271330 if (baseUrl) {
13281331 const oauthRequired = yield* startOauth2PkceSourceCredentialSetup({
13291332 rows: input.rows,
1333+ sourceStore: input.sourceStore,
13301334 source: persistedDraft,
13311335 actorAccountId: input.sourceInput.actorAccountId,
13321336 executionId: input.sourceInput.executionId,
@@ -1340,7 +1344,7 @@ const addExecutorHttpSource = (input: { · 7 unmodified lines
13401344 }
13411345 }
13421346
1343− const authRequiredSource = yield* updateSourceStatus(input.rows, persistedDraft, {
1347+ const authRequiredSource = yield* updateSourceStatus(input.sourceStore, persistedDraft, {
13441348 actorAccountId: input.sourceInput.actorAccountId,
13451349 status: "auth_required",
13461350 lastError: null,
@@ -1354,21 +1358,19 @@ const addExecutorHttpSource = (input: { · 7 unmodified lines
13541358 }
13551359
13561360 const synced = yield* Effect.either(
1357− syncSourceMaterialization({
1358− rows: input.rows,
1361+ input.sourceMaterialization.sync({
13591362 source: {
13601363 ...persistedDraft,
13611364 status: "connected",
13621365 },
13631366 actorAccountId: input.sourceInput.actorAccountId,
1364− resolveSecretMaterial: input.resolveSecretMaterial,
13651367 }),
13661368 );
13671369
13681370 return yield* Either.match(synced, {
13691371 onLeft: (error) =>
13701372 isSourceCredentialRequiredError(error)
1371− ? updateSourceStatus(input.rows, persistedDraft, {
1373+ ? updateSourceStatus(input.sourceStore, persistedDraft, {
13721374 actorAccountId: input.sourceInput.actorAccountId,
13731375 status: "auth_required",
13741376 lastError: null,
@@ -1381,7 +1383,7 @@ const addExecutorHttpSource = (input: { · 6 unmodified lines
13811383 } satisfies ExecutorSourceAddResult)
13821384 ),
13831385 )
1384− : updateSourceStatus(input.rows, persistedDraft, {
1386+ : updateSourceStatus(input.sourceStore, persistedDraft, {
13851387 actorAccountId: input.sourceInput.actorAccountId,
13861388 status: "error",
13871389 lastError: error.message,
@@ -1389,7 +1391,7 @@ const addExecutorHttpSource = (input: { · 1 unmodified line
13891391 Effect.zipRight(Effect.fail(error)),
13901392 ),
13911393 onRight: () =>
1392− updateSourceStatus(input.rows, persistedDraft, {
1394+ updateSourceStatus(input.sourceStore, persistedDraft, {
13931395 actorAccountId: input.sourceInput.actorAccountId,
13941396 status: "connected",
13951397 lastError: null,
@@ -1406,6 +1408,8 @@ const addExecutorHttpSource = (input: { · 10 unmodified lines
14061408
14071409 const addExecutorGoogleDiscoverySource = (input: {
14081410 rows: ControlPlaneStoreShape;
1411+ sourceStore: RuntimeSourceStore;
1412+ sourceMaterialization: RuntimeSourceMaterializationShape;
14091413 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;
14101414 storeSecretMaterial: StoreSecretMaterial;
14111415 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1419,8 +1423,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 7 unmodified lines
14191423 trimOrNull(input.sourceInput.discoveryUrl)
14201424 ?? defaultGoogleDiscoveryUrl(normalizedService, normalizedVersion),
14211425 );
1422− const existingSources = yield* loadSourcesInWorkspace(
1423− input.rows,
1426+ const existingSources = yield* input.sourceStore.loadSourcesInWorkspace(
14241427 input.sourceInput.workspaceId,
14251428 {
14261429 actorAccountId: input.sourceInput.actorAccountId,
@@ -1524,7 +1527,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 97 unmodified lines
15241527 now,
15251528 });
15261529
1527− const persistedDraft = yield* persistSource(input.rows, draftSource, {
1530+ const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {
15281531 actorAccountId: input.sourceInput.actorAccountId,
15291532 });
15301533
@@ -1547,6 +1550,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 16 unmodified lines
15471550 if (baseUrl) {
15481551 const oauthRequired = yield* startOauth2PkceSourceCredentialSetup({
15491552 rows: input.rows,
1553+ sourceStore: input.sourceStore,
15501554 source: persistedDraft,
15511555 actorAccountId: input.sourceInput.actorAccountId,
15521556 executionId: input.sourceInput.executionId,
@@ -1560,7 +1564,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 7 unmodified lines
15601564 }
15611565 }
15621566
1563− const authRequiredSource = yield* updateSourceStatus(input.rows, persistedDraft, {
1567+ const authRequiredSource = yield* updateSourceStatus(input.sourceStore, persistedDraft, {
15641568 actorAccountId: input.sourceInput.actorAccountId,
15651569 status: "auth_required",
15661570 lastError: null,
@@ -1574,21 +1578,19 @@ const addExecutorGoogleDiscoverySource = (input: { · 7 unmodified lines
15741578 }
15751579
15761580 const synced = yield* Effect.either(
1577− syncSourceMaterialization({
1578− rows: input.rows,
1581+ input.sourceMaterialization.sync({
15791582 source: {
15801583 ...persistedDraft,
15811584 status: "connected",
15821585 },
15831586 actorAccountId: input.sourceInput.actorAccountId,
1584− resolveSecretMaterial: input.resolveSecretMaterial,
15851587 }),
15861588 );
15871589
15881590 return yield* Either.match(synced, {
15891591 onLeft: (error) =>
15901592 isSourceCredentialRequiredError(error)
1591− ? updateSourceStatus(input.rows, persistedDraft, {
1593+ ? updateSourceStatus(input.sourceStore, persistedDraft, {
15921594 actorAccountId: input.sourceInput.actorAccountId,
15931595 status: "auth_required",
15941596 lastError: null,
@@ -1601,7 +1603,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 6 unmodified lines
16011603 } satisfies ExecutorSourceAddResult)
16021604 ),
16031605 )
1604− : updateSourceStatus(input.rows, persistedDraft, {
1606+ : updateSourceStatus(input.sourceStore, persistedDraft, {
16051607 actorAccountId: input.sourceInput.actorAccountId,
16061608 status: "error",
16071609 lastError: error.message,
@@ -1609,7 +1611,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 1 unmodified line
16091611 Effect.zipRight(Effect.fail(error)),
16101612 ),
16111613 onRight: () =>
1612− updateSourceStatus(input.rows, persistedDraft, {
1614+ updateSourceStatus(input.sourceStore, persistedDraft, {
16131615 actorAccountId: input.sourceInput.actorAccountId,
16141616 status: "connected",
16151617 lastError: null,
@@ -1668,19 +1670,13 @@ type RuntimeSourceAuthServiceShape = { · 52 unmodified lines
16681670 export const createRuntimeSourceAuthService = (input: {
16691671 rows: ControlPlaneStoreShape;
16701672 liveExecutionManager: LiveExecutionManager;
1673+ sourceStore: RuntimeSourceStore;
1674+ sourceMaterialization: RuntimeSourceMaterializationShape;
1675+ resolveSecretMaterial: ResolveSecretMaterial;
1676+ storeSecretMaterial: StoreSecretMaterial;
16711677 getLocalServerBaseUrl?: () => string | undefined;
1672− localConfig?: LocalExecutorConfig | null;
1673− workspaceRoot?: string | null;
16741678 localWorkspaceState?: RuntimeLocalWorkspaceState;
16751679 }) => {
1676− const resolveSecretMaterial = createDefaultSecretMaterialResolver({
1677− rows: input.rows,
1678− localConfig: input.localConfig,
1679− workspaceRoot: input.workspaceRoot,
1680− });
1681− const storeSecretMaterial = createDefaultSecretMaterialStorer({
1682− rows: input.rows,
1683− });
16841680 const mirrorLocalSourceResult = (
16851681 result: ExecutorSourceAddResult,
16861682 ): Effect.Effect<ExecutorSourceAddResult, Error, WorkspaceStorageServices> =>
@@ -1690,27 +1686,20 @@ export const createRuntimeSourceAuthService = (input: { · 3 unmodified lines
16901686 ): Effect.Effect<McpSourceConnectResult, Error, WorkspaceStorageServices> =>
16911687 Effect.succeed(result);
16921688 const provideLocalWorkspace = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1693− input.localWorkspaceState
1694− ? effect.pipe(
1695− Effect.provideService(
1696− RuntimeLocalWorkspaceService,
1697− input.localWorkspaceState,
1698− ),
1699− )
1700− : effect;
1689+ provideOptionalRuntimeLocalWorkspace(effect, input.localWorkspaceState);
17011690
17021691 return {
17031692 getLocalServerBaseUrl: () => input.getLocalServerBaseUrl?.() ?? null,
17041693
17051694 storeSecretMaterial: ({ purpose, value }) =>
1706− storeSecretMaterial({
1695+ input.storeSecretMaterial({
17071696 purpose,
17081697 value,
17091698 }),
17101699
17111700 getSourceById: ({ workspaceId, sourceId, actorAccountId }) =>
17121701 provideLocalWorkspace(
1713− loadSourceById(input.rows, {
1702+ input.sourceStore.loadSourceById({
17141703 workspaceId,
17151704 sourceId,
17161705 actorAccountId,
@@ -1722,26 +1711,32 @@ export const createRuntimeSourceAuthService = (input: { · 5 unmodified lines
17221711 (sourceInput.kind === "google_discovery"
17231712 ? addExecutorGoogleDiscoverySource({
17241713 rows: input.rows,
1714+ sourceStore: input.sourceStore,
1715+ sourceMaterialization: input.sourceMaterialization,
17251716 sourceInput,
1726− storeSecretMaterial,
1727− resolveSecretMaterial,
1717+ storeSecretMaterial: input.storeSecretMaterial,
1718+ resolveSecretMaterial: input.resolveSecretMaterial,
17281719 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17291720 baseUrl: options?.baseUrl,
17301721 })
17311722 : hasSourceAdapterFamily(sourceInput.kind ?? "mcp", "http_api")
17321723 ? addExecutorHttpSource({
17331724 rows: input.rows,
1725+ sourceStore: input.sourceStore,
1726+ sourceMaterialization: input.sourceMaterialization,
17341727 sourceInput: sourceInput as Extract<
17351728 ExecutorAddSourceInput,
17361729 { kind: "openapi" | "graphql" }
17371730 >,
1738− storeSecretMaterial,
1739− resolveSecretMaterial,
1731+ storeSecretMaterial: input.storeSecretMaterial,
1732+ resolveSecretMaterial: input.resolveSecretMaterial,
17401733 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17411734 baseUrl: options?.baseUrl,
17421735 })
17431736 : connectMcpSourceInternal({
17441737 rows: input.rows,
1738+ sourceStore: input.sourceStore,
1739+ sourceMaterialization: input.sourceMaterialization,
17451740 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17461741 workspaceId: sourceInput.workspaceId,
17471742 actorAccountId: sourceInput.actorAccountId,
@@ -1752,7 +1747,7 @@ export const createRuntimeSourceAuthService = (input: { · 4 unmodified lines
17521747 namespace: sourceInput.namespace,
17531748 mcpDiscoveryElicitation: options?.mcpDiscoveryElicitation,
17541749 baseUrl: options?.baseUrl,
1755− resolveSecretMaterial,
1750+ resolveSecretMaterial: input.resolveSecretMaterial,
17561751 })).pipe(
17571752 Effect.flatMap(mirrorLocalSourceResult),
17581753 ),
@@ -1762,6 +1757,8 @@ export const createRuntimeSourceAuthService = (input: { · 3 unmodified lines
17621757 provideLocalWorkspace(
17631758 connectMcpSourceInternal({
17641759 rows: input.rows,
1760+ sourceStore: input.sourceStore,
1761+ sourceMaterialization: input.sourceMaterialization,
17651762 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17661763 workspaceId: sourceInput.workspaceId,
17671764 actorAccountId: sourceInput.actorAccountId,
@@ -1776,7 +1773,7 @@ export const createRuntimeSourceAuthService = (input: { · 8 unmodified lines
17761773 queryParams: sourceInput.queryParams,
17771774 headers: sourceInput.headers,
17781775 baseUrl: sourceInput.baseUrl,
1779− resolveSecretMaterial,
1776+ resolveSecretMaterial: input.resolveSecretMaterial,
17801777 }).pipe(
17811778 Effect.flatMap(mirrorLocalMcpSourceResult),
17821779 ),
@@ -1912,13 +1909,13 @@ export const createRuntimeSourceAuthService = (input: { · 129 unmodified lines
19121909 displayName: readSourceOAuthSessionDisplayName(session.state),
19131910 endpoint: sessionData.endpoint,
19141911 });
1915− const accessTokenRef = yield* storeSecretMaterial({
1912+ const accessTokenRef = yield* input.storeSecretMaterial({
19161913 purpose: "oauth_access_token",
19171914 value: exchanged.tokens.access_token,
19181915 name: oauthSecretName,
19191916 });
19201917 const refreshTokenRef = exchanged.tokens.refresh_token
1921− ? yield* storeSecretMaterial({
1918+ ? yield* input.storeSecretMaterial({
19221919 purpose: "oauth_refresh_token",
19231920 value: exchanged.tokens.refresh_token,
19241921 name: `${oauthSecretName} Refresh`,
@@ -1991,7 +1988,7 @@ export const createRuntimeSourceAuthService = (input: { · 66 unmodified lines
19911988 );
19921989 }
19931990
1994− const source = yield* loadSourceById(input.rows, {
1991+ const source = yield* input.sourceStore.loadSourceById({
19951992 workspaceId: session.workspaceId,
19961993 sourceId: session.sourceId,
19971994 actorAccountId: session.actorAccountId,
@@ -2024,16 +2021,14 @@ export const createRuntimeSourceAuthService = (input: { · 26 unmodified lines
20242021 errorText: reason,
20252022 }),
20262023 );
2027− const failedSource = yield* updateSourceStatus(input.rows, source, {
2024+ const failedSource = yield* updateSourceStatus(input.sourceStore, source, {
20282025 actorAccountId: session.actorAccountId,
20292026 status: "error",
20302027 lastError: reason,
20312028 });
2032− yield* syncSourceMaterialization({
2033− rows: input.rows,
2029+ yield* input.sourceMaterialization.sync({
20342030 source: failedSource,
20352031 actorAccountId: session.actorAccountId,
2036− resolveSecretMaterial,
20372032 });
20382033 yield* completeLiveInteraction({
20392034 rows: input.rows,
@@ -2063,7 +2058,7 @@ export const createRuntimeSourceAuthService = (input: { · 23 unmodified lines
20632058 const oauthSessionData = decodeOauth2PkceSourceAuthSessionData(session);
20642059 let clientSecret: string | null = null;
20652060 if (oauthSessionData.clientSecret) {
2066− clientSecret = yield* resolveSecretMaterial({
2061+ clientSecret = yield* input.resolveSecretMaterial({
20672062 ref: oauthSessionData.clientSecret,
20682063 });
20692064 }
@@ -2084,7 +2079,7 @@ export const createRuntimeSourceAuthService = (input: { · 14 unmodified lines
20842079 );
20852080 }
20862081
2087− const refreshTokenRef = yield* storeSecretMaterial({
2082+ const refreshTokenRef = yield* input.storeSecretMaterial({
20882083 purpose: "oauth_refresh_token",
20892084 value: refreshToken,
20902085 name: `${source.name} Refresh`,
@@ -2092,7 +2087,7 @@ export const createRuntimeSourceAuthService = (input: { · 1 unmodified line
20922087 const grantedScopes = trimOrNull(exchanged.scope)
20932088 ? exchanged.scope!.split(/\s+/).filter((scope) => scope.length > 0)
20942089 : [...oauthSessionData.scopes];
2095− connectedSource = yield* updateSourceStatus(input.rows, source, {
2090+ connectedSource = yield* updateSourceStatus(input.sourceStore, source, {
20962091 actorAccountId: session.actorAccountId,
20972092 status: "connected",
20982093 lastError: null,
@@ -2157,20 +2152,20 @@ export const createRuntimeSourceAuthService = (input: { · 58 unmodified lines
21572152 displayName: source.name,
21582153 endpoint: source.endpoint,
21592154 });
2160− const accessTokenRef = yield* storeSecretMaterial({
2155+ const accessTokenRef = yield* input.storeSecretMaterial({
21612156 purpose: "oauth_access_token",
21622157 value: exchanged.tokens.access_token,
21632158 name: oauthSecretName,
21642159 });
21652160 const refreshTokenRef = exchanged.tokens.refresh_token
2166− ? yield* storeSecretMaterial({
2161+ ? yield* input.storeSecretMaterial({
21672162 purpose: "oauth_refresh_token",
21682163 value: exchanged.tokens.refresh_token,
21692164 name: `${oauthSecretName} Refresh`,
21702165 })
21712166 : null;
21722167
2173− connectedSource = yield* updateSourceStatus(input.rows, source, {
2168+ connectedSource = yield* updateSourceStatus(input.sourceStore, source, {
21742169 actorAccountId: session.actorAccountId,
21752170 status: "connected",
21762171 lastError: null,
@@ -2204,16 +2199,14 @@ export const createRuntimeSourceAuthService = (input: { · 27 unmodified lines
22042199 );
22052200 }
22062201 const indexed = yield* Effect.either(
2207− syncSourceMaterialization({
2208− rows: input.rows,
2202+ input.sourceMaterialization.sync({
22092203 source: connectedSource,
22102204 actorAccountId: session.actorAccountId,
2211− resolveSecretMaterial,
22122205 }),
22132206 );
22142207 yield* Either.match(indexed, {
22152208 onLeft: (error) =>
2216− updateSourceStatus(input.rows, connectedSource, {
2209+ updateSourceStatus(input.sourceStore, connectedSource, {
22172210 actorAccountId: session.actorAccountId,
22182211 status: "error",
22192212 lastError: error.message,
@@ -2245,23 +2238,27 @@ export class RuntimeSourceAuthServiceTag extends Context.Tag( · 25 unmodified lines
22452238
22462239 export const RuntimeSourceAuthServiceLive = (input: {
22472240 getLocalServerBaseUrl?: () => string | undefined;
2248− localConfig?: LocalExecutorConfig | null;
2249− workspaceRoot?: string | null;
2250− localWorkspaceState?: RuntimeLocalWorkspaceState;
22512241 } = {}) =>
22522242 Layer.effect(
22532243 RuntimeSourceAuthServiceTag,
22542244 Effect.gen(function* () {
22552245 const rows = yield* ControlPlaneStore;
22562246 const liveExecutionManager = yield* LiveExecutionManagerService;
2247+ const sourceStore = yield* RuntimeSourceStoreService;
2248+ const sourceMaterialization = yield* RuntimeSourceMaterializationService;
2249+ const resolveSecretMaterial = yield* SecretMaterialResolverService;
2250+ const storeSecretMaterial = yield* SecretMaterialStorerService;
2251+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
22572252
22582253 return createRuntimeSourceAuthService({
22592254 rows,
22602255 liveExecutionManager,
2256+ sourceStore,
2257+ sourceMaterialization,
2258+ resolveSecretMaterial,
2259+ storeSecretMaterial,
22612260 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
2262− localConfig: input.localConfig,
2263− workspaceRoot: input.workspaceRoot,
2264− localWorkspaceState: input.localWorkspaceState,
2261+ localWorkspaceState: runtimeLocalWorkspace ?? undefined,
22652262 });
22662263 }),
22672264 );
packages/control-plane/src/runtime/source-inspection.ts+5−9
@@ -26,14 +26,12 @@ import { · 25 unmodified lines
2626 getSourceAdapterForOperation,
2727 } from "./source-adapters";
2828 import {
29− loadSourceWithRecipe,
29+ RuntimeSourceRecipeStoreService,
3030 recipeToolMetadata,
3131 recipeToolPath,
3232 } from "./source-recipes-runtime";
3333 import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
3434 import { namespaceFromSourceName } from "./source-names";
35−import { loadSourceById } from "./source-store";
36−import { ControlPlaneStore } from "./store";
3735
3836 const sourceInspectOps = {
3937 bundle: operationErrors("sources.inspect.bundle"),
@@ -109,10 +107,9 @@ const loadSourceRecipeRecord = (input: { · 69 unmodified lines
109107 workspaceId: WorkspaceId;
110108 sourceId: SourceId;
111109 }) =>
112− Effect.flatMap(ControlPlaneStore, (store) =>
110+ Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) =>
113111 Effect.gen(function* () {
114− const recipe = yield* loadSourceWithRecipe({
115− rows: store,
112+ const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({
116113 workspaceId: input.workspaceId,
117114 sourceId: input.sourceId,
118115 }).pipe(
@@ -390,10 +387,9 @@ export const getSourceInspectionSchemaBundle = (input: { · 271 unmodified lines
390387 sourceId: SourceId;
391388 schemaBundleId: string;
392389 }) =>
393− Effect.flatMap(ControlPlaneStore, (store) =>
390+ Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) =>
394391 Effect.gen(function* () {
395− const recipe = yield* loadSourceWithRecipe({
396− rows: store,
392+ const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({
397393 workspaceId: input.workspaceId,
398394 sourceId: input.sourceId,
399395 }).pipe(
packages/control-plane/src/runtime/source-materialization.ts+165−43
@@ -3,51 +3,99 @@ import type { · 2 unmodified lines
33 Source,
44 SourceStatus,
55 } from "#schema";
6+import * as Context from "effect/Context";
67 import * as Effect from "effect/Effect";
8+import * as Layer from "effect/Layer";
79
8−import type { ControlPlaneStoreShape } from "./store";
910 import {
10− requireRuntimeLocalWorkspace,
11+ RuntimeLocalWorkspaceService,
12+ type RuntimeLocalWorkspaceState,
1113 } from "./local-runtime-context";
1214 import {
13− type WorkspaceStorageServices,
1415 SourceArtifactStore,
16+ type SourceArtifactStoreShape,
1517 WorkspaceStateStore,
18+ type WorkspaceStateStoreShape,
1619 } from "./local-storage";
1720 import {
1821 type LocalWorkspaceState,
1922 } from "./local-workspace-state";
20−import { resolveSourceAuthMaterial } from "./source-auth-material";
23+import {
24+ RuntimeSourceAuthMaterialService,
25+} from "./source-auth-material";
2126 import {
2227 getSourceAdapterForSource,
2328 } from "./source-adapters";
2429 import {
2530 materializationFromMcpManifestEntries,
2631 } from "./source-adapters/mcp";
27−import type {
28− ResolveSecretMaterial as ResolveSourceSecretMaterial,
29−} from "./secret-material-providers";
32+import { SecretMaterialResolverService } from "./secret-material-providers";
3033
3134 const shouldIndexSource = (source: Source): boolean =>
3235 source.enabled
3336 && source.status === "connected"
3437 && getSourceAdapterForSource(source).family !== "internal";
3538
36−export const syncSourceMaterialization = (input: {
37− rows: ControlPlaneStoreShape;
38− source: Source;
39− actorAccountId?: AccountId | null;
40− resolveSecretMaterial: ResolveSourceSecretMaterial;
41−}): Effect.Effect<void, Error, WorkspaceStorageServices> =>
39+type RuntimeSourceMaterializationDeps = {
40+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
41+ workspaceStateStore: WorkspaceStateStoreShape;
42+ sourceArtifactStore: SourceArtifactStoreShape;
43+ resolveSecretMaterial: Effect.Effect.Success<typeof SecretMaterialResolverService>;
44+ sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
45+};
46+
47+type SourceMaterializationServices =
48+ | RuntimeLocalWorkspaceService
49+ | WorkspaceStateStore
50+ | SourceArtifactStore
51+ | RuntimeSourceAuthMaterialService
52+ | SecretMaterialResolverService;
53+
54+export type RuntimeSourceMaterializationShape = {
55+ sync: (input: {
56+ source: Source;
57+ actorAccountId?: AccountId | null;
58+ }) => Effect.Effect<void, Error, never>;
59+ persistMcpRecipeMaterializationFromManifest: (input: {
60+ source: Source;
61+ manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
62+ }) => Effect.Effect<void, Error, never>;
63+};
64+
65+export class RuntimeSourceMaterializationService extends Context.Tag(
66+ "#runtime/RuntimeSourceMaterializationService",
67+)<RuntimeSourceMaterializationService, RuntimeSourceMaterializationShape>() {}
68+
69+const ensureRuntimeMaterializationWorkspace = (
70+ deps: RuntimeSourceMaterializationDeps,
71+ workspaceId: Source["workspaceId"],
72+) => {
73+ if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
74+ return Effect.fail(
75+ new Error(
76+ `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
77+ ),
78+ );
79+ }
80+
81+ return Effect.succeed(deps.runtimeLocalWorkspace.context);
82+};
83+
84+const syncSourceMaterializationWithDeps = (
85+ deps: RuntimeSourceMaterializationDeps,
86+ input: {
87+ source: Source;
88+ actorAccountId?: AccountId | null;
89+ },
90+): Effect.Effect<void, Error, never> =>
4291 Effect.gen(function* () {
43− const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
44− const workspaceStateStore = yield* WorkspaceStateStore;
45− const sourceArtifactStore = yield* SourceArtifactStore;
92+ const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(
93+ deps,
94+ input.source.workspaceId,
95+ );
4696
4797 if (!shouldIndexSource(input.source)) {
48− const state = yield* workspaceStateStore.load(
49− runtimeLocalWorkspace.context,
50− );
98+ const state = yield* deps.workspaceStateStore.load(workspaceContext);
5199 const existingSourceState = state.sources[input.source.id];
52100 const nextState: LocalWorkspaceState = {
53101 ...state,
@@ -62,8 +110,8 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
62110 },
63111 },
64112 };
65− yield* workspaceStateStore.write({
66− context: runtimeLocalWorkspace.context,
113+ yield* deps.workspaceStateStore.write({
114+ context: workspaceContext,
67115 state: nextState,
68116 });
69117 return;
@@ -72,28 +120,24 @@ export const syncSourceMaterialization = (input: { · 2 unmodified lines
72120 const adapter = getSourceAdapterForSource(input.source);
73121 const materialization = yield* adapter.materializeSource({
74122 source: input.source,
75− resolveSecretMaterial: input.resolveSecretMaterial,
123+ resolveSecretMaterial: deps.resolveSecretMaterial,
76124 resolveAuthMaterialForSlot: (slot) =>
77− resolveSourceAuthMaterial({
78− rows: input.rows,
125+ deps.sourceAuthMaterialService.resolve({
79126 source: input.source,
80127 slot,
81128 actorAccountId: input.actorAccountId,
82− resolveSecretMaterial: input.resolveSecretMaterial,
83129 }),
84130 });
85− yield* sourceArtifactStore.write({
86− context: runtimeLocalWorkspace.context,
131+ yield* deps.sourceArtifactStore.write({
132+ context: workspaceContext,
87133 sourceId: input.source.id,
88− artifact: sourceArtifactStore.build({
134+ artifact: deps.sourceArtifactStore.build({
89135 source: input.source,
90136 materialization,
91137 }),
92138 });
93139
94− const state = yield* workspaceStateStore.load(
95− runtimeLocalWorkspace.context,
96− );
140+ const state = yield* deps.workspaceStateStore.load(workspaceContext);
97141 const existingSourceState = state.sources[input.source.id];
98142 const nextState: LocalWorkspaceState = {
99143 ...state,
@@ -108,32 +152,110 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
108152 },
109153 },
110154 };
111− yield* workspaceStateStore.write({
112− context: runtimeLocalWorkspace.context,
155+ yield* deps.workspaceStateStore.write({
156+ context: workspaceContext,
113157 state: nextState,
114158 });
115159 });
116160
117−export const persistMcpRecipeMaterializationFromManifest = (input: {
118− rows: ControlPlaneStoreShape;
119− source: Source;
120− manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
121−}): Effect.Effect<void, Error, WorkspaceStorageServices> =>
161+const persistMcpRecipeMaterializationFromManifestWithDeps = (
162+ deps: RuntimeSourceMaterializationDeps,
163+ input: {
164+ source: Source;
165+ manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
166+ },
167+): Effect.Effect<void, Error, never> =>
122168 Effect.gen(function* () {
123− const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
124− const sourceArtifactStore = yield* SourceArtifactStore;
169+ const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(
170+ deps,
171+ input.source.workspaceId,
172+ );
125173 const materialization = materializationFromMcpManifestEntries({
126174 recipeRevisionId: "src_recipe_rev_materialization" as never,
127175 endpoint: input.source.endpoint,
128176 manifestEntries: input.manifestEntries,
129177 });
130178
131− yield* sourceArtifactStore.write({
132− context: runtimeLocalWorkspace.context,
179+ yield* deps.sourceArtifactStore.write({
180+ context: workspaceContext,
133181 sourceId: input.source.id,
134− artifact: sourceArtifactStore.build({
182+ artifact: deps.sourceArtifactStore.build({
135183 source: input.source,
136184 materialization,
137185 }),
138186 });
139187 });
188+
189+export const syncSourceMaterialization = (input: {
190+ source: Source;
191+ actorAccountId?: AccountId | null;
192+}): Effect.Effect<void, Error, SourceMaterializationServices> =>
193+ Effect.gen(function* () {
194+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
195+ const workspaceStateStore = yield* WorkspaceStateStore;
196+ const sourceArtifactStore = yield* SourceArtifactStore;
197+ const resolveSecretMaterial = yield* SecretMaterialResolverService;
198+ const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
199+
200+ return yield* syncSourceMaterializationWithDeps(
201+ {
202+ runtimeLocalWorkspace,
203+ workspaceStateStore,
204+ sourceArtifactStore,
205+ resolveSecretMaterial,
206+ sourceAuthMaterialService,
207+ },
208+ {
209+ source: input.source,
210+ actorAccountId: input.actorAccountId,
211+ },
212+ );
213+ });
214+
215+export const persistMcpRecipeMaterializationFromManifest = (input: {
216+ source: Source;
217+ manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
218+}): Effect.Effect<void, Error, SourceMaterializationServices> =>
219+ Effect.gen(function* () {
220+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
221+ const workspaceStateStore = yield* WorkspaceStateStore;
222+ const sourceArtifactStore = yield* SourceArtifactStore;
223+ const resolveSecretMaterial = yield* SecretMaterialResolverService;
224+ const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
225+
226+ return yield* persistMcpRecipeMaterializationFromManifestWithDeps(
227+ {
228+ runtimeLocalWorkspace,
229+ workspaceStateStore,
230+ sourceArtifactStore,
231+ resolveSecretMaterial,
232+ sourceAuthMaterialService,
233+ },
234+ input,
235+ );
236+ });
237+
238+export const RuntimeSourceMaterializationLive = Layer.effect(
239+ RuntimeSourceMaterializationService,
240+ Effect.gen(function* () {
241+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
242+ const workspaceStateStore = yield* WorkspaceStateStore;
243+ const sourceArtifactStore = yield* SourceArtifactStore;
244+ const resolveSecretMaterial = yield* SecretMaterialResolverService;
245+ const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
246+
247+ const deps: RuntimeSourceMaterializationDeps = {
248+ runtimeLocalWorkspace,
249+ workspaceStateStore,
250+ sourceArtifactStore,
251+ resolveSecretMaterial,
252+ sourceAuthMaterialService,
253+ };
254+
255+ return RuntimeSourceMaterializationService.of({
256+ sync: (input) => syncSourceMaterializationWithDeps(deps, input),
257+ persistMcpRecipeMaterializationFromManifest: (input) =>
258+ persistMcpRecipeMaterializationFromManifestWithDeps(deps, input),
259+ });
260+ }),
261+);
packages/control-plane/src/runtime/source-recipes-runtime.ts+255−67
@@ -12,24 +12,29 @@ import type { · 11 unmodified lines
1212 StoredSourceRecipeRevisionRecord,
1313 WorkspaceId,
1414 } from "#schema";
15+import * as Context from "effect/Context";
1516 import * as Effect from "effect/Effect";
17+import * as Layer from "effect/Layer";
1618 import { LocalSourceArtifactMissingError } from "./local-errors";
1719 import {
18− requireRuntimeLocalWorkspace,
20+ RuntimeLocalWorkspaceService,
21+ type RuntimeLocalWorkspaceState,
1922 } from "./local-runtime-context";
2023 import {
21− type WorkspaceStorageServices,
2224 SourceArtifactStore,
25+ type SourceArtifactStoreShape,
2326 } from "./local-storage";
2427 import { namespaceFromSourceName } from "./source-names";
2528 import {
2629 getSourceAdapterForOperation,
2730 getSourceAdapterForSource,
2831 } from "./source-adapters";
29−import type { ControlPlaneStoreShape } from "./store";
3032 import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
3133 import { firstSchemaBundle } from "./source-adapters/shared";
32−import { loadSourceById, loadSourcesInWorkspace } from "./source-store";
34+import {
35+ RuntimeSourceStoreService,
36+ type RuntimeSourceStore,
37+} from "./source-store";
3338
3439 type RecipeManifest = unknown | null;
3540
@@ -167,46 +172,119 @@ const primarySchemaBundleForRevision = (input: { · 131 unmodified lines
167172 : null;
168173 };
169174
170−export const loadWorkspaceSourceRecipes = (input: {
171− rows: ControlPlaneStoreShape;
175+const sourceRecordFromRecipeArtifact = (input: {
176+ source: Source;
177+ artifact: {
178+ recipeId: StoredSourceRecord["recipeId"];
179+ revision: StoredSourceRecipeRevisionRecord;
180+ };
181+}): StoredSourceRecord => ({
182+ id: input.source.id,
183+ workspaceId: input.source.workspaceId,
184+ recipeId: input.artifact.recipeId,
185+ recipeRevisionId: input.artifact.revision.id,
186+ name: input.source.name,
187+ kind: input.source.kind,
188+ endpoint: input.source.endpoint,
189+ status: input.source.status,
190+ enabled: input.source.enabled,
191+ namespace: input.source.namespace,
192+ importAuthPolicy: input.source.importAuthPolicy,
193+ bindingConfigJson: getSourceAdapterForSource(input.source).serializeBindingConfig(input.source),
194+ sourceHash: input.source.sourceHash,
195+ lastError: input.source.lastError,
196+ createdAt: input.source.createdAt,
197+ updatedAt: input.source.updatedAt,
198+});
199+
200+type RuntimeSourceRecipeStoreShape = {
201+ loadWorkspaceSourceRecipes: (input: {
202+ workspaceId: WorkspaceId;
203+ actorAccountId?: AccountId | null;
204+ }) => Effect.Effect<readonly LoadedSourceRecipe[], Error, never>;
205+ loadSourceWithRecipe: (input: {
206+ workspaceId: WorkspaceId;
207+ sourceId: Source["id"];
208+ actorAccountId?: AccountId | null;
209+ }) => Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never>;
210+ loadWorkspaceSourceRecipeToolIndex: (input: {
211+ workspaceId: WorkspaceId;
212+ actorAccountId?: AccountId | null;
213+ includeSchemas: boolean;
214+ }) => Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, never>;
215+ loadWorkspaceSourceRecipeToolByPath: (input: {
216+ workspaceId: WorkspaceId;
217+ path: string;
218+ actorAccountId?: AccountId | null;
219+ includeSchemas: boolean;
220+ }) => Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, never>;
221+ loadWorkspaceSchemaBundle: (input: {
222+ workspaceId: WorkspaceId;
223+ id: string;
224+ }) => Effect.Effect<
225+ { id: string; kind: string; hash: string; refsJson: string } | null,
226+ Error,
227+ never
228+ >;
229+};
230+
231+export type RuntimeSourceRecipeStore = RuntimeSourceRecipeStoreShape;
232+
233+export class RuntimeSourceRecipeStoreService extends Context.Tag(
234+ "#runtime/RuntimeSourceRecipeStoreService",
235+)<RuntimeSourceRecipeStoreService, RuntimeSourceRecipeStoreShape>() {}
236+
237+type RuntimeSourceRecipeStoreDeps = {
238+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
239+ sourceStore: RuntimeSourceStore;
240+ sourceArtifactStore: SourceArtifactStoreShape;
241+};
242+
243+type SourceRecipeRuntimeServices =
244+ | RuntimeLocalWorkspaceService
245+ | RuntimeSourceStoreService
246+ | SourceArtifactStore;
247+
248+const ensureRuntimeRecipeWorkspace = (
249+ deps: RuntimeSourceRecipeStoreDeps,
250+ workspaceId: WorkspaceId,
251+) => {
252+ if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
253+ return Effect.fail(
254+ new Error(
255+ `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
256+ ),
257+ );
258+ }
259+
260+ return Effect.succeed(deps.runtimeLocalWorkspace.context);
261+};
262+
263+const loadWorkspaceSourceRecipesWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: {
172264 workspaceId: WorkspaceId;
173265 actorAccountId?: AccountId | null;
174−}): Effect.Effect<readonly LoadedSourceRecipe[], Error, WorkspaceStorageServices> =>
266+}): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> =>
175267 Effect.gen(function* () {
176− const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
177− const sourceArtifactStore = yield* SourceArtifactStore;
178− const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, {
179− actorAccountId: input.actorAccountId,
180− });
268+ const workspaceContext = yield* ensureRuntimeRecipeWorkspace(
269+ deps,
270+ input.workspaceId,
271+ );
272+ const sources = yield* deps.sourceStore.loadSourcesInWorkspace(
273+ input.workspaceId,
274+ {
275+ actorAccountId: input.actorAccountId,
276+ },
277+ );
181278 const localRecipes = yield* Effect.forEach(sources, (source) =>
182279 Effect.gen(function* () {
183− const artifact = yield* sourceArtifactStore.read({
184− context: runtimeLocalWorkspace.context,
280+ const artifact = yield* deps.sourceArtifactStore.read({
281+ context: workspaceContext,
185282 sourceId: source.id,
186283 });
187284 if (artifact === null) {
188285 return null;
189286 }
190287
191− const sourceRecord: StoredSourceRecord = {
192− id: source.id,
193− workspaceId: source.workspaceId,
194− recipeId: artifact.recipeId,
195− recipeRevisionId: artifact.revision.id,
196− name: source.name,
197− kind: source.kind,
198− endpoint: source.endpoint,
199− status: source.status,
200− enabled: source.enabled,
201− namespace: source.namespace,
202− importAuthPolicy: source.importAuthPolicy,
203− bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
204− sourceHash: source.sourceHash,
205− lastError: source.lastError,
206− createdAt: source.createdAt,
207− updatedAt: source.updatedAt,
208− };
209−
210288 const manifest = yield* parseManifestForRecipe({
211289 source,
212290 revision: artifact.revision,
@@ -214,7 +292,7 @@ export const loadWorkspaceSourceRecipes = (input: { · 1 unmodified line
214292
215293 return {
216294 source,
217− sourceRecord,
295+ sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }),
218296 revision: artifact.revision,
219297 documents: artifact.documents,
220298 schemaBundles: artifact.schemaBundles,
@@ -226,22 +304,23 @@ export const loadWorkspaceSourceRecipes = (input: { · 5 unmodified lines
226304 return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null);
227305 });
228306
229−export const loadSourceWithRecipe = (input: {
230− rows: ControlPlaneStoreShape;
307+const loadSourceWithRecipeWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: {
231308 workspaceId: WorkspaceId;
232309 sourceId: Source["id"];
233310 actorAccountId?: AccountId | null;
234−}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, WorkspaceStorageServices> =>
311+}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> =>
235312 Effect.gen(function* () {
236− const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
237− const sourceArtifactStore = yield* SourceArtifactStore;
238− const source = yield* loadSourceById(input.rows, {
313+ const workspaceContext = yield* ensureRuntimeRecipeWorkspace(
314+ deps,
315+ input.workspaceId,
316+ );
317+ const source = yield* deps.sourceStore.loadSourceById({
239318 workspaceId: input.workspaceId,
240319 sourceId: input.sourceId,
241320 actorAccountId: input.actorAccountId,
242321 });
243− const artifact = yield* sourceArtifactStore.read({
244− context: runtimeLocalWorkspace.context,
322+ const artifact = yield* deps.sourceArtifactStore.read({
323+ context: workspaceContext,
245324 sourceId: source.id,
246325 });
247326 if (artifact === null) {
@@ -253,24 +332,6 @@ export const loadSourceWithRecipe = (input: { · 5 unmodified lines
253332 );
254333 }
255334
256− const sourceRecord: StoredSourceRecord = {
257− id: source.id,
258− workspaceId: source.workspaceId,
259− recipeId: artifact.recipeId,
260− recipeRevisionId: artifact.revision.id,
261− name: source.name,
262− kind: source.kind,
263− endpoint: source.endpoint,
264− status: source.status,
265− enabled: source.enabled,
266− namespace: source.namespace,
267− importAuthPolicy: source.importAuthPolicy,
268− bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
269− sourceHash: source.sourceHash,
270− lastError: source.lastError,
271− createdAt: source.createdAt,
272− updatedAt: source.updatedAt,
273− };
274335 const manifest = yield* parseManifestForRecipe({
275336 source,
276337 revision: artifact.revision,
@@ -278,7 +339,7 @@ export const loadSourceWithRecipe = (input: { · 1 unmodified line
278339
279340 return {
280341 source,
281− sourceRecord,
342+ sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }),
282343 revision: artifact.revision,
283344 documents: artifact.documents,
284345 schemaBundles: artifact.schemaBundles,
@@ -287,6 +348,49 @@ export const loadSourceWithRecipe = (input: { · 2 unmodified lines
287348 } satisfies LoadedSourceRecipe;
288349 });
289350
351+export const loadWorkspaceSourceRecipes = (input: {
352+ workspaceId: WorkspaceId;
353+ actorAccountId?: AccountId | null;
354+}): Effect.Effect<readonly LoadedSourceRecipe[], Error, SourceRecipeRuntimeServices> =>
355+ Effect.gen(function* () {
356+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
357+ const sourceStore = yield* RuntimeSourceStoreService;
358+ const sourceArtifactStore = yield* SourceArtifactStore;
359+
360+ return yield* loadWorkspaceSourceRecipesWithDeps(
361+ {
362+ runtimeLocalWorkspace,
363+ sourceStore,
364+ sourceArtifactStore,
365+ },
366+ input,
367+ );
368+ });
369+
370+export const loadSourceWithRecipe = (input: {
371+ workspaceId: WorkspaceId;
372+ sourceId: Source["id"];
373+ actorAccountId?: AccountId | null;
374+}): Effect.Effect<
375+ LoadedSourceRecipe,
376+ Error | LocalSourceArtifactMissingError,
377+ SourceRecipeRuntimeServices
378+> =>
379+ Effect.gen(function* () {
380+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
381+ const sourceStore = yield* RuntimeSourceStoreService;
382+ const sourceArtifactStore = yield* SourceArtifactStore;
383+
384+ return yield* loadSourceWithRecipeWithDeps(
385+ {
386+ runtimeLocalWorkspace,
387+ sourceStore,
388+ sourceArtifactStore,
389+ },
390+ input,
391+ );
392+ });
393+
290394 export const expandRecipeTools = (input: {
291395 recipes: readonly LoadedSourceRecipe[];
292396 includeSchemas: boolean;
@@ -348,14 +452,16 @@ export const expandRecipeTools = (input: { · 55 unmodified lines
348452 );
349453
350454 export const loadWorkspaceSourceRecipeToolIndex = (input: {
351− rows: ControlPlaneStoreShape;
352455 workspaceId: WorkspaceId;
353456 actorAccountId?: AccountId | null;
354457 includeSchemas: boolean;
355−}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> =>
458+}): Effect.Effect<
459+ readonly LoadedSourceRecipeToolIndexEntry[],
460+ Error,
461+ SourceRecipeRuntimeServices
462+> =>
356463 Effect.gen(function* () {
357464 const recipes = yield* loadWorkspaceSourceRecipes({
358− rows: input.rows,
359465 workspaceId: input.workspaceId,
360466 actorAccountId: input.actorAccountId,
361467 });
@@ -377,15 +483,17 @@ export const loadWorkspaceSourceRecipeToolIndex = (input: { · 15 unmodified lines
377483 });
378484
379485 export const loadWorkspaceSourceRecipeToolByPath = (input: {
380− rows: ControlPlaneStoreShape;
381486 workspaceId: WorkspaceId;
382487 path: string;
383488 actorAccountId?: AccountId | null;
384489 includeSchemas: boolean;
385−}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> =>
490+}): Effect.Effect<
491+ LoadedSourceRecipeToolIndexEntry | null,
492+ Error,
493+ SourceRecipeRuntimeServices
494+> =>
386495 Effect.gen(function* () {
387496 const recipes = yield* loadWorkspaceSourceRecipes({
388− rows: input.rows,
389497 workspaceId: input.workspaceId,
390498 actorAccountId: input.actorAccountId,
391499 });
@@ -408,3 +516,83 @@ export const loadWorkspaceSourceRecipeToolByPath = (input: { · 16 unmodified lines
408516 }
409517 : null;
410518 });
519+
520+export const RuntimeSourceRecipeStoreLive = Layer.effect(
521+ RuntimeSourceRecipeStoreService,
522+ Effect.gen(function* () {
523+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
524+ const sourceStore = yield* RuntimeSourceStoreService;
525+ const sourceArtifactStore = yield* SourceArtifactStore;
526+
527+ const deps: RuntimeSourceRecipeStoreDeps = {
528+ runtimeLocalWorkspace,
529+ sourceStore,
530+ sourceArtifactStore,
531+ };
532+
533+ return RuntimeSourceRecipeStoreService.of({
534+ loadWorkspaceSourceRecipes: (input) =>
535+ loadWorkspaceSourceRecipesWithDeps(deps, input),
536+ loadSourceWithRecipe: (input) =>
537+ loadSourceWithRecipeWithDeps(deps, input),
538+ loadWorkspaceSourceRecipeToolIndex: (input) =>
539+ Effect.gen(function* () {
540+ const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input);
541+ const tools = yield* expandRecipeTools({
542+ recipes,
543+ includeSchemas: input.includeSchemas,
544+ });
545+ return tools.map((tool) => ({
546+ path: tool.path,
547+ searchNamespace: tool.searchNamespace,
548+ searchText: tool.searchText,
549+ source: tool.source,
550+ sourceRecord: tool.sourceRecord,
551+ operation: tool.operation,
552+ metadata: tool.metadata,
553+ schemaBundleId: tool.schemaBundleId,
554+ descriptor: tool.descriptor,
555+ }));
556+ }),
557+ loadWorkspaceSourceRecipeToolByPath: (input) =>
558+ Effect.gen(function* () {
559+ const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input);
560+ const tools = yield* expandRecipeTools({
561+ recipes,
562+ includeSchemas: input.includeSchemas,
563+ });
564+ const tool = tools.find((entry) => entry.path === input.path) ?? null;
565+ return tool
566+ ? {
567+ path: tool.path,
568+ searchNamespace: tool.searchNamespace,
569+ searchText: tool.searchText,
570+ source: tool.source,
571+ sourceRecord: tool.sourceRecord,
572+ operation: tool.operation,
573+ metadata: tool.metadata,
574+ schemaBundleId: tool.schemaBundleId,
575+ descriptor: tool.descriptor,
576+ }
577+ : null;
578+ }),
579+ loadWorkspaceSchemaBundle: (input) =>
580+ Effect.gen(function* () {
581+ const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, {
582+ workspaceId: input.workspaceId,
583+ });
584+ const localBundle = recipes
585+ .flatMap((recipe) => recipe.schemaBundles)
586+ .find((schemaBundle) => schemaBundle.id === input.id);
587+ return localBundle
588+ ? {
589+ id: localBundle.id,
590+ kind: localBundle.bundleKind,
591+ hash: localBundle.contentHash,
592+ refsJson: localBundle.refsJson,
593+ }
594+ : null;
595+ }),
596+ });
597+ }),
598+);
packages/control-plane/src/runtime/source-store.ts+306−55
@@ -9,7 +9,9 @@ import type { · 8 unmodified lines
99 WorkspaceId,
1010 } from "#schema";
1111 import { SourceIdSchema } from "#schema";
12+import * as Context from "effect/Context";
1213 import * as Effect from "effect/Effect";
14+import * as Layer from "effect/Layer";
1315
1416 import {
1517 stableSourceRecipeId,
@@ -34,6 +36,8 @@ import { · 18 unmodified lines
3436 } from "./local-errors";
3537 import {
3638 requireRuntimeLocalWorkspace,
39+ RuntimeLocalWorkspaceService,
40+ type RuntimeLocalWorkspaceState,
3741 } from "./local-runtime-context";
3842 import type {
3943 SourceArtifactStoreShape,
@@ -56,7 +60,7 @@ import { · 16 unmodified lines
5660 import { createDefaultSecretMaterialDeleter } from "./secret-material-providers";
5761 import { authArtifactSecretMaterialRefs } from "./auth-artifacts";
5862 import { removeAuthLeaseAndSecrets } from "./auth-leases";
59−import type { ControlPlaneStoreShape } from "./store";
63+import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
6064 import { getSourceAdapter } from "./source-adapters";
6165 import { slugify } from "./slug";
6266
@@ -145,6 +149,27 @@ const deriveLocalSourceId = ( · 82 unmodified lines
145149 return SourceIdSchema.make(candidate);
146150 };
147151
152+type RuntimeSourceStoreDeps = {
153+ rows: ControlPlaneStoreShape;
154+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
155+ workspaceConfigStore: WorkspaceConfigStoreShape;
156+ workspaceStateStore: WorkspaceStateStoreShape;
157+ sourceArtifactStore: SourceArtifactStoreShape;
158+};
159+
160+type ResolvedSourceStoreWorkspace = {
161+ context: ResolvedLocalWorkspaceContext;
162+ installation: {
163+ workspaceId: WorkspaceId;
164+ accountId: AccountId;
165+ };
166+ workspaceConfigStore: WorkspaceConfigStoreShape;
167+ workspaceStateStore: WorkspaceStateStoreShape;
168+ sourceArtifactStore: SourceArtifactStoreShape;
169+ loadedConfig: LoadedLocalExecutorConfig;
170+ workspaceState: LocalWorkspaceState;
171+};
172+
148173 const resolveLocalConfigSecretProviderAlias = (config: LoadedLocalExecutorConfig["config"]): string | null => {
149174 const defaultAlias = trimOrNull(config?.secrets?.defaults?.env);
150175 if (defaultAlias !== null && config?.secrets?.providers?.[defaultAlias]) {
@@ -243,45 +268,71 @@ const configAuthFromSource = (input: { · 92 unmodified lines
243268 return input.existingConfigAuth;
244269 };
245270
246−const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{
247− context: ResolvedLocalWorkspaceContext;
248− installation: {
249− workspaceId: WorkspaceId;
250− accountId: AccountId;
251− };
252− workspaceConfigStore: WorkspaceConfigStoreShape;
253− workspaceStateStore: WorkspaceStateStoreShape;
254− sourceArtifactStore: SourceArtifactStoreShape;
255− loadedConfig: LoadedLocalExecutorConfig;
256− workspaceState: LocalWorkspaceState;
257−},
271+const resolveRuntimeLocalWorkspaceFromDeps = (
272+ deps: RuntimeSourceStoreDeps,
273+ workspaceId: WorkspaceId,
274+): Effect.Effect<ResolvedSourceStoreWorkspace,
275+ | RuntimeLocalWorkspaceUnavailableError
276+ | RuntimeLocalWorkspaceMismatchError
277+ | LocalFileSystemError
278+ | LocalExecutorConfigDecodeError
279+ | LocalWorkspaceStateDecodeError
280+ | Error,
281+never> =>
282+ Effect.gen(function* () {
283+ if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
284+ return yield* Effect.fail(
285+ new RuntimeLocalWorkspaceMismatchError({
286+ message: `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
287+ requestedWorkspaceId: workspaceId,
288+ activeWorkspaceId: deps.runtimeLocalWorkspace.installation.workspaceId,
289+ }),
290+ );
291+ }
292+
293+ const loadedConfig = yield* deps.workspaceConfigStore.load(
294+ deps.runtimeLocalWorkspace.context,
295+ );
296+ const workspaceState = yield* deps.workspaceStateStore.load(
297+ deps.runtimeLocalWorkspace.context,
298+ );
299+
300+ return {
301+ context: deps.runtimeLocalWorkspace.context,
302+ installation: deps.runtimeLocalWorkspace.installation,
303+ workspaceConfigStore: deps.workspaceConfigStore,
304+ workspaceStateStore: deps.workspaceStateStore,
305+ sourceArtifactStore: deps.sourceArtifactStore,
306+ loadedConfig,
307+ workspaceState,
308+ };
309+ });
310+
311+const loadRuntimeSourceStoreDeps = (
312+ rows: ControlPlaneStoreShape,
313+ workspaceId: WorkspaceId,
314+): Effect.Effect<
315+ RuntimeSourceStoreDeps,
258316 | RuntimeLocalWorkspaceUnavailableError
259317 | RuntimeLocalWorkspaceMismatchError
260318 | LocalFileSystemError
261319 | LocalExecutorConfigDecodeError
262320 | LocalWorkspaceStateDecodeError
263321 | Error,
264−WorkspaceStorageServices> =>
322+ WorkspaceStorageServices
323+> =>
265324 Effect.gen(function* () {
266325 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
267326 const workspaceConfigStore = yield* WorkspaceConfigStore;
268327 const workspaceStateStore = yield* WorkspaceStateStore;
269328 const sourceArtifactStore = yield* SourceArtifactStore;
270− const loadedConfig = yield* workspaceConfigStore.load(
271− runtimeLocalWorkspace.context,
272− );
273− const workspaceState = yield* workspaceStateStore.load(
274− runtimeLocalWorkspace.context,
275− );
276329
277330 return {
278− context: runtimeLocalWorkspace.context,
279− installation: runtimeLocalWorkspace.installation,
331+ rows,
332+ runtimeLocalWorkspace,
280333 workspaceConfigStore,
281334 workspaceStateStore,
282335 sourceArtifactStore,
283− loadedConfig,
284− workspaceState,
285336 };
286337 });
287338
@@ -396,8 +447,8 @@ const buildLocalSourceRecord = (input: { · 108 unmodified lines
396447 };
397448 });
398449
399−export const loadSourcesInWorkspace = (
400− rows: ControlPlaneStoreShape,
450+const loadSourcesInWorkspaceWithDeps = (
451+ deps: RuntimeSourceStoreDeps,
401452 workspaceId: WorkspaceId,
402453 options: {
403454 actorAccountId?: AccountId | null;
@@ -411,11 +462,11 @@ export const loadSourcesInWorkspace = ( · 7 unmodified lines
411462 | LocalWorkspaceStateDecodeError
412463 | LocalConfiguredSourceNotFoundError
413464 | Error,
414− WorkspaceStorageServices
465+ never
415466 > =>
416467 Effect.gen(function* () {
417− const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);
418− const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
468+ const localWorkspace = yield* resolveRuntimeLocalWorkspaceFromDeps(deps, workspaceId);
469+ const authArtifacts = yield* deps.rows.authArtifacts.listByWorkspaceId(workspaceId);
419470 return yield* Effect.forEach(
420471 Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}),
421472 (sourceId) =>
@@ -435,7 +486,104 @@ export const loadSourcesInWorkspace = ( · 13 unmodified lines
435486 );
436487 });
437488
438−export const loadSourceById = (rows: ControlPlaneStoreShape, input: {
489+export const loadSourcesInWorkspace = (
490+ rows: ControlPlaneStoreShape,
491+ workspaceId: WorkspaceId,
492+ options: {
493+ actorAccountId?: AccountId | null;
494+ } = {},
495+): Effect.Effect<
496+ readonly Source[],
497+ | RuntimeLocalWorkspaceUnavailableError
498+ | RuntimeLocalWorkspaceMismatchError
499+ | LocalFileSystemError
500+ | LocalExecutorConfigDecodeError
501+ | LocalWorkspaceStateDecodeError
502+ | LocalConfiguredSourceNotFoundError
503+ | Error,
504+ WorkspaceStorageServices
505+> =>
506+ Effect.flatMap(
507+ loadRuntimeSourceStoreDeps(rows, workspaceId),
508+ (deps) => loadSourcesInWorkspaceWithDeps(deps, workspaceId, options),
509+ );
510+
511+const listLinkedSecretSourcesInWorkspaceWithDeps = (
512+ deps: RuntimeSourceStoreDeps,
513+ workspaceId: WorkspaceId,
514+ options: {
515+ actorAccountId?: AccountId | null;
516+ } = {},
517+): Effect.Effect<
518+ Map<string, Array<{ sourceId: string; sourceName: string }>>,
519+ | RuntimeLocalWorkspaceUnavailableError
520+ | RuntimeLocalWorkspaceMismatchError
521+ | LocalFileSystemError
522+ | LocalExecutorConfigDecodeError
523+ | LocalWorkspaceStateDecodeError
524+ | LocalConfiguredSourceNotFoundError
525+ | Error,
526+ never
527+> =>
528+ Effect.gen(function* () {
529+ const [sources, authArtifacts, materialIds] = yield* Effect.all([
530+ loadSourcesInWorkspaceWithDeps(deps, workspaceId, {
531+ actorAccountId: options.actorAccountId,
532+ }),
533+ deps.rows.authArtifacts.listByWorkspaceId(workspaceId),
534+ deps.rows.secretMaterials.listAll().pipe(
535+ Effect.map((materials) => new Set(materials.map((material) => String(material.id)))),
536+ ),
537+ ]);
538+
539+ const sourceNames = new Map(
540+ sources.map((source) => [source.id, source.name] as const),
541+ );
542+ const linkedSources = new Map<string, Array<{ sourceId: string; sourceName: string }>>();
543+
544+ for (const artifact of authArtifacts) {
545+ for (const ref of authArtifactSecretMaterialRefs(artifact)) {
546+ if (!materialIds.has(ref.handle)) {
547+ continue;
548+ }
549+
550+ const existing = linkedSources.get(ref.handle) ?? [];
551+ if (!existing.some((link) => link.sourceId === artifact.sourceId)) {
552+ existing.push({
553+ sourceId: artifact.sourceId,
554+ sourceName: sourceNames.get(artifact.sourceId) ?? artifact.sourceId,
555+ });
556+ linkedSources.set(ref.handle, existing);
557+ }
558+ }
559+ }
560+
561+ return linkedSources;
562+ });
563+
564+export const listLinkedSecretSourcesInWorkspace = (
565+ rows: ControlPlaneStoreShape,
566+ workspaceId: WorkspaceId,
567+ options: {
568+ actorAccountId?: AccountId | null;
569+ } = {},
570+): Effect.Effect<
571+ Map<string, Array<{ sourceId: string; sourceName: string }>>,
572+ | RuntimeLocalWorkspaceUnavailableError
573+ | RuntimeLocalWorkspaceMismatchError
574+ | LocalFileSystemError
575+ | LocalExecutorConfigDecodeError
576+ | LocalWorkspaceStateDecodeError
577+ | LocalConfiguredSourceNotFoundError
578+ | Error,
579+ WorkspaceStorageServices
580+> =>
581+ Effect.flatMap(
582+ loadRuntimeSourceStoreDeps(rows, workspaceId),
583+ (deps) => listLinkedSecretSourcesInWorkspaceWithDeps(deps, workspaceId, options),
584+ );
585+
586+const loadSourceByIdWithDeps = (deps: RuntimeSourceStoreDeps, input: {
439587 workspaceId: WorkspaceId;
440588 sourceId: Source["id"];
441589 actorAccountId?: AccountId | null;
@@ -448,11 +596,11 @@ export const loadSourceById = (rows: ControlPlaneStoreShape, input: { · 6 unmodified lines
448596 | LocalWorkspaceStateDecodeError
449597 | LocalConfiguredSourceNotFoundError
450598 | Error,
451− WorkspaceStorageServices
599+ never
452600 > =>
453601 Effect.gen(function* () {
454− const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
455− const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
602+ const localWorkspace = yield* resolveRuntimeLocalWorkspaceFromDeps(deps, input.workspaceId);
603+ const authArtifacts = yield* deps.rows.authArtifacts.listByWorkspaceId(input.workspaceId);
456604 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
457605 return yield* Effect.fail(
458606 new LocalConfiguredSourceNotFoundError({
@@ -476,6 +624,26 @@ export const loadSourceById = (rows: ControlPlaneStoreShape, input: { · 17 unmodified lines
476624 return localSource.source;
477625 });
478626
627+export const loadSourceById = (rows: ControlPlaneStoreShape, input: {
628+ workspaceId: WorkspaceId;
629+ sourceId: Source["id"];
630+ actorAccountId?: AccountId | null;
631+}): Effect.Effect<
632+ Source,
633+ | RuntimeLocalWorkspaceUnavailableError
634+ | RuntimeLocalWorkspaceMismatchError
635+ | LocalFileSystemError
636+ | LocalExecutorConfigDecodeError
637+ | LocalWorkspaceStateDecodeError
638+ | LocalConfiguredSourceNotFoundError
639+ | Error,
640+ WorkspaceStorageServices
641+> =>
642+ Effect.flatMap(
643+ loadRuntimeSourceStoreDeps(rows, input.workspaceId),
644+ (deps) => loadSourceByIdWithDeps(deps, input),
645+ );
646+
479647 const configSourceFromLocalSource = (input: {
480648 source: Source;
481649 existingConfigAuth: LocalConfigSecretInput | undefined;
@@ -571,12 +739,12 @@ const removeAuthArtifactsForSource = (rows: ControlPlaneStoreShape, input: { · 89 unmodified lines
571739 return existingAuthArtifacts.length;
572740 });
573741
574−export const removeSourceById = (rows: ControlPlaneStoreShape, input: {
742+const removeSourceByIdWithDeps = (deps: RuntimeSourceStoreDeps, input: {
575743 workspaceId: WorkspaceId;
576744 sourceId: Source["id"];
577−}): Effect.Effect<boolean, Error, WorkspaceStorageServices> =>
745+}): Effect.Effect<boolean, Error, never> =>
578746 Effect.gen(function* () {
579− const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
747+ const localWorkspace = yield* resolveRuntimeLocalWorkspaceFromDeps(deps, input.workspaceId);
580748 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
581749 return false;
582750 }
@@ -611,28 +779,37 @@ export const removeSourceById = (rows: ControlPlaneStoreShape, input: { · 28 unmodified lines
611779 sourceId: input.sourceId,
612780 });
613781
614− yield* rows.sourceAuthSessions.removeByWorkspaceAndSourceId(
782+ yield* deps.rows.sourceAuthSessions.removeByWorkspaceAndSourceId(
615783 input.workspaceId,
616784 input.sourceId,
617785 );
618− yield* rows.sourceOauthClients.removeByWorkspaceAndSourceId({
786+ yield* deps.rows.sourceOauthClients.removeByWorkspaceAndSourceId({
619787 workspaceId: input.workspaceId,
620788 sourceId: input.sourceId,
621789 });
622− yield* removeAuthArtifactsForSource(rows, input);
790+ yield* removeAuthArtifactsForSource(deps.rows, input);
623791
624792 return true;
625793 });
626794
627−export const persistSource = (
628− rows: ControlPlaneStoreShape,
795+export const removeSourceById = (rows: ControlPlaneStoreShape, input: {
796+ workspaceId: WorkspaceId;
797+ sourceId: Source["id"];
798+}): Effect.Effect<boolean, Error, WorkspaceStorageServices> =>
799+ Effect.flatMap(
800+ loadRuntimeSourceStoreDeps(rows, input.workspaceId),
801+ (deps) => removeSourceByIdWithDeps(deps, input),
802+ );
803+
804+const persistSourceWithDeps = (
805+ deps: RuntimeSourceStoreDeps,
629806 source: Source,
630807 options: {
631808 actorAccountId?: AccountId | null;
632809 } = {},
633−): Effect.Effect<Source, Error, WorkspaceStorageServices> =>
810+): Effect.Effect<Source, Error, never> =>
634811 Effect.gen(function* () {
635− const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId);
812+ const localWorkspace = yield* resolveRuntimeLocalWorkspaceFromDeps(deps, source.workspaceId);
636813 const nextSource = {
637814 ...source,
638815 id:
@@ -644,7 +821,7 @@ export const persistSource = ( · 5 unmodified lines
644821 new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})),
645822 ),
646823 } satisfies Source;
647− const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({
824+ const existingAuthArtifacts = yield* deps.rows.authArtifacts.listByWorkspaceAndSourceId({
648825 workspaceId: nextSource.workspaceId,
649826 sourceId: nextSource.id,
650827 });
@@ -687,58 +864,58 @@ export const persistSource = ( · 36 unmodified lines
687864
688865 if (runtimeAuthArtifact === null) {
689866 if (existingRuntimeAuthArtifact !== null) {
690− yield* removeAuthLeaseAndSecrets(rows, {
867+ yield* removeAuthLeaseAndSecrets(deps.rows, {
691868 authArtifactId: existingRuntimeAuthArtifact.id,
692869 });
693870 }
694− yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
871+ yield* deps.rows.authArtifacts.removeByWorkspaceSourceAndActor({
695872 workspaceId: nextSource.workspaceId,
696873 sourceId: nextSource.id,
697874 actorAccountId: options.actorAccountId ?? null,
698875 slot: "runtime",
699876 });
700877 } else {
701− yield* rows.authArtifacts.upsert(runtimeAuthArtifact);
878+ yield* deps.rows.authArtifacts.upsert(runtimeAuthArtifact);
702879 if (
703880 existingRuntimeAuthArtifact !== null
704881 && existingRuntimeAuthArtifact.id !== runtimeAuthArtifact.id
705882 ) {
706− yield* removeAuthLeaseAndSecrets(rows, {
883+ yield* removeAuthLeaseAndSecrets(deps.rows, {
707884 authArtifactId: existingRuntimeAuthArtifact.id,
708885 });
709886 }
710887 }
711888
712− yield* cleanupAuthArtifactSecretRefs(rows, {
889+ yield* cleanupAuthArtifactSecretRefs(deps.rows, {
713890 previous: existingRuntimeAuthArtifact ?? null,
714891 next: runtimeAuthArtifact,
715892 });
716893
717894 if (importAuthArtifact === null) {
718895 if (existingImportAuthArtifact !== null) {
719− yield* removeAuthLeaseAndSecrets(rows, {
896+ yield* removeAuthLeaseAndSecrets(deps.rows, {
720897 authArtifactId: existingImportAuthArtifact.id,
721898 });
722899 }
723− yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
900+ yield* deps.rows.authArtifacts.removeByWorkspaceSourceAndActor({
724901 workspaceId: nextSource.workspaceId,
725902 sourceId: nextSource.id,
726903 actorAccountId: options.actorAccountId ?? null,
727904 slot: "import",
728905 });
729906 } else {
730− yield* rows.authArtifacts.upsert(importAuthArtifact);
907+ yield* deps.rows.authArtifacts.upsert(importAuthArtifact);
731908 if (
732909 existingImportAuthArtifact !== null
733910 && existingImportAuthArtifact.id !== importAuthArtifact.id
734911 ) {
735− yield* removeAuthLeaseAndSecrets(rows, {
912+ yield* removeAuthLeaseAndSecrets(deps.rows, {
736913 authArtifactId: existingImportAuthArtifact.id,
737914 });
738915 }
739916 }
740917
741− yield* cleanupAuthArtifactSecretRefs(rows, {
918+ yield* cleanupAuthArtifactSecretRefs(deps.rows, {
742919 previous: existingImportAuthArtifact ?? null,
743920 next: importAuthArtifact,
744921 });
@@ -762,9 +939,83 @@ export const persistSource = ( · 17 unmodified lines
762939 state: workspaceState,
763940 });
764941
765− return yield* loadSourceById(rows, {
942+ return yield* loadSourceByIdWithDeps(deps, {
766943 workspaceId: nextSource.workspaceId,
767944 sourceId: nextSource.id,
768945 actorAccountId: options.actorAccountId,
769946 });
770947 });
948+
949+export const persistSource = (
950+ rows: ControlPlaneStoreShape,
951+ source: Source,
952+ options: {
953+ actorAccountId?: AccountId | null;
954+ } = {},
955+): Effect.Effect<Source, Error, WorkspaceStorageServices> =>
956+ Effect.flatMap(
957+ loadRuntimeSourceStoreDeps(rows, source.workspaceId),
958+ (deps) => persistSourceWithDeps(deps, source, options),
959+ );
960+
961+type RuntimeSourceStoreShape = {
962+ loadSourcesInWorkspace: (
963+ workspaceId: WorkspaceId,
964+ options?: { actorAccountId?: AccountId | null },
965+ ) => ReturnType<typeof loadSourcesInWorkspaceWithDeps>;
966+ listLinkedSecretSourcesInWorkspace: (
967+ workspaceId: WorkspaceId,
968+ options?: { actorAccountId?: AccountId | null },
969+ ) => ReturnType<typeof listLinkedSecretSourcesInWorkspaceWithDeps>;
970+ loadSourceById: (input: {
971+ workspaceId: WorkspaceId;
972+ sourceId: Source["id"];
973+ actorAccountId?: AccountId | null;
974+ }) => ReturnType<typeof loadSourceByIdWithDeps>;
975+ removeSourceById: (input: {
976+ workspaceId: WorkspaceId;
977+ sourceId: Source["id"];
978+ }) => ReturnType<typeof removeSourceByIdWithDeps>;
979+ persistSource: (
980+ source: Source,
981+ options?: { actorAccountId?: AccountId | null },
982+ ) => ReturnType<typeof persistSourceWithDeps>;
983+};
984+
985+export type RuntimeSourceStore = RuntimeSourceStoreShape;
986+
987+export class RuntimeSourceStoreService extends Context.Tag(
988+ "#runtime/RuntimeSourceStoreService",
989+)<RuntimeSourceStoreService, RuntimeSourceStoreShape>() {}
990+
991+export const RuntimeSourceStoreLive = Layer.effect(
992+ RuntimeSourceStoreService,
993+ Effect.gen(function* () {
994+ const rows = yield* ControlPlaneStore;
995+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
996+ const workspaceConfigStore = yield* WorkspaceConfigStore;
997+ const workspaceStateStore = yield* WorkspaceStateStore;
998+ const sourceArtifactStore = yield* SourceArtifactStore;
999+
1000+ const deps: RuntimeSourceStoreDeps = {
1001+ rows,
1002+ runtimeLocalWorkspace,
1003+ workspaceConfigStore,
1004+ workspaceStateStore,
1005+ sourceArtifactStore,
1006+ };
1007+
1008+ return RuntimeSourceStoreService.of({
1009+ loadSourcesInWorkspace: (workspaceId, options = {}) =>
1010+ loadSourcesInWorkspaceWithDeps(deps, workspaceId, options),
1011+ listLinkedSecretSourcesInWorkspace: (workspaceId, options = {}) =>
1012+ listLinkedSecretSourcesInWorkspaceWithDeps(deps, workspaceId, options),
1013+ loadSourceById: (input) =>
1014+ loadSourceByIdWithDeps(deps, input),
1015+ removeSourceById: (input) =>
1016+ removeSourceByIdWithDeps(deps, input),
1017+ persistSource: (source, options = {}) =>
1018+ persistSourceWithDeps(deps, source, options),
1019+ });
1020+ }),
1021+);
packages/control-plane/src/runtime/sources-operations.ts+22−20
@@ -24,14 +24,10 @@ import { · 23 unmodified lines
2424 import {
2525 operationErrors,
2626 } from "./operation-errors";
27−import { createDefaultSecretMaterialResolver } from "./secret-material-providers";
2827 import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
29−import { syncSourceMaterialization } from "./source-materialization";
28+import { RuntimeSourceMaterializationService } from "./source-materialization";
3029 import {
31− loadSourceById,
32− loadSourcesInWorkspace,
33− persistSource,
34− removeSourceById,
30+ RuntimeSourceStoreService,
3531 } from "./source-store";
3632
3733 const sourceOps = {
@@ -47,6 +43,7 @@ const shouldAutoProbeSource = (source: Source): boolean => · 9 unmodified lines
4743
4844 const syncArtifactsForSource = (input: {
4945 store: ControlPlaneStoreShape;
46+ sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
5047 source: Source;
5148 actorAccountId: AccountId;
5249 operation:
@@ -54,9 +51,7 @@ const syncArtifactsForSource = (input: { · 1 unmodified line
5451 | typeof sourceOps.update;
5552 }) =>
5653 Effect.gen(function* () {
57− const resolveSecretMaterial = createDefaultSecretMaterialResolver({
58− rows: input.store,
59− });
54+ const materializationService = yield* RuntimeSourceMaterializationService;
6055
6156 // For HTTP-backed source kinds that can validate themselves from a remote
6257 // document, automatically attempt to probe and connect. This mirrors the
@@ -68,11 +63,9 @@ const syncArtifactsForSource = (input: { · 5 unmodified lines
6863 : input.source;
6964
7065 const synced = yield* Effect.either(
71− syncSourceMaterialization({
72− rows: input.store,
66+ materializationService.sync({
7367 source: sourceForSync,
7468 actorAccountId: input.actorAccountId,
75− resolveSecretMaterial,
7669 }),
7770 );
7871
@@ -94,7 +87,7 @@ const syncArtifactsForSource = (input: { · 15 unmodified lines
9487 );
9588 yield* mapPersistenceError(
9689 input.operation.child("source_connected"),
97− persistSource(input.store, connectedSource, {
90+ input.sourceStore.persistSource(connectedSource, {
9891 actorAccountId: input.actorAccountId,
9992 }),
10093 );
@@ -123,7 +116,7 @@ const syncArtifactsForSource = (input: { · 22 unmodified lines
123116
124117 yield* mapPersistenceError(
125118 input.operation.child("source_error"),
126− persistSource(input.store, erroredSource, {
119+ input.sourceStore.persistSource(erroredSource, {
127120 actorAccountId: input.actorAccountId,
128121 }),
129122 );
@@ -142,7 +135,9 @@ export const listSources = (input: { · 12 unmodified lines
142135 }) =>
143136 Effect.flatMap(ControlPlaneStore, (store) =>
144137 Effect.gen(function* () {
145− return yield* loadSourcesInWorkspace(store, input.workspaceId, {
138+ const sourceStore = yield* RuntimeSourceStoreService;
139+
140+ return yield* sourceStore.loadSourcesInWorkspace(input.workspaceId, {
146141 actorAccountId: input.accountId,
147142 }).pipe(
148143 Effect.mapError((error) =>
@@ -161,6 +156,7 @@ export const createSource = (input: { · 12 unmodified lines
161156 }) =>
162157 Effect.flatMap(ControlPlaneStore, (store) =>
163158 Effect.gen(function* () {
159+ const sourceStore = yield* RuntimeSourceStoreService;
164160 const now = Date.now();
165161
166162 const source = yield* createSourceFromPayload({
@@ -179,13 +175,14 @@ export const createSource = (input: { · 12 unmodified lines
179175
180176 const persistedSource = yield* mapPersistenceError(
181177 sourceOps.create.child("persist"),
182− persistSource(store, source, {
178+ sourceStore.persistSource(source, {
183179 actorAccountId: input.accountId,
184180 }),
185181 );
186182
187183 const synchronizedSource = yield* syncArtifactsForSource({
188184 store,
185+ sourceStore,
189186 source: persistedSource,
190187 actorAccountId: input.accountId,
191188 operation: sourceOps.create,
@@ -201,7 +198,9 @@ export const getSource = (input: { · 9 unmodified lines
201198 }) =>
202199 Effect.flatMap(ControlPlaneStore, (store) =>
203200 Effect.gen(function* () {
204− return yield* loadSourceById(store, {
201+ const sourceStore = yield* RuntimeSourceStoreService;
202+
203+ return yield* sourceStore.loadSourceById({
205204 workspaceId: input.workspaceId,
206205 sourceId: input.sourceId,
207206 actorAccountId: input.accountId,
@@ -228,7 +227,8 @@ export const updateSource = (input: { · 20 unmodified lines
228227 }) =>
229228 Effect.flatMap(ControlPlaneStore, (store) =>
230229 Effect.gen(function* () {
231− const existingSource = yield* loadSourceById(store, {
230+ const sourceStore = yield* RuntimeSourceStoreService;
231+ const existingSource = yield* sourceStore.loadSourceById({
232232 workspaceId: input.workspaceId,
233233 sourceId: input.sourceId,
234234 actorAccountId: input.accountId,
@@ -261,13 +261,14 @@ export const updateSource = (input: { · 26 unmodified lines
261261
262262 const persistedSource = yield* mapPersistenceError(
263263 sourceOps.update.child("persist"),
264− persistSource(store, updatedSource, {
264+ sourceStore.persistSource(updatedSource, {
265265 actorAccountId: input.accountId,
266266 }),
267267 );
268268
269269 const synchronizedSource = yield* syncArtifactsForSource({
270270 store,
271+ sourceStore,
271272 source: persistedSource,
272273 actorAccountId: input.accountId,
273274 operation: sourceOps.update,
@@ -282,9 +283,10 @@ export const removeSource = (input: { · 8 unmodified lines
282283 }) =>
283284 Effect.flatMap(ControlPlaneStore, (store) =>
284285 Effect.gen(function* () {
286+ const sourceStore = yield* RuntimeSourceStoreService;
285287 const removed = yield* mapPersistenceError(
286288 sourceOps.remove.child("remove"),
287− removeSourceById(store, {
289+ sourceStore.removeSourceById({
288290 workspaceId: input.workspaceId,
289291 sourceId: input.sourceId,
290292 }),
packages/control-plane/src/runtime/store.ts+0−5
@@ -124,11 +124,6 @@ export type ControlPlaneStoreShape = { · 123 unmodified lines
124124 never
125125 >;
126126 removeById: (id: SecretMaterial["id"]) => Effect.Effect<boolean, Error, never>;
127− listLinkedSources: () => Effect.Effect<
128− Map<string, Array<{ sourceId: string; sourceName: string }>>,
129− Error,
130− never
131− >;
132127 };
133128 executions: {
134129 getById: (
packages/control-plane/src/runtime/workspace-execution-environment.ts+63−85
@@ -29,19 +29,14 @@ import { · 28 unmodified lines
2929 type RuntimeSourceAuthService,
3030 } from "./source-auth-service";
3131 import {
32− loadSourceWithRecipe,
33− loadWorkspaceSourceRecipeToolByPath,
34− loadWorkspaceSourceRecipeToolIndex,
35− loadWorkspaceSourceRecipes,
32+ RuntimeSourceRecipeStoreService,
3633 type LoadedSourceRecipeToolIndexEntry,
3734 recipeToolCatalogEntry,
3835 } from "./source-recipes-runtime";
39−import { resolveSourceAuthMaterial } from "./source-auth-material";
36+import { RuntimeSourceAuthMaterialService } from "./source-auth-material";
4037 import { namespaceFromSourceName } from "./source-names";
4138 import { getSourceAdapterForOperation } from "./source-adapters";
4239 import {
43− createDefaultSecretMaterialResolver,
44− type ResolveSecretMaterial,
4540 type SecretMaterialResolveContext,
4641 } from "./secret-material-providers";
4742 import {
@@ -51,12 +46,17 @@ import { · 3 unmodified lines
5146 import { loadRuntimeLocalWorkspacePolicies } from "./policies-operations";
5247 import {
5348 getRuntimeLocalWorkspaceOption,
54− RuntimeLocalWorkspaceService,
49+ provideOptionalRuntimeLocalWorkspace,
5550 type RuntimeLocalWorkspaceState,
5651 } from "./local-runtime-context";
57−import { loadLocalToolRuntime, type LocalToolRuntime } from "./local-tools";
52+import {
53+ LocalToolRuntimeLoaderService,
54+ type LocalToolRuntime,
55+ type LocalToolRuntimeLoaderShape,
56+} from "./local-tools";
5857 import {
5958 SourceArtifactStore,
59+ makeWorkspaceStorageLayer,
6060 type SourceArtifactStoreShape,
6161 WorkspaceConfigStore,
6262 type WorkspaceConfigStoreShape,
@@ -64,7 +64,6 @@ import { · 1 unmodified line
6464 type WorkspaceStateStoreShape,
6565 type WorkspaceStorageServices,
6666 } from "./local-storage";
67−import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
6867
6968 const asToolPath = (value: string): ToolPath => value as ToolPath;
7069
@@ -147,14 +146,13 @@ const queryTokenWeight = (token: string): number => · 76 unmodified lines
147146 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
148147
149148 const loadWorkspaceRecipeTools = (input: {
150− rows: ControlPlaneStoreShape;
151149 workspaceId: Source["workspaceId"];
152150 accountId: AccountId;
151+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
153152 includeSchemas: boolean;
154153 }): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> =>
155154 Effect.map(
156− loadWorkspaceSourceRecipeToolIndex({
157− rows: input.rows,
155+ input.sourceRecipeStore.loadWorkspaceSourceRecipeToolIndex({
158156 workspaceId: input.workspaceId,
159157 actorAccountId: input.accountId,
160158 includeSchemas: input.includeSchemas,
@@ -166,14 +164,13 @@ const loadWorkspaceRecipeTools = (input: { · 5 unmodified lines
166164 );
167165
168166 const loadWorkspaceRecipeToolByPath = (input: {
169− rows: ControlPlaneStoreShape;
170167 workspaceId: Source["workspaceId"];
171168 accountId: AccountId;
169+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
172170 path: string;
173171 includeSchemas: boolean;
174172 }): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> =>
175− loadWorkspaceSourceRecipeToolByPath({
176− rows: input.rows,
173+ input.sourceRecipeStore.loadWorkspaceSourceRecipeToolByPath({
177174 workspaceId: input.workspaceId,
178175 path: input.path,
179176 actorAccountId: input.accountId,
@@ -187,27 +184,25 @@ const loadWorkspaceRecipeToolByPath = (input: { · 7 unmodified lines
187184 );
188185
189186 const loadWorkspaceSchemaBundle = (input: {
190− rows: ControlPlaneStoreShape;
191187 workspaceId: Source["workspaceId"];
188+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
192189 id: SourceRecipeSchemaBundleId;
193190 }): Effect.Effect<ToolSchemaBundle | null, Error, WorkspaceStorageServices> =>
194− Effect.gen(function* () {
195− const recipes = yield* loadWorkspaceSourceRecipes({
196− rows: input.rows,
191+ Effect.map(
192+ input.sourceRecipeStore.loadWorkspaceSchemaBundle({
197193 workspaceId: input.workspaceId,
198− });
199− const localBundle = recipes
200− .flatMap((recipe) => recipe.schemaBundles)
201− .find((schemaBundle) => schemaBundle.id === input.id);
202− return localBundle
203− ? {
204− id: localBundle.id,
205− kind: localBundle.bundleKind,
206− hash: localBundle.contentHash,
207− refsJson: localBundle.refsJson,
208− }
209− : null;
210− });
194+ id: input.id,
195+ }),
196+ (bundle) =>
197+ bundle
198+ ? {
199+ id: bundle.id,
200+ kind: bundle.kind as ToolSchemaBundle["kind"],
201+ hash: bundle.hash,
202+ refsJson: bundle.refsJson,
203+ }
204+ : null,
205+ );
211206
212207 const scoreRecipeTool = (
213208 queryTokens: readonly string[],
@@ -363,7 +358,6 @@ const toInvocationDescriptorFromRecipeTool = (input: { · 149 unmodified lines
363358 });
364359
365360 const authorizePersistedToolInvocation = (input: {
366− rows: ControlPlaneStoreShape;
367361 workspaceId: Source["workspaceId"];
368362 accountId: AccountId;
369363 descriptor: InvocationDescriptor;
@@ -456,38 +450,31 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 86 unmodified lines
456450 effect: Effect.Effect<A, E, R>,
457451 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null,
458452 ): Effect.Effect<A, E, R> =>
459− runtimeLocalWorkspace === null
460− ? effect
461− : effect.pipe(
462− Effect.provideService(
463− RuntimeLocalWorkspaceService,
464− runtimeLocalWorkspace,
465− ),
466− );
453+ provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace);
467454
468455 const createWorkspaceRecipeCatalog = (input: {
469456 workspaceId: Source["workspaceId"];
470457 accountId: AccountId;
471− rows: ControlPlaneStoreShape;
458+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
472459 workspaceConfigStore: WorkspaceConfigStoreShape;
473460 workspaceStateStore: WorkspaceStateStoreShape;
474461 sourceArtifactStore: SourceArtifactStoreShape;
475462 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null;
476463 }): ToolCatalog => {
477− const workspaceStorageLayer = Layer.mergeAll(
478− Layer.succeed(WorkspaceConfigStore, input.workspaceConfigStore),
479− Layer.succeed(WorkspaceStateStore, input.workspaceStateStore),
480− Layer.succeed(SourceArtifactStore, input.sourceArtifactStore),
481− );
464+ const workspaceStorageLayer = makeWorkspaceStorageLayer({
465+ workspaceConfigStore: input.workspaceConfigStore,
466+ workspaceStateStore: input.workspaceStateStore,
467+ sourceArtifactStore: input.sourceArtifactStore,
468+ });
482469 const provideWorkspaceStorage = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
483470 effect.pipe(Effect.provide(workspaceStorageLayer));
484471
485472 const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>
486473 provideWorkspaceStorage(Effect.gen(function* () {
487474 const recipeTools = yield* loadWorkspaceRecipeTools({
488− rows: input.rows,
489475 workspaceId: input.workspaceId,
490476 accountId: input.accountId,
477+ sourceRecipeStore: input.sourceRecipeStore,
491478 includeSchemas,
492479 });
493480
@@ -500,8 +487,8 @@ const createWorkspaceRecipeCatalog = (input: { · 6 unmodified lines
500487 ),
501488 getSchemaBundle: ({ id }) =>
502489 provideWorkspaceStorage(loadWorkspaceSchemaBundle({
503− rows: input.rows,
504490 workspaceId: input.workspaceId,
491+ sourceRecipeStore: input.sourceRecipeStore,
505492 id: SourceRecipeSchemaBundleIdSchema.make(id),
506493 })),
507494 });
@@ -562,11 +549,11 @@ const createWorkspaceRecipeCatalog = (input: { · 54 unmodified lines
562549 const createWorkspaceToolInvoker = (input: {
563550 workspaceId: Source["workspaceId"];
564551 accountId: AccountId;
565− rows: ControlPlaneStoreShape;
552+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
566553 workspaceConfigStore: WorkspaceConfigStoreShape;
567554 workspaceStateStore: WorkspaceStateStoreShape;
568555 sourceArtifactStore: SourceArtifactStoreShape;
569− resolveSecretMaterial: ResolveSecretMaterial;
556+ sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
570557 sourceAuthService: RuntimeSourceAuthService;
571558 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null;
572559 localToolRuntime: LocalToolRuntime;
@@ -577,11 +564,11 @@ const createWorkspaceToolInvoker = (input: { · 4 unmodified lines
577564 catalog: ToolCatalog;
578565 toolInvoker: ToolInvoker;
579566 } => {
580− const workspaceStorageLayer = Layer.mergeAll(
581− Layer.succeed(WorkspaceConfigStore, input.workspaceConfigStore),
582− Layer.succeed(WorkspaceStateStore, input.workspaceStateStore),
583− Layer.succeed(SourceArtifactStore, input.sourceArtifactStore),
584− );
567+ const workspaceStorageLayer = makeWorkspaceStorageLayer({
568+ workspaceConfigStore: input.workspaceConfigStore,
569+ workspaceStateStore: input.workspaceStateStore,
570+ sourceArtifactStore: input.sourceArtifactStore,
571+ });
585572 const provideWorkspaceStorage = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
586573 effect.pipe(Effect.provide(workspaceStorageLayer));
587574
@@ -594,7 +581,7 @@ const createWorkspaceToolInvoker = (input: { · 6 unmodified lines
594581 const recipeCatalog = createWorkspaceRecipeCatalog({
595582 workspaceId: input.workspaceId,
596583 accountId: input.accountId,
597− rows: input.rows,
584+ sourceRecipeStore: input.sourceRecipeStore,
598585 workspaceConfigStore: input.workspaceConfigStore,
599586 workspaceStateStore: input.workspaceStateStore,
600587 sourceArtifactStore: input.sourceArtifactStore,
@@ -635,9 +622,9 @@ const createWorkspaceToolInvoker = (input: { · 34 unmodified lines
635622 provideRuntimeLocalWorkspace(
636623 provideWorkspaceStorage(Effect.gen(function* () {
637624 const recipeTool = yield* loadWorkspaceRecipeToolByPath({
638− rows: input.rows,
639625 workspaceId: input.workspaceId,
640626 accountId: input.accountId,
627+ sourceRecipeStore: input.sourceRecipeStore,
641628 path: invocation.path,
642629 includeSchemas: false,
643630 });
@@ -648,7 +635,6 @@ const createWorkspaceToolInvoker = (input: { · 4 unmodified lines
648635 }
649636
650637 yield* authorizePersistedToolInvocation({
651− rows: input.rows,
652638 workspaceId: input.workspaceId,
653639 accountId: input.accountId,
654640 descriptor: toInvocationDescriptorFromRecipeTool({
@@ -660,24 +646,21 @@ const createWorkspaceToolInvoker = (input: { · 5 unmodified lines
660646 onElicitation: input.onElicitation,
661647 });
662648
663− const auth = yield* resolveSourceAuthMaterial({
664− rows: input.rows,
649+ const auth = yield* input.sourceAuthMaterialService.resolve({
665650 source: recipeTool.source,
666651 actorAccountId: input.accountId,
667− resolveSecretMaterial: input.resolveSecretMaterial,
668652 context: toSecretResolutionContext(invocation.context),
669653 });
670654 const schemaBundle = recipeTool.schemaBundleId
671655 ? yield* loadWorkspaceSchemaBundle({
672− rows: input.rows,
673656 workspaceId: input.workspaceId,
657+ sourceRecipeStore: input.sourceRecipeStore,
674658 id: SourceRecipeSchemaBundleIdSchema.make(
675659 recipeTool.schemaBundleId,
676660 ),
677661 })
678662 : null;
679− const recipe = yield* loadSourceWithRecipe({
680− rows: input.rows,
663+ const recipe = yield* input.sourceRecipeStore.loadSourceWithRecipe({
681664 workspaceId: input.workspaceId,
682665 sourceId: recipeTool.source.id,
683666 actorAccountId: input.accountId,
@@ -686,7 +669,6 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines
686669 return yield* getSourceAdapterForOperation(
687670 recipeTool.operation,
688671 ).invokePersistedTool({
689− rows: input.rows,
690672 workspaceId: input.workspaceId,
691673 accountId: input.accountId,
692674 source: recipeTool.source,
@@ -718,20 +700,15 @@ const createWorkspaceToolInvoker = (input: { · 25 unmodified lines
718700 };
719701
720702 export const createWorkspaceExecutionEnvironmentResolver = (input: {
721− rows: ControlPlaneStoreShape;
722− resolveSecretMaterial?: ResolveSecretMaterial;
703+ sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
723704 sourceAuthService: RuntimeSourceAuthService;
705+ sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
706+ localToolRuntimeLoader: LocalToolRuntimeLoaderShape;
724707 workspaceConfigStore: WorkspaceConfigStoreShape;
725708 workspaceStateStore: WorkspaceStateStoreShape;
726709 sourceArtifactStore: SourceArtifactStoreShape;
727−}): ResolveExecutionEnvironment => {
728− const resolveSecretMaterial =
729− input.resolveSecretMaterial ??
730− createDefaultSecretMaterialResolver({
731− rows: input.rows,
732− });
733−
734− return ({ workspaceId, accountId, onElicitation }) =>
710+}): ResolveExecutionEnvironment =>
711+ ({ workspaceId, accountId, onElicitation }) =>
735712 Effect.gen(function* () {
736713 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
737714 const localToolRuntime =
@@ -742,15 +719,15 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 4 unmodified lines
742719 toolInvoker: makeToolInvokerFromTools({ tools: {} }),
743720 toolPaths: new Set<string>(),
744721 }
745− : yield* loadLocalToolRuntime(runtimeLocalWorkspace.context);
722+ : yield* input.localToolRuntimeLoader.load(runtimeLocalWorkspace.context);
746723 const { catalog, toolInvoker } = createWorkspaceToolInvoker({
747724 workspaceId,
748725 accountId,
749− rows: input.rows,
726+ sourceRecipeStore: input.sourceRecipeStore,
750727 workspaceConfigStore: input.workspaceConfigStore,
751728 workspaceStateStore: input.workspaceStateStore,
752729 sourceArtifactStore: input.sourceArtifactStore,
753− resolveSecretMaterial,
730+ sourceAuthMaterialService: input.sourceAuthMaterialService,
754731 sourceAuthService: input.sourceAuthService,
755732 runtimeLocalWorkspace,
756733 localToolRuntime,
@@ -765,7 +742,6 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 8 unmodified lines
765742 catalog,
766743 } satisfies ExecutionEnvironment;
767744 });
768−};
769745
770746 export class RuntimeExecutionResolverService extends Context.Tag(
771747 "#runtime/RuntimeExecutionResolverService",
@@ -777,7 +753,6 @@ export class RuntimeExecutionResolverService extends Context.Tag( · 5 unmodified lines
777753 export const RuntimeExecutionResolverLive = (
778754 input: {
779755 executionResolver?: ResolveExecutionEnvironment;
780− resolveSecretMaterial?: ResolveSecretMaterial;
781756 } = {},
782757 ) =>
783758 input.executionResolver
@@ -785,16 +760,19 @@ export const RuntimeExecutionResolverLive = ( · 1 unmodified line
785760 : Layer.effect(
786761 RuntimeExecutionResolverService,
787762 Effect.gen(function* () {
788− const rows = yield* ControlPlaneStore;
763+ const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
789764 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
765+ const sourceRecipeStore = yield* RuntimeSourceRecipeStoreService;
766+ const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;
790767 const workspaceConfigStore = yield* WorkspaceConfigStore;
791768 const workspaceStateStore = yield* WorkspaceStateStore;
792769 const sourceArtifactStore = yield* SourceArtifactStore;
793770
794771 return createWorkspaceExecutionEnvironmentResolver({
795− rows,
796772 sourceAuthService,
797− resolveSecretMaterial: input.resolveSecretMaterial,
773+ sourceAuthMaterialService,
774+ sourceRecipeStore,
775+ localToolRuntimeLoader,
798776 workspaceConfigStore,
799777 workspaceStateStore,
800778 sourceArtifactStore,