History

Complete local-first control-plane cut

Rhys Sullivan committed 8 months ago

Commit 02b7cb61 parent d64dce342 files · +1878 −4359Browse files

Unified · Split

42 files
  1. apps/executor/src/cli/main.ts +5 −5
  2. apps/executor/src/server/server.test.ts +7 −7
  3. packages/control-plane/src/api/api.ts +1 −7
  4. packages/control-plane/src/api/http.ts +0 −6
  5. packages/control-plane/src/api/index.ts +0 −21
  6. packages/control-plane/src/api/memberships/api.ts +0 −84
  7. packages/control-plane/src/api/memberships/http.ts +0 −67
  8. packages/control-plane/src/api/organizations/api.ts +0 −82
  9. packages/control-plane/src/api/organizations/http.ts +0 −75
  10. packages/control-plane/src/api/payload-schemas.test.ts +0 −35
  11. packages/control-plane/src/api/policies/api.ts +0 −49
  12. packages/control-plane/src/api/policies/http.ts +2 −67
  13. packages/control-plane/src/api/workspaces/api.ts +0 −83
  14. packages/control-plane/src/api/workspaces/http.ts +0 −89
  15. packages/control-plane/src/persistence/post-migrations.test.ts +47 −34
  16. packages/control-plane/src/runtime/actor-resolver.ts +32 −52
  17. packages/control-plane/src/runtime/control-plane-runtime.test.ts +4 −405
  18. packages/control-plane/src/runtime/execution-http.test.ts +2 −2
  19. packages/control-plane/src/runtime/execution-mcp-resume.test.ts +2 −2
  20. packages/control-plane/src/runtime/index.ts +14 −16
  21. packages/control-plane/src/runtime/local-installation.test.ts +8 −15
  22. packages/control-plane/src/runtime/local-installation.ts +31 −257
  23. packages/control-plane/src/runtime/local-operations.ts +1 −21
  24. packages/control-plane/src/runtime/local-runtime-context.ts +22 −1
  25. packages/control-plane/src/runtime/local-workspace-sync.ts +1 −45
  26. packages/control-plane/src/runtime/operations-shared.ts +0 −55
  27. packages/control-plane/src/runtime/organizations-operations.ts +0 −434
  28. packages/control-plane/src/runtime/policies-operations.ts +232 −573
  29. packages/control-plane/src/runtime/source-auth-service.test.ts +76 −60
  30. packages/control-plane/src/runtime/source-inspection.test.ts +162 −121
  31. packages/control-plane/src/runtime/source-materialization.ts +67 −105
  32. packages/control-plane/src/runtime/source-recipes-runtime.test.ts +371 −245
  33. packages/control-plane/src/runtime/source-recipes-runtime.ts +78 −198
  34. packages/control-plane/src/runtime/source-store.test.ts +130 −348
  35. packages/control-plane/src/runtime/source-store.ts +178 −428
  36. packages/control-plane/src/runtime/test-http-client.ts +3 −3
  37. packages/control-plane/src/runtime/workspace-execution-environment.test.ts +377 −196
  38. packages/control-plane/src/runtime/workspace-execution-environment.ts +8 −33
  39. packages/control-plane/src/schema/models/local-config.ts +1 −0
  40. packages/control-plane/src/schema/models/local-installation.ts +5 −22
  41. packages/react/src/index.test.tsx +4 −4
  42. packages/server/src/index.ts +7 −7
apps/executor/src/cli/main.ts+5−5
@@ -15,11 +15,11 @@ import { · 14 unmodified lines
1515 ExecutionIdSchema,
1616 RuntimeExecutionResolverService,
1717 createControlPlaneClient,
18− createSqlControlPlaneRuntime,
18+ createControlPlaneRuntime,
1919 type ControlPlaneClient,
2020 type ExecutionEnvelope,
2121 type ExecutionInteraction,
22− type SqlControlPlaneRuntime,
22+ type ControlPlaneRuntime,
2323 } from "@executor/control-plane";
2424 import type { ToolCatalog } from "@executor/codemode-core";
2525
@@ -247,7 +247,7 @@ const formatCatalogUnavailableMessage = (cause: Cause.Cause<unknown>): string => · 221 unmodified lines
247247 : `Current workspace catalog unavailable: ${message}`;
248248 };
249249
250−const closeSqlRuntime = (runtime: SqlControlPlaneRuntime) =>
250+const closeRuntime = (runtime: ControlPlaneRuntime) =>
251251 Effect.tryPromise({
252252 try: () => runtime.close(),
253253 catch: toError,
@@ -274,7 +274,7 @@ const buildRunWorkflowText = ( · 20 unmodified lines
274274
275275 const loadRunWorkflowText = (): Effect.Effect<string, Error, never> =>
276276 Effect.acquireUseRelease(
277− createSqlControlPlaneRuntime({
277+ createControlPlaneRuntime({
278278 localDataDir: DEFAULT_LOCAL_DATA_DIR,
279279 }).pipe(Effect.mapError(toError)),
280280 (runtime) =>
@@ -293,7 +293,7 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 12 unmodified lines
293293
294294 return yield* buildRunWorkflowText(environment.catalog);
295295 }),
296− closeSqlRuntime,
296+ closeRuntime,
297297 ).pipe(
298298 Effect.catchAllCause((cause) =>
299299 Effect.succeed(
apps/executor/src/server/server.test.ts+7−7
@@ -21,6 +21,7 @@ import { · 20 unmodified lines
2121 createControlPlaneClient,
2222 type ControlPlaneClient,
2323 buildLocalSourceArtifact,
24+ deriveLocalInstallation,
2425 materializationFromMcpManifestEntries,
2526 type ResolveExecutionEnvironment,
2627 resolveLocalWorkspaceContext,
@@ -96,6 +97,7 @@ const writeConfiguredLocalMcpSource = (input: { · 69 unmodified lines
9697 const context = await resolveLocalWorkspaceContext({
9798 workspaceRoot: input.workspaceRoot,
9899 });
100+ const installation = deriveLocalInstallation(context);
99101
100102 await writeProjectLocalExecutorConfig({
101103 context,
@@ -118,7 +120,7 @@ const writeConfiguredLocalMcpSource = (input: { · 16 unmodified lines
118120
119121 const source = {
120122 id: sourceId,
121− workspaceId: "ws_local_server_test" as never,
123+ workspaceId: installation.workspaceId,
122124 name: input.name ?? "Demo",
123125 kind: "mcp" as const,
124126 endpoint: input.endpoint,
@@ -1259,7 +1261,7 @@ describe("local-executor-server", () => { · 1134 unmodified lines
12591261 15_000,
12601262 );
12611263
1262− it.scoped("gates mutating OpenAPI tools by default and allows them via organization policy", () =>
1264+ it.scoped("gates mutating OpenAPI tools by default and allows them via workspace policy", () =>
12631265 Effect.gen(function* () {
12641266 const openApiServer = yield* Effect.acquireRelease(
12651267 Effect.promise(() => startMutatingOpenApiDemoServer()),
@@ -1331,19 +1333,17 @@ describe("local-executor-server", () => { · 65 unmodified lines
13311333 expect(approved.execution.status).toBe("completed");
13321334 expect(openApiServer.createdBodies).toHaveLength(1);
13331335
1334− const policy = yield* client.policies.createOrganization({
1336+ const policy = yield* client.policies.create({
13351337 path: {
1336− organizationId: installation.organizationId,
1338+ workspaceId: installation.workspaceId,
13371339 },
13381340 payload: {
1339− resourceType: "tool_path",
13401341 resourcePattern: "dns.records.createRecord",
1341− matchType: "exact",
13421342 effect: "allow",
13431343 approvalMode: "auto",
13441344 },
13451345 });
1346− expect(policy.scopeType).toBe("organization");
1346+ expect(policy.scopeType).toBe("workspace");
13471347
13481348 const automatic = yield* client.executions.create({
13491349 path: {
packages/control-plane/src/api/api.ts+1−7
@@ -2,26 +2,20 @@ import { HttpApi, OpenApi } from "@effect/platform"; · 1 unmodified line
22
33 import { ExecutionsApi } from "./executions/api";
44 import { LocalApi } from "./local/api";
5−import { MembershipsApi } from "./memberships/api";
65 import { OAuthApi } from "./oauth/api";
7−import { OrganizationsApi } from "./organizations/api";
86 import { PoliciesApi } from "./policies/api";
97 import { SourcesApi } from "./sources/api";
10−import { WorkspacesApi } from "./workspaces/api";
118
129 export class ControlPlaneApi extends HttpApi.make("controlPlane")
1310 .add(LocalApi)
1411 .add(OAuthApi)
15− .add(OrganizationsApi)
16− .add(MembershipsApi)
17− .add(WorkspacesApi)
1812 .add(SourcesApi)
1913 .add(PoliciesApi)
2014 .add(ExecutionsApi)
2115 .annotateContext(
2216 OpenApi.annotations({
2317 title: "Executor Control Plane API",
24− description: "CRUD control plane for organizations, workspaces, sources, and policies",
18+ description: "Local-first control plane for workspace sources, policies, auth, and execution",
2519 }),
2620 ) {}
2721
packages/control-plane/src/api/http.ts+0−6
@@ -8,19 +8,13 @@ import { · 7 unmodified lines
88 } from "./auth/actor-resolver";
99 import { ControlPlaneExecutionsLive } from "./executions/http";
1010 import { ControlPlaneLocalLive } from "./local/http";
11−import { ControlPlaneMembershipsLive } from "./memberships/http";
1211 import { ControlPlaneOAuthLive } from "./oauth/http";
13−import { ControlPlaneOrganizationsLive } from "./organizations/http";
1412 import { ControlPlanePoliciesLive } from "./policies/http";
1513 import { ControlPlaneSourcesLive } from "./sources/http";
16−import { ControlPlaneWorkspacesLive } from "./workspaces/http";
1714
1815 export const ControlPlaneApiLive = HttpApiBuilder.api(ControlPlaneApi).pipe(
1916 Layer.provide(ControlPlaneLocalLive),
2017 Layer.provide(ControlPlaneOAuthLive),
21− Layer.provide(ControlPlaneOrganizationsLive),
22− Layer.provide(ControlPlaneMembershipsLive),
23− Layer.provide(ControlPlaneWorkspacesLive),
2418 Layer.provide(ControlPlaneSourcesLive),
2519 Layer.provide(ControlPlanePoliciesLive),
2620 Layer.provide(ControlPlaneExecutionsLive),
packages/control-plane/src/api/index.ts+0−21
@@ -60,27 +60,6 @@ export { · 59 unmodified lines
6060 type CompleteSourceOAuthResult,
6161 } from "./oauth/api";
6262
63−export {
64− CreateOrganizationPayloadSchema,
65− UpdateOrganizationPayloadSchema,
66− type CreateOrganizationPayload,
67− type UpdateOrganizationPayload,
68−} from "./organizations/api";
69−
70−export {
71− CreateMembershipPayloadSchema,
72− UpdateMembershipPayloadSchema,
73− type CreateMembershipPayload,
74− type UpdateMembershipPayload,
75−} from "./memberships/api";
76−
77−export {
78− CreateWorkspacePayloadSchema,
79− UpdateWorkspacePayloadSchema,
80− type CreateWorkspacePayload,
81− type UpdateWorkspacePayload,
82−} from "./workspaces/api";
83−
8463 export {
8564 ConnectSourcePayloadSchema,
8665 ConnectSourceResultSchema,
packages/control-plane/src/api/memberships/api.ts+0−84
@@ -1,84 +0,0 @@
1−import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2−import {
3− AccountIdSchema,
4− OrganizationIdSchema,
5− OrganizationMembershipInsertSchema,
6− OrganizationMembershipSchema,
7− OrganizationMembershipUpdateSchema,
8−} from "#schema";
9−import * as Schema from "effect/Schema";
10−
11−import {
12− ControlPlaneBadRequestError,
13− ControlPlaneForbiddenError,
14− ControlPlaneNotFoundError,
15− ControlPlaneStorageError,
16− ControlPlaneUnauthorizedError,
17−} from "../errors";
18−
19−const createMembershipPayloadRequiredSchema = OrganizationMembershipInsertSchema.pipe(
20− Schema.pick("accountId", "role"),
21−);
22−
23−const createMembershipPayloadOptionalSchema = OrganizationMembershipInsertSchema.pipe(
24− Schema.pick("status", "billable", "invitedByAccountId"),
25− Schema.partialWith({ exact: true }),
26−);
27−
28−export const CreateMembershipPayloadSchema = Schema.extend(
29− createMembershipPayloadRequiredSchema,
30− createMembershipPayloadOptionalSchema,
31−);
32−
33−export type CreateMembershipPayload = typeof CreateMembershipPayloadSchema.Type;
34−
35−export const UpdateMembershipPayloadSchema = OrganizationMembershipUpdateSchema.pipe(
36− Schema.pick("role", "status", "billable"),
37− Schema.partialWith({ exact: true }),
38−);
39−
40−export type UpdateMembershipPayload = typeof UpdateMembershipPayloadSchema.Type;
41−
42−const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema);
43−const accountIdParam = HttpApiSchema.param("accountId", AccountIdSchema);
44−
45−export class MembershipsApi extends HttpApiGroup.make("memberships")
46− .add(
47− HttpApiEndpoint.get("list")`/organizations/${organizationIdParam}/memberships`
48− .addSuccess(Schema.Array(OrganizationMembershipSchema))
49− .addError(ControlPlaneBadRequestError)
50− .addError(ControlPlaneUnauthorizedError)
51− .addError(ControlPlaneForbiddenError)
52− .addError(ControlPlaneNotFoundError)
53− .addError(ControlPlaneStorageError),
54− )
55− .add(
56− HttpApiEndpoint.post("create")`/organizations/${organizationIdParam}/memberships`
57− .setPayload(CreateMembershipPayloadSchema)
58− .addSuccess(OrganizationMembershipSchema)
59− .addError(ControlPlaneBadRequestError)
60− .addError(ControlPlaneUnauthorizedError)
61− .addError(ControlPlaneForbiddenError)
62− .addError(ControlPlaneNotFoundError)
63− .addError(ControlPlaneStorageError),
64− )
65− .add(
66− HttpApiEndpoint.patch("update")`/organizations/${organizationIdParam}/memberships/${accountIdParam}`
67− .setPayload(UpdateMembershipPayloadSchema)
68− .addSuccess(OrganizationMembershipSchema)
69− .addError(ControlPlaneBadRequestError)
70− .addError(ControlPlaneUnauthorizedError)
71− .addError(ControlPlaneForbiddenError)
72− .addError(ControlPlaneNotFoundError)
73− .addError(ControlPlaneStorageError),
74− )
75− .add(
76− HttpApiEndpoint.del("remove")`/organizations/${organizationIdParam}/memberships/${accountIdParam}`
77− .addSuccess(Schema.Struct({ removed: Schema.Boolean }))
78− .addError(ControlPlaneBadRequestError)
79− .addError(ControlPlaneUnauthorizedError)
80− .addError(ControlPlaneForbiddenError)
81− .addError(ControlPlaneNotFoundError)
82− .addError(ControlPlaneStorageError),
83− )
84− .prefix("/v1") {}
packages/control-plane/src/api/memberships/http.ts+0−67
@@ -1,67 +0,0 @@
1−import { HttpApiBuilder } from "@effect/platform";
2−import type { OrganizationId } from "#schema";
3−
4−import { requirePermission, withPolicy } from "#domain";
5−import {
6− createMembership,
7− listMemberships,
8− removeMembership,
9− updateMembership,
10−} from "../../runtime/organizations-operations";
11−
12−import { ControlPlaneApi } from "../api";
13−import { withRequestActor } from "../http-auth";
14−
15−const requireReadMemberships = (organizationId: OrganizationId) =>
16− requirePermission({
17− permission: "memberships:read",
18− organizationId,
19− });
20−
21−const requireWriteMemberships = (organizationId: OrganizationId) =>
22− requirePermission({
23− permission: "memberships:write",
24− organizationId,
25− });
26−
27−export const ControlPlaneMembershipsLive = HttpApiBuilder.group(
28− ControlPlaneApi,
29− "memberships",
30− (handlers) =>
31− handlers
32− .handle("list", ({ path }) =>
33− withRequestActor("memberships.list", () =>
34− withPolicy(requireReadMemberships(path.organizationId))(
35− listMemberships(path.organizationId),
36− ),
37− ),
38− )
39− .handle("create", ({ path, payload }) =>
40− withRequestActor("memberships.create", () =>
41− withPolicy(requireWriteMemberships(path.organizationId))(
42− createMembership({ organizationId: path.organizationId, payload }),
43− ),
44− ),
45− )
46− .handle("update", ({ path, payload }) =>
47− withRequestActor("memberships.update", () =>
48− withPolicy(requireWriteMemberships(path.organizationId))(
49− updateMembership({
50− organizationId: path.organizationId,
51− accountId: path.accountId,
52− payload,
53− }),
54− ),
55− ),
56− )
57− .handle("remove", ({ path }) =>
58− withRequestActor("memberships.remove", () =>
59− withPolicy(requireWriteMemberships(path.organizationId))(
60− removeMembership({
61− organizationId: path.organizationId,
62− accountId: path.accountId,
63− }),
64− ),
65− ),
66− ),
67−);
packages/control-plane/src/api/organizations/api.ts+0−82
@@ -1,82 +0,0 @@
1−import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2−import {
3− OrganizationIdSchema,
4− OrganizationStatusSchema,
5− OrganizationSchema,
6−} from "#schema";
7−import * as Schema from "effect/Schema";
8−
9−import {
10− ControlPlaneBadRequestError,
11− ControlPlaneForbiddenError,
12− ControlPlaneNotFoundError,
13− ControlPlaneStorageError,
14− ControlPlaneUnauthorizedError,
15−} from "../errors";
16−import {
17− OptionalTrimmedNonEmptyStringSchema,
18− TrimmedNonEmptyStringSchema,
19−} from "../string-schemas";
20−
21−export const CreateOrganizationPayloadSchema = Schema.Struct({
22− name: TrimmedNonEmptyStringSchema,
23− slug: OptionalTrimmedNonEmptyStringSchema,
24−});
25−
26−export type CreateOrganizationPayload = typeof CreateOrganizationPayloadSchema.Type;
27−
28−export const UpdateOrganizationPayloadSchema = Schema.Struct({
29− name: OptionalTrimmedNonEmptyStringSchema,
30− status: Schema.optional(OrganizationStatusSchema),
31−});
32−
33−export type UpdateOrganizationPayload = typeof UpdateOrganizationPayloadSchema.Type;
34−
35−const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema);
36−
37−export class OrganizationsApi extends HttpApiGroup.make("organizations")
38− .add(
39− HttpApiEndpoint.get("list")`/organizations`
40− .addSuccess(Schema.Array(OrganizationSchema))
41− .addError(ControlPlaneBadRequestError)
42− .addError(ControlPlaneUnauthorizedError)
43− .addError(ControlPlaneForbiddenError)
44− .addError(ControlPlaneStorageError),
45− )
46− .add(
47− HttpApiEndpoint.post("create")`/organizations`
48− .setPayload(CreateOrganizationPayloadSchema)
49− .addSuccess(OrganizationSchema)
50− .addError(ControlPlaneBadRequestError)
51− .addError(ControlPlaneUnauthorizedError)
52− .addError(ControlPlaneForbiddenError)
53− .addError(ControlPlaneStorageError),
54− )
55− .add(
56− HttpApiEndpoint.get("get")`/organizations/${organizationIdParam}`
57− .addSuccess(OrganizationSchema)
58− .addError(ControlPlaneBadRequestError)
59− .addError(ControlPlaneUnauthorizedError)
60− .addError(ControlPlaneForbiddenError)
61− .addError(ControlPlaneNotFoundError)
62− .addError(ControlPlaneStorageError),
63− )
64− .add(
65− HttpApiEndpoint.patch("update")`/organizations/${organizationIdParam}`
66− .setPayload(UpdateOrganizationPayloadSchema)
67− .addSuccess(OrganizationSchema)
68− .addError(ControlPlaneBadRequestError)
69− .addError(ControlPlaneUnauthorizedError)
70− .addError(ControlPlaneForbiddenError)
71− .addError(ControlPlaneNotFoundError)
72− .addError(ControlPlaneStorageError),
73− )
74− .add(
75− HttpApiEndpoint.del("remove")`/organizations/${organizationIdParam}`
76− .addSuccess(Schema.Struct({ removed: Schema.Boolean }))
77− .addError(ControlPlaneBadRequestError)
78− .addError(ControlPlaneUnauthorizedError)
79− .addError(ControlPlaneForbiddenError)
80− .addError(ControlPlaneStorageError),
81− )
82− .prefix("/v1") {}
packages/control-plane/src/api/organizations/http.ts+0−75
@@ -1,75 +0,0 @@
1−import { HttpApiBuilder } from "@effect/platform";
2−import type { OrganizationId } from "#schema";
3−
4−import { requirePermission, withPolicy } from "#domain";
5−import {
6− createOrganization,
7− getOrganization,
8− listOrganizations,
9− removeOrganization,
10− updateOrganization,
11−} from "../../runtime/organizations-operations";
12−
13−import { ControlPlaneApi } from "../api";
14−import { withRequestActor } from "../http-auth";
15−
16−const requireReadOrganizations = requirePermission({
17− permission: "organizations:read",
18−});
19−
20−const requireManageOrganizations = (organizationId: OrganizationId) =>
21− requirePermission({
22− permission: "organizations:manage",
23− organizationId,
24− });
25−
26−export const ControlPlaneOrganizationsLive = HttpApiBuilder.group(
27− ControlPlaneApi,
28− "organizations",
29− (handlers) =>
30− handlers
31− .handle("list", () =>
32− withRequestActor("organizations.list", (actor) =>
33− withPolicy(requireReadOrganizations)(
34− listOrganizations({
35− accountId: actor.principal.accountId,
36− }),
37− ),
38− ),
39− )
40− .handle("create", ({ payload }) =>
41− withRequestActor("organizations.create", (actor) =>
42− createOrganization({
43− payload,
44− createdByAccountId: actor.principal.accountId,
45− }),
46− ),
47− )
48− .handle("get", ({ path }) =>
49− withRequestActor("organizations.get", (actor) =>
50− withPolicy(requireReadOrganizations)(
51− getOrganization({
52− organizationId: path.organizationId,
53− accountId: actor.principal.accountId,
54− }),
55− ),
56− ),
57− )
58− .handle("update", ({ path, payload }) =>
59− withRequestActor("organizations.update", () =>
60− withPolicy(requireManageOrganizations(path.organizationId))(
61− updateOrganization({
62− organizationId: path.organizationId,
63− payload,
64− }),
65− ),
66− ),
67− )
68− .handle("remove", ({ path }) =>
69− withRequestActor("organizations.remove", () =>
70− withPolicy(requireManageOrganizations(path.organizationId))(
71− removeOrganization({ organizationId: path.organizationId }),
72− ),
73− ),
74− ),
75−);
packages/control-plane/src/api/payload-schemas.test.ts+0−35
@@ -5,38 +5,15 @@ import * as Schema from "effect/Schema"; · 4 unmodified lines
55 import {
66 CreateExecutionPayloadSchema,
77 } from "./executions/api";
8−import {
9− CreateOrganizationPayloadSchema,
10− UpdateOrganizationPayloadSchema,
11−} from "./organizations/api";
128 import { CreatePolicyPayloadSchema } from "./policies/api";
139 import {
1410 ConnectSourcePayloadSchema,
1511 CreateSourcePayloadSchema,
1612 UpdateSourcePayloadSchema,
1713 } from "./sources/api";
18−import { CreateWorkspacePayloadSchema } from "./workspaces/api";
1914
2015 describe("control-plane payload schemas", () => {
2116 it("normalizes trimmed strings at decode time", () => {
22− expect(
23− Schema.decodeUnknownSync(CreateOrganizationPayloadSchema)({
24− name: " Acme ",
25− slug: " acme ",
26− }),
27− ).toEqual({
28− name: "Acme",
29− slug: "acme",
30− });
31−
32− expect(
33− Schema.decodeUnknownSync(CreateWorkspacePayloadSchema)({
34− name: " Primary ",
35− }),
36− ).toEqual({
37− name: "Primary",
38− });
39−
4017 expect(
4118 Schema.decodeUnknownSync(CreateSourcePayloadSchema)({
4219 name: " Github ",
@@ -101,18 +78,6 @@ describe("control-plane payload schemas", () => { · 58 unmodified lines
10178 });
10279
10380 it("rejects blank strings for normalized string fields", () => {
104− throws(() =>
105− Schema.decodeUnknownSync(CreateOrganizationPayloadSchema)({
106− name: " ",
107− })
108− );
109−
110− throws(() =>
111− Schema.decodeUnknownSync(UpdateOrganizationPayloadSchema)({
112− name: " ",
113− })
114− );
115−
11681 throws(() =>
11782 Schema.decodeUnknownSync(UpdateSourcePayloadSchema)({
11883 endpoint: " ",
packages/control-plane/src/api/policies/api.ts+0−49
@@ -1,7 +1,6 @@
11 import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
22 import {
33 AccountIdSchema,
4− OrganizationIdSchema,
54 PolicyIdSchema,
65 PolicyApprovalModeSchema,
76 PolicyEffectSchema,
@@ -51,58 +50,10 @@ export const UpdatePolicyPayloadSchema = Schema.Struct({ · 43 unmodified lines
5150
5251 export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type;
5352
54−const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema);
5553 const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
5654 const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema);
5755
5856 export class PoliciesApi extends HttpApiGroup.make("policies")
59− .add(
60− HttpApiEndpoint.get("listOrganization")`/organizations/${organizationIdParam}/policies`
61− .addSuccess(Schema.Array(PolicySchema))
62− .addError(ControlPlaneBadRequestError)
63− .addError(ControlPlaneUnauthorizedError)
64− .addError(ControlPlaneForbiddenError)
65− .addError(ControlPlaneNotFoundError)
66− .addError(ControlPlaneStorageError),
67− )
68− .add(
69− HttpApiEndpoint.post("createOrganization")`/organizations/${organizationIdParam}/policies`
70− .setPayload(CreatePolicyPayloadSchema)
71− .addSuccess(PolicySchema)
72− .addError(ControlPlaneBadRequestError)
73− .addError(ControlPlaneUnauthorizedError)
74− .addError(ControlPlaneForbiddenError)
75− .addError(ControlPlaneNotFoundError)
76− .addError(ControlPlaneStorageError),
77− )
78− .add(
79− HttpApiEndpoint.get("getOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}`
80− .addSuccess(PolicySchema)
81− .addError(ControlPlaneBadRequestError)
82− .addError(ControlPlaneUnauthorizedError)
83− .addError(ControlPlaneForbiddenError)
84− .addError(ControlPlaneNotFoundError)
85− .addError(ControlPlaneStorageError),
86− )
87− .add(
88− HttpApiEndpoint.patch("updateOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}`
89− .setPayload(UpdatePolicyPayloadSchema)
90− .addSuccess(PolicySchema)
91− .addError(ControlPlaneBadRequestError)
92− .addError(ControlPlaneUnauthorizedError)
93− .addError(ControlPlaneForbiddenError)
94− .addError(ControlPlaneNotFoundError)
95− .addError(ControlPlaneStorageError),
96− )
97− .add(
98− HttpApiEndpoint.del("removeOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}`
99− .addSuccess(Schema.Struct({ removed: Schema.Boolean }))
100− .addError(ControlPlaneBadRequestError)
101− .addError(ControlPlaneUnauthorizedError)
102− .addError(ControlPlaneForbiddenError)
103− .addError(ControlPlaneNotFoundError)
104− .addError(ControlPlaneStorageError),
105− )
10657 .add(
10758 HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/policies`
10859 .addSuccess(Schema.Array(PolicySchema))
packages/control-plane/src/api/policies/http.ts+2−67
@@ -1,34 +1,17 @@
11 import { HttpApiBuilder } from "@effect/platform";
2−import type { OrganizationId, WorkspaceId } from "#schema";
2+import type { WorkspaceId } from "#schema";
33
44 import { requirePermission, withPolicy } from "#domain";
55 import {
6− createOrganizationPolicy,
76 createPolicy,
8− getOrganizationPolicy,
97 getPolicy,
10− listOrganizationPolicies,
118 listPolicies,
12− removeOrganizationPolicy,
139 removePolicy,
14− updateOrganizationPolicy,
1510 updatePolicy,
1611 } from "../../runtime/policies-operations";
1712
1813 import { ControlPlaneApi } from "../api";
19−import { withRequestActor, withWorkspaceRequestActor } from "../http-auth";
20−
21−const requireReadOrganizationPolicies = (organizationId: OrganizationId) =>
22− requirePermission({
23− permission: "policies:read",
24− organizationId,
25− });
26−
27−const requireWriteOrganizationPolicies = (organizationId: OrganizationId) =>
28− requirePermission({
29− permission: "policies:write",
30− organizationId,
31− });
14+import { withWorkspaceRequestActor } from "../http-auth";
3215
3316 const requireReadPolicies = (workspaceId: WorkspaceId) =>
3417 requirePermission({
@@ -47,54 +30,6 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 12 unmodified lines
4730 "policies",
4831 (handlers) =>
4932 handlers
50− .handle("listOrganization", ({ path }) =>
51− withRequestActor("policies.listOrganization", () =>
52− withPolicy(requireReadOrganizationPolicies(path.organizationId))(
53− listOrganizationPolicies(path.organizationId),
54− ),
55− ),
56− )
57− .handle("createOrganization", ({ path, payload }) =>
58− withRequestActor("policies.createOrganization", () =>
59− withPolicy(requireWriteOrganizationPolicies(path.organizationId))(
60− createOrganizationPolicy({
61− organizationId: path.organizationId,
62− payload,
63− }),
64− ),
65− ),
66− )
67− .handle("getOrganization", ({ path }) =>
68− withRequestActor("policies.getOrganization", () =>
69− withPolicy(requireReadOrganizationPolicies(path.organizationId))(
70− getOrganizationPolicy({
71− organizationId: path.organizationId,
72− policyId: path.policyId,
73− }),
74− ),
75− ),
76− )
77− .handle("updateOrganization", ({ path, payload }) =>
78− withRequestActor("policies.updateOrganization", () =>
79− withPolicy(requireWriteOrganizationPolicies(path.organizationId))(
80− updateOrganizationPolicy({
81− organizationId: path.organizationId,
82− policyId: path.policyId,
83− payload,
84− }),
85− ),
86− ),
87− )
88− .handle("removeOrganization", ({ path }) =>
89− withRequestActor("policies.removeOrganization", () =>
90− withPolicy(requireWriteOrganizationPolicies(path.organizationId))(
91− removeOrganizationPolicy({
92− organizationId: path.organizationId,
93− policyId: path.policyId,
94− }),
95− ),
96− ),
97− )
9833 .handle("list", ({ path }) =>
9934 withWorkspaceRequestActor("policies.list", path.workspaceId, () =>
10035 withPolicy(requireReadPolicies(path.workspaceId))(
packages/control-plane/src/api/workspaces/api.ts+0−83
@@ -1,83 +0,0 @@
1−import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2−import {
3− OrganizationIdSchema,
4− WorkspaceIdSchema,
5− WorkspaceSchema,
6−} from "#schema";
7−import * as Schema from "effect/Schema";
8−
9−import {
10− ControlPlaneBadRequestError,
11− ControlPlaneForbiddenError,
12− ControlPlaneNotFoundError,
13− ControlPlaneStorageError,
14− ControlPlaneUnauthorizedError,
15−} from "../errors";
16−import {
17− OptionalTrimmedNonEmptyStringSchema,
18− TrimmedNonEmptyStringSchema,
19−} from "../string-schemas";
20−
21−export const CreateWorkspacePayloadSchema = Schema.Struct({
22− name: TrimmedNonEmptyStringSchema,
23−});
24−
25−export type CreateWorkspacePayload = typeof CreateWorkspacePayloadSchema.Type;
26−
27−export const UpdateWorkspacePayloadSchema = Schema.Struct({
28− name: OptionalTrimmedNonEmptyStringSchema,
29−});
30−
31−export type UpdateWorkspacePayload = typeof UpdateWorkspacePayloadSchema.Type;
32−
33−const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema);
34−const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
35−
36−export class WorkspacesApi extends HttpApiGroup.make("workspaces")
37− .add(
38− HttpApiEndpoint.get("list")`/organizations/${organizationIdParam}/workspaces`
39− .addSuccess(Schema.Array(WorkspaceSchema))
40− .addError(ControlPlaneBadRequestError)
41− .addError(ControlPlaneUnauthorizedError)
42− .addError(ControlPlaneForbiddenError)
43− .addError(ControlPlaneNotFoundError)
44− .addError(ControlPlaneStorageError),
45− )
46− .add(
47− HttpApiEndpoint.post("create")`/organizations/${organizationIdParam}/workspaces`
48− .setPayload(CreateWorkspacePayloadSchema)
49− .addSuccess(WorkspaceSchema)
50− .addError(ControlPlaneBadRequestError)
51− .addError(ControlPlaneUnauthorizedError)
52− .addError(ControlPlaneForbiddenError)
53− .addError(ControlPlaneNotFoundError)
54− .addError(ControlPlaneStorageError),
55− )
56− .add(
57− HttpApiEndpoint.get("get")`/workspaces/${workspaceIdParam}`
58− .addSuccess(WorkspaceSchema)
59− .addError(ControlPlaneBadRequestError)
60− .addError(ControlPlaneUnauthorizedError)
61− .addError(ControlPlaneForbiddenError)
62− .addError(ControlPlaneNotFoundError)
63− .addError(ControlPlaneStorageError),
64− )
65− .add(
66− HttpApiEndpoint.patch("update")`/workspaces/${workspaceIdParam}`
67− .setPayload(UpdateWorkspacePayloadSchema)
68− .addSuccess(WorkspaceSchema)
69− .addError(ControlPlaneBadRequestError)
70− .addError(ControlPlaneUnauthorizedError)
71− .addError(ControlPlaneForbiddenError)
72− .addError(ControlPlaneNotFoundError)
73− .addError(ControlPlaneStorageError),
74− )
75− .add(
76− HttpApiEndpoint.del("remove")`/workspaces/${workspaceIdParam}`
77− .addSuccess(Schema.Struct({ removed: Schema.Boolean }))
78− .addError(ControlPlaneBadRequestError)
79− .addError(ControlPlaneUnauthorizedError)
80− .addError(ControlPlaneForbiddenError)
81− .addError(ControlPlaneStorageError),
82− )
83− .prefix("/v1") {}
packages/control-plane/src/api/workspaces/http.ts+0−89
@@ -1,89 +0,0 @@
1−import { HttpApiBuilder } from "@effect/platform";
2−import type { OrganizationId, WorkspaceId } from "#schema";
3−
4−import { requirePermission, withPolicy } from "#domain";
5−import {
6− createWorkspace,
7− getWorkspace,
8− listWorkspaces,
9− removeWorkspace,
10− updateWorkspace,
11−} from "../../runtime/organizations-operations";
12−
13−import { ControlPlaneApi } from "../api";
14−import {
15− withRequestActor,
16− withWorkspaceRequestActor,
17−} from "../http-auth";
18−
19−const requireReadWorkspace = (workspaceId: WorkspaceId) =>
20− requirePermission({
21− permission: "workspace:read",
22− workspaceId,
23− });
24−
25−const requireManageWorkspace = (workspaceId: WorkspaceId) =>
26− requirePermission({
27− permission: "workspace:manage",
28− workspaceId,
29− });
30−
31−const requireOrganizationWorkspaceRead = (organizationId: OrganizationId) =>
32− requirePermission({
33− permission: "workspace:read",
34− organizationId,
35− });
36−
37−const requireOrganizationWorkspaceManage = (organizationId: OrganizationId) =>
38− requirePermission({
39− permission: "workspace:manage",
40− organizationId,
41− });
42−
43−export const ControlPlaneWorkspacesLive = HttpApiBuilder.group(
44− ControlPlaneApi,
45− "workspaces",
46− (handlers) =>
47− handlers
48− .handle("list", ({ path }) =>
49− withRequestActor("workspaces.list", () =>
50− withPolicy(
51− requireOrganizationWorkspaceRead(path.organizationId),
52− )(listWorkspaces(path.organizationId))
53− ),
54− )
55− .handle("create", ({ path, payload }) =>
56− withRequestActor("workspaces.create", (actor) =>
57− withPolicy(
58− requireOrganizationWorkspaceManage(path.organizationId),
59− )(
60− createWorkspace({
61− organizationId: path.organizationId,
62− payload,
63− createdByAccountId: actor.principal.accountId,
64− }),
65− ),
66− ),
67− )
68− .handle("get", ({ path }) =>
69− withWorkspaceRequestActor("workspaces.get", path.workspaceId, () =>
70− withPolicy(requireReadWorkspace(path.workspaceId))(
71− getWorkspace(path.workspaceId),
72− ),
73− ),
74− )
75− .handle("update", ({ path, payload }) =>
76− withWorkspaceRequestActor("workspaces.update", path.workspaceId, () =>
77− withPolicy(requireManageWorkspace(path.workspaceId))(
78− updateWorkspace({ workspaceId: path.workspaceId, payload }),
79− ),
80− ),
81− )
82− .handle("remove", ({ path }) =>
83− withWorkspaceRequestActor("workspaces.remove", path.workspaceId, () =>
84− withPolicy(requireManageWorkspace(path.workspaceId))(
85− removeWorkspace({ workspaceId: path.workspaceId }),
86− ),
87− ),
88− ),
89−);
packages/control-plane/src/persistence/post-migrations.test.ts+47−34
@@ -27,7 +27,6 @@ import { · 26 unmodified lines
2727 type SqlControlPlanePersistence,
2828 } from "./index";
2929 import { runCodeMigrations } from "./code-migrations";
30−import { loadSourceById } from "../runtime/source-store";
3130
3231 const openApiBindingConfigJson = (specUrl: string): string =>
3332 JSON.stringify({
@@ -693,31 +692,40 @@ describe("code-migrations", () => { · 659 unmodified lines
693692 }),
694693 );
695694
696− const openApiSource = await Effect.runPromise(loadSourceById(
697− upgradedPersistence.rows,
698− {
695+ const openApiSource = await Effect.runPromise(
696+ upgradedPersistence.rows.sources.getByWorkspaceAndId(
699697 workspaceId,
700− sourceId: SourceIdSchema.make("src_legacy_openapi"),
701− },
702− ));
703− expect(openApiSource.binding).toEqual({
704− specUrl: "https://api.example.com/openapi.json",
705− defaultHeaders: {
706− accept: "application/json",
698+ SourceIdSchema.make("src_legacy_openapi"),
699+ ),
700+ );
701+ expect(
702+ JSON.parse(Option.getOrThrow(openApiSource).bindingConfigJson),
703+ ).toEqual({
704+ adapterKey: "openapi",
705+ version: 1,
706+ payload: {
707+ specUrl: "https://api.example.com/openapi.json",
708+ defaultHeaders: {
709+ accept: "application/json",
710+ },
707711 },
708712 });
709− expect(openApiSource.bindingVersion).toBe(1);
710713
711− const graphqlSource = await Effect.runPromise(loadSourceById(
712− upgradedPersistence.rows,
713− {
714+ const graphqlSource = await Effect.runPromise(
715+ upgradedPersistence.rows.sources.getByWorkspaceAndId(
714716 workspaceId,
715− sourceId: SourceIdSchema.make("src_legacy_graphql"),
716− },
717− ));
718− expect(graphqlSource.binding).toEqual({
719− defaultHeaders: {
720− accept: "application/json",
717+ SourceIdSchema.make("src_legacy_graphql"),
718+ ),
719+ );
720+ expect(
721+ JSON.parse(Option.getOrThrow(graphqlSource).bindingConfigJson),
722+ ).toEqual({
723+ adapterKey: "graphql",
724+ version: 1,
725+ payload: {
726+ defaultHeaders: {
727+ accept: "application/json",
728+ },
721729 },
722730 });
723731
@@ -726,20 +734,25 @@ describe("code-migrations", () => { · 2 unmodified lines
726734 );
727735 expect(Option.getOrNull(graphqlRecipe)?.adapterKey).toBe("graphql");
728736
729− const mcpSource = await Effect.runPromise(loadSourceById(
730− upgradedPersistence.rows,
731− {
737+ const mcpSource = await Effect.runPromise(
738+ upgradedPersistence.rows.sources.getByWorkspaceAndId(
732739 workspaceId,
733− sourceId: SourceIdSchema.make("src_legacy_mcp"),
734− },
735− ));
736− expect(mcpSource.binding).toEqual({
737− transport: "streamable-http",
738− queryParams: {
739− tenant: "acme",
740− },
741− headers: {
742− "x-tenant": "acme",
740+ SourceIdSchema.make("src_legacy_mcp"),
741+ ),
742+ );
743+ expect(
744+ JSON.parse(Option.getOrThrow(mcpSource).bindingConfigJson),
745+ ).toEqual({
746+ adapterKey: "mcp",
747+ version: 1,
748+ payload: {
749+ transport: "streamable-http",
750+ queryParams: {
751+ tenant: "acme",
752+ },
753+ headers: {
754+ "x-tenant": "acme",
755+ },
743756 },
744757 });
745758
packages/control-plane/src/runtime/actor-resolver.ts+32−52
@@ -3,16 +3,11 @@ import { ControlPlaneAuthHeaders } from "../auth-headers"; · 2 unmodified lines
33 import {
44 ControlPlaneActorResolver,
55 type ControlPlaneActorResolverShape,
6− deriveWorkspaceMembershipsForPrincipal,
76 } from "#api";
87 import {
98 ActorUnauthenticatedError,
109 createActor,
1110 } from "#domain";
12−import {
13− SqlControlPlaneRowsService,
14− type SqlControlPlaneRows,
15−} from "#persistence";
1611 import {
1712 PrincipalProviderSchema,
1813 PrincipalSchema,
@@ -23,6 +18,7 @@ import * as Layer from "effect/Layer"; · 4 unmodified lines
2318 import * as Option from "effect/Option";
2419 import * as ParseResult from "effect/ParseResult";
2520 import * as Schema from "effect/Schema";
21+import { getRuntimeLocalWorkspaceOption } from "./local-runtime-context";
2622
2723 export { ControlPlaneAuthHeaders };
2824
@@ -95,57 +91,44 @@ const readPrincipalFromHeaders = ( · 66 unmodified lines
9591 );
9692 });
9793
98−export const createHeaderActorResolver = (
99− rows: SqlControlPlaneRows,
100−): ControlPlaneActorResolverShape => ({
94+const createLocalActor = (principal: Principal) =>
95+ createActor({
96+ principal,
97+ workspaceMemberships: [],
98+ organizationMemberships: [],
99+ });
100+
101+export const createHeaderActorResolver = (): ControlPlaneActorResolverShape => ({
101102 resolveActor: ({ headers }) =>
102103 Effect.gen(function* () {
103104 const principal = yield* readPrincipalFromHeaders(headers);
104− const organizationMemberships = yield* rows.organizationMemberships
105− .listByAccountId(principal.accountId)
106− .pipe(
107− Effect.mapError((cause) =>
108− toUnauthenticatedError("Failed loading memberships", cause),
109− ),
110− );
111−
112− return yield* createActor({
113− principal,
114− workspaceMemberships: [],
115− organizationMemberships,
116− });
105+ return yield* createLocalActor(principal);
117106 }),
118107
119108 resolveWorkspaceActor: ({ workspaceId, headers }) =>
120109 Effect.gen(function* () {
121110 const principal = yield* readPrincipalFromHeaders(headers);
122− const organizationMemberships = yield* rows.organizationMemberships
123− .listByAccountId(principal.accountId)
124− .pipe(
125− Effect.mapError((cause) =>
126− toUnauthenticatedError("Failed loading memberships", cause),
127− ),
128− );
129−
130− const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe(
131− Effect.mapError((cause) =>
132− toUnauthenticatedError("Failed loading workspace context", cause),
133− ),
134− );
135− const workspace = Option.getOrNull(workspaceOption);
136−
137− const workspaceMemberships = deriveWorkspaceMembershipsForPrincipal({
138− principalAccountId: principal.accountId,
139− workspaceId,
140− workspace,
141− organizationMemberships,
142− });
143−
144− return yield* createActor({
145− principal,
146− workspaceMemberships,
147− organizationMemberships,
148− });
111+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
112+ if (
113+ runtimeLocalWorkspace !== null
114+ && runtimeLocalWorkspace.installation.workspaceId === workspaceId
115+ && runtimeLocalWorkspace.installation.accountId === principal.accountId
116+ ) {
117+ return yield* createActor({
118+ principal,
119+ workspaceMemberships: [{
120+ accountId: principal.accountId,
121+ workspaceId,
122+ role: "owner",
123+ status: "active",
124+ grantedAt: 0,
125+ updatedAt: 0,
126+ }],
127+ organizationMemberships: [],
128+ });
129+ }
130+
131+ return yield* createLocalActor(principal);
149132 }),
150133 });
151134
@@ -154,7 +137,4 @@ export const RuntimeActorResolverLive = ( · 2 unmodified lines
154137 ) =>
155138 actorResolver
156139 ? Layer.succeed(ControlPlaneActorResolver, actorResolver)
157− : Layer.effect(
158− ControlPlaneActorResolver,
159− Effect.map(SqlControlPlaneRowsService, (rows) => createHeaderActorResolver(rows)),
160− );
140+ : Layer.succeed(ControlPlaneActorResolver, createHeaderActorResolver());
packages/control-plane/src/runtime/control-plane-runtime.test.ts+4−405
@@ -19,7 +19,7 @@ import { · 18 unmodified lines
1919 import type { ToolPath } from "@executor/codemode-core";
2020
2121 import {
22− createSqlControlPlaneRuntime,
22+ createControlPlaneRuntime,
2323 LiveExecutionManagerService,
2424 } from "./index";
2525 import { createSourceFromPayload } from "./source-definitions";
@@ -28,7 +28,7 @@ import { persistSource } from "./source-store"; · 2 unmodified lines
2828 import { withControlPlaneClient } from "./test-http-client";
2929
3030 const makeRuntime = Effect.acquireRelease(
31− createSqlControlPlaneRuntime({
31+ createControlPlaneRuntime({
3232 localDataDir: ":memory:",
3333 workspaceRoot: mkdtempSync(join(tmpdir(), "executor-control-plane-runtime-")),
3434 }),
@@ -123,7 +123,7 @@ describe("control-plane-runtime", () => { · 88 unmodified lines
123123 Effect.gen(function* () {
124124 const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-local-config-runtime-"));
125125 const runtime = yield* Effect.acquireRelease(
126− createSqlControlPlaneRuntime({
126+ createControlPlaneRuntime({
127127 localDataDir: ":memory:",
128128 workspaceRoot,
129129 }),
@@ -175,150 +175,9 @@ describe("control-plane-runtime", () => { · 45 unmodified lines
175175 };
176176 expect(removedConfig.sources?.github).toBeUndefined();
177177 }),
178+ 60_000,
178179 );
179180
180− it.scoped("supports full CRUD flow over HTTP API", () =>
181− Effect.gen(function* () {
182− const runtime = yield* makeRuntime;
183− const openApiServer = yield* makeOpenApiSpecServer;
184−
185− const createOrg = yield* withControlPlaneClient(
186− { runtime, accountId: "acc_1" },
187− (client) =>
188− client.organizations.create({
189− payload: {
190− name: "Acme",
191− },
192− }),
193− );
194− const organizationId = createOrg.id;
195−
196− const createWorkspace = yield* withControlPlaneClient(
197− { runtime, accountId: "acc_1" },
198− (client) =>
199− client.workspaces.create({
200− path: { organizationId },
201− payload: { name: "Primary" },
202− }),
203− );
204− expect(createWorkspace.createdByAccountId).toBe("acc_1");
205− const workspaceId = createWorkspace.id;
206−
207− yield* withControlPlaneClient(
208− { runtime, accountId: "acc_1" },
209− (client) =>
210− client.sources.create({
211− path: { workspaceId },
212− payload: {
213− name: "Github",
214− kind: "openapi",
215− endpoint: openApiServer.baseUrl,
216− binding: {
217− specUrl: openApiServer.specUrl,
218− defaultHeaders: null,
219− },
220− auth: {
221− kind: "none",
222− },
223− },
224− }),
225− );
226−
227− yield* withControlPlaneClient(
228− { runtime, accountId: "acc_1" },
229− (client) =>
230− client.policies.create({
231− path: { workspaceId },
232− payload: {
233− resourceType: "tool_path",
234− resourcePattern: "source.github.*",
235− matchType: "glob",
236− effect: "allow",
237− approvalMode: "auto",
238− priority: 50,
239− enabled: true,
240− },
241− }),
242− );
243−
244− const listSources = yield* withControlPlaneClient(
245− { runtime, accountId: "acc_1" },
246− (client) =>
247− client.sources.list({
248− path: { workspaceId },
249− }),
250− );
251− expect(listSources.length).toBe(1);
252−
253− const listPolicies = yield* withControlPlaneClient(
254− { runtime, accountId: "acc_1" },
255− (client) =>
256− client.policies.list({
257− path: { workspaceId },
258− }),
259− );
260− expect(listPolicies.length).toBe(1);
261− }),
262− );
263−
264− it.scoped("discovers and connects an OpenAPI source through the HTTP API", () =>
265− Effect.gen(function* () {
266− const runtime = yield* makeRuntime;
267− const openApiServer = yield* makeOpenApiSpecServer;
268−
269− const organization = yield* withControlPlaneClient(
270− { runtime, accountId: "acc_discover" },
271− (client) =>
272− client.organizations.create({
273− payload: { name: "Discovery Org" },
274− }),
275− );
276− const workspace = yield* withControlPlaneClient(
277− { runtime, accountId: "acc_discover" },
278− (client) =>
279− client.workspaces.create({
280− path: { organizationId: organization.id },
281− payload: { name: "Discovery Workspace" },
282− }),
283− );
284−
285− const discovered = yield* withControlPlaneClient(
286− { runtime, accountId: "acc_discover" },
287− (client) =>
288− client.sources.discover({
289− payload: {
290− url: openApiServer.specUrl,
291− },
292− }),
293− );
294− expect(discovered.detectedKind).toBe("openapi");
295− expect(discovered.specUrl).toBe(openApiServer.specUrl);
296− expect(discovered.endpoint).toBe(openApiServer.baseUrl);
297−
298− const connected = yield* withControlPlaneClient(
299− { runtime, accountId: "acc_discover" },
300− (client) =>
301− client.sources.connect({
302− path: { workspaceId: workspace.id },
303− payload: {
304− kind: "openapi",
305− endpoint: discovered.endpoint,
306− specUrl: discovered.specUrl ?? openApiServer.specUrl,
307− name: discovered.name,
308− namespace: discovered.namespace,
309− auth: { kind: "none" },
310− },
311− }),
312− );
313− expect(connected.kind).toBe("connected");
314− expect(connected.source.kind).toBe("openapi");
315− expect(connected.source.status).toBe("connected");
316− expect(connected.source.binding).toEqual({
317− specUrl: openApiServer.specUrl,
318− defaultHeaders: null,
319− });
320− }),
321− );
322181
323182 it.scoped("captures credential requests through the local HTML flow without persisting raw tokens", () =>
324183 Effect.gen(function* () {
@@ -608,264 +467,4 @@ describe("control-plane-runtime", () => { · 283 unmodified lines
608467 }),
609468 );
610469
611− it.scoped("scopes organization list/get to memberships", () =>
612− Effect.gen(function* () {
613− const runtime = yield* makeRuntime;
614−
615− const orgOne = yield* withControlPlaneClient(
616− { runtime, accountId: "acc_1" },
617− (client) =>
618− client.organizations.create({
619− payload: { name: "Org One" },
620− }),
621− );
622−
623− const orgTwo = yield* withControlPlaneClient(
624− { runtime, accountId: "acc_2" },
625− (client) =>
626− client.organizations.create({
627− payload: { name: "Org Two" },
628− }),
629− );
630−
631− const listForAcc1 = yield* withControlPlaneClient(
632− { runtime, accountId: "acc_1" },
633− (client) => client.organizations.list({}),
634− );
635−
636− expect(listForAcc1.length).toBe(1);
637−
638− const getOtherOrgError = yield* expectLeft(
639− withControlPlaneClient(
640− { runtime, accountId: "acc_1" },
641− (client) =>
642− client.organizations.get({
643− path: { organizationId: orgTwo.id },
644− }),
645− ),
646− );
647−
648− assertTrue(orgOne.id.length > 0);
649− expect(getOtherOrgError._tag).toBe("ControlPlaneNotFoundError");
650− }),
651− );
652−
653− it.scoped("prevents viewers from workspace manage actions", () =>
654− Effect.gen(function* () {
655− const runtime = yield* makeRuntime;
656−
657− const organization = yield* withControlPlaneClient(
658− { runtime, accountId: "owner_acc" },
659− (client) =>
660− client.organizations.create({
661− payload: { name: "Secured Org" },
662− }),
663− );
664−
665− const workspace = yield* withControlPlaneClient(
666− { runtime, accountId: "owner_acc" },
667− (client) =>
668− client.workspaces.create({
669− path: { organizationId: organization.id },
670− payload: { name: "Secured WS" },
671− }),
672− );
673−
674− yield* withControlPlaneClient(
675− { runtime, accountId: "owner_acc" },
676− (client) =>
677− client.memberships.create({
678− path: { organizationId: organization.id },
679− payload: {
680− accountId: "viewer_acc" as AccountId,
681− role: "viewer",
682− status: "active",
683− },
684− }),
685− );
686−
687− const viewerDeleteError = yield* expectLeft(
688− withControlPlaneClient(
689− { runtime, accountId: "viewer_acc" },
690− (client) =>
691− client.workspaces.remove({
692− path: { workspaceId: workspace.id },
693− }),
694− ),
695− );
696−
697− expect(viewerDeleteError._tag).toBe("ControlPlaneForbiddenError");
698− }),
699− );
700−
701− it.scoped("blocks organization manage across tenants", () =>
702− Effect.gen(function* () {
703− const runtime = yield* makeRuntime;
704−
705− const orgA = yield* withControlPlaneClient(
706− { runtime, accountId: "acc_a" },
707− (client) =>
708− client.organizations.create({
709− payload: { name: "Org A" },
710− }),
711− );
712−
713− const orgB = yield* withControlPlaneClient(
714− { runtime, accountId: "acc_b" },
715− (client) =>
716− client.organizations.create({
717− payload: { name: "Org B" },
718− }),
719− );
720−
721− const patchOtherOrgError = yield* expectLeft(
722− withControlPlaneClient(
723− { runtime, accountId: "acc_a" },
724− (client) =>
725− client.organizations.update({
726− path: { organizationId: orgB.id },
727− payload: { name: "Renamed" },
728− }),
729− ),
730− );
731− assertTrue(orgA.id.length > 0);
732− expect(patchOtherOrgError._tag).toBe("ControlPlaneForbiddenError");
733−
734− const deleteOtherOrgError = yield* expectLeft(
735− withControlPlaneClient(
736− { runtime, accountId: "acc_a" },
737− (client) =>
738− client.organizations.remove({
739− path: { organizationId: orgB.id },
740− }),
741− ),
742− );
743− expect(deleteOtherOrgError._tag).toBe("ControlPlaneForbiddenError");
744− }),
745− );
746−
747− it.scoped("suspended creators cannot manage previously created workspaces", () =>
748− Effect.gen(function* () {
749− const runtime = yield* makeRuntime;
750−
751− const organization = yield* withControlPlaneClient(
752− { runtime, accountId: "creator_acc" },
753− (client) =>
754− client.organizations.create({
755− payload: { name: "Creator Org" },
756− }),
757− );
758−
759− const workspace = yield* withControlPlaneClient(
760− { runtime, accountId: "creator_acc" },
761− (client) =>
762− client.workspaces.create({
763− path: { organizationId: organization.id },
764− payload: { name: "Creator WS" },
765− }),
766− );
767−
768− yield* withControlPlaneClient(
769− { runtime, accountId: "creator_acc" },
770− (client) =>
771− client.memberships.update({
772− path: {
773− organizationId: organization.id,
774− accountId: "creator_acc" as AccountId,
775− },
776− payload: { status: "suspended" },
777− }),
778− );
779−
780− const deleteWorkspaceError = yield* expectLeft(
781− withControlPlaneClient(
782− { runtime, accountId: "creator_acc" },
783− (client) =>
784− client.workspaces.remove({
785− path: { workspaceId: workspace.id },
786− }),
787− ),
788− );
789−
790− expect(deleteWorkspaceError._tag).toBe("ControlPlaneForbiddenError");
791− }),
792− );
793−
794− it.scoped("deleting organization cascades and blocks stale org operations", () =>
795− Effect.gen(function* () {
796− const runtime = yield* makeRuntime;
797−
798− const organization = yield* withControlPlaneClient(
799− { runtime, accountId: "acc_del" },
800− (client) =>
801− client.organizations.create({
802− payload: { name: "Delete Me" },
803− }),
804− );
805−
806− const workspace = yield* withControlPlaneClient(
807− { runtime, accountId: "acc_del" },
808− (client) =>
809− client.workspaces.create({
810− path: { organizationId: organization.id },
811− payload: { name: "Delete WS" },
812− }),
813− );
814−
815− const deleteOrg = yield* withControlPlaneClient(
816− { runtime, accountId: "acc_del" },
817− (client) =>
818− client.organizations.remove({
819− path: { organizationId: organization.id },
820− }),
821− );
822− assertTrue(deleteOrg.removed);
823−
824− const listStaleWorkspacesError = yield* expectLeft(
825− withControlPlaneClient(
826− { runtime, accountId: "acc_del" },
827− (client) =>
828− client.workspaces.list({
829− path: { organizationId: organization.id },
830− }),
831− ),
832− );
833− expect(listStaleWorkspacesError._tag).toBe("ControlPlaneForbiddenError");
834−
835− const createStaleWorkspaceError = yield* expectLeft(
836− withControlPlaneClient(
837− { runtime, accountId: "acc_del" },
838− (client) =>
839− client.workspaces.create({
840− path: { organizationId: organization.id },
841− payload: { name: "Should Fail" },
842− }),
843− ),
844− );
845− expect(createStaleWorkspaceError._tag).toBe("ControlPlaneForbiddenError");
846−
847− const getDeletedWorkspaceError = yield* expectLeft(
848− withControlPlaneClient(
849− { runtime, accountId: "acc_del" },
850− (client) =>
851− client.workspaces.get({
852− path: { workspaceId: workspace.id },
853− }),
854− ),
855− );
856− expect(getDeletedWorkspaceError._tag).toBe("ControlPlaneForbiddenError");
857− }),
858− );
859−
860− it.scoped("rejects unauthenticated calls", () =>
861− Effect.gen(function* () {
862− const runtime = yield* makeRuntime;
863−
864− const error = yield* expectLeft(
865− withControlPlaneClient({ runtime }, (client) => client.organizations.list({})),
866− );
867−
868− expect(error._tag).toBe("ControlPlaneUnauthorizedError");
869− }),
870− );
871470 });
packages/control-plane/src/runtime/execution-http.test.ts+2−2
@@ -9,7 +9,7 @@ import { makeToolInvokerFromTools } from "@executor/codemode-core"; · 8 unmodified lines
99 import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess";
1010
1111 import {
12− createSqlControlPlaneRuntime,
12+ createControlPlaneRuntime,
1313 } from "./index";
1414 import { withControlPlaneClient } from "./test-http-client";
1515
@@ -40,7 +40,7 @@ const makeExecutionResolver = () => { · 24 unmodified lines
4040 };
4141
4242 const makeRuntime = Effect.acquireRelease(
43− createSqlControlPlaneRuntime({
43+ createControlPlaneRuntime({
4444 localDataDir: ":memory:",
4545 workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-http-")),
4646 executionResolver: makeExecutionResolver(),
packages/control-plane/src/runtime/execution-mcp-resume.test.ts+2−2
@@ -18,7 +18,7 @@ import { discoverMcpToolsFromConnector } from "@executor/codemode-mcp"; · 17 unmodified lines
1818 import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess";
1919
2020 import {
21− createSqlControlPlaneRuntime,
21+ createControlPlaneRuntime,
2222 type ResolveExecutionEnvironment,
2323 } from "./index";
2424 import { withControlPlaneClient } from "./test-http-client";
@@ -284,7 +284,7 @@ describe("execution-mcp-resume", () => { · 259 unmodified lines
284284 Effect.gen(function* () {
285285 const mcpServer = yield* makeFormElicitationServer;
286286 const runtime = yield* Effect.acquireRelease(
287− createSqlControlPlaneRuntime({
287+ createControlPlaneRuntime({
288288 localDataDir: ":memory:",
289289 workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-")),
290290 executionResolver: makeMcpExecutionResolver(mcpServer.endpoint),
packages/control-plane/src/runtime/index.ts+14−16
@@ -88,12 +88,12 @@ export type RuntimeControlPlaneOptions = { · 87 unmodified lines
8888 const detailsFromCause = (cause: unknown): string =>
8989 cause instanceof Error ? cause.message : String(cause);
9090
91−const toLocalInstallationBootstrapError = (
91+const toLocalRuntimeBootstrapError = (
9292 cause: unknown,
9393 ): SqlPersistenceBootstrapError => {
9494 const details = detailsFromCause(cause);
9595 return new SqlPersistenceBootstrapError({
96− message: `Failed provisioning local installation: ${details}`,
96+ message: `Failed initializing local runtime: ${details}`,
9797 details,
9898 });
9999 };
@@ -165,19 +165,19 @@ export const createRuntimeControlPlaneLayer = ( · 65 unmodified lines
165165 );
166166 };
167167
168−export type SqlControlPlaneRuntime = {
168+export type ControlPlaneRuntime = {
169169 persistence: SqlControlPlanePersistence;
170170 localInstallation: LocalInstallation;
171171 runtimeLayer: RuntimeControlPlaneLayer;
172172 close: () => Promise<void>;
173173 };
174174
175−export type CreateSqlControlPlaneRuntimeOptions = CreateSqlRuntimeOptions
175+export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions
176176 & RuntimeControlPlaneOptions;
177177
178−export const createSqlControlPlaneRuntime = (
179− options: CreateSqlControlPlaneRuntimeOptions,
180−): Effect.Effect<SqlControlPlaneRuntime, SqlPersistenceBootstrapError> =>
178+export const createControlPlaneRuntime = (
179+ options: CreateControlPlaneRuntimeOptions,
180+): Effect.Effect<ControlPlaneRuntime, SqlPersistenceBootstrapError> =>
181181 Effect.gen(function* () {
182182 const scope = yield* Scope.make();
183183 const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe(
@@ -198,7 +198,7 @@ export const createSqlControlPlaneRuntime = ( · 14 unmodified lines
198198 resolveLocalWorkspaceContext({
199199 workspaceRoot: options.workspaceRoot,
200200 }),
201− catch: toLocalInstallationBootstrapError,
201+ catch: toLocalRuntimeBootstrapError,
202202 }).pipe(
203203 Effect.catchAll((error) =>
204204 closeScope(scope).pipe(
@@ -210,7 +210,7 @@ export const createSqlControlPlaneRuntime = ( · 5 unmodified lines
210210 rows,
211211 context: localWorkspaceContext,
212212 }).pipe(
213− Effect.mapError(toLocalInstallationBootstrapError),
213+ Effect.mapError(toLocalRuntimeBootstrapError),
214214 Effect.catchAll((error) =>
215215 closeScope(scope).pipe(
216216 Effect.zipRight(Effect.fail(error)),
@@ -219,7 +219,7 @@ export const createSqlControlPlaneRuntime = ( · 2 unmodified lines
219219
220220 const loadedLocalConfig = yield* Effect.tryPromise({
221221 try: () => loadLocalExecutorConfig(localWorkspaceContext),
222− catch: toLocalInstallationBootstrapError,
222+ catch: toLocalRuntimeBootstrapError,
223223 }).pipe(
224224 Effect.catchAll((error) =>
225225 closeScope(scope).pipe(
@@ -228,14 +228,10 @@ export const createSqlControlPlaneRuntime = ( · 2 unmodified lines
228228 );
229229
230230 const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({
231− rows,
232231 context: localWorkspaceContext,
233232 loadedConfig: loadedLocalConfig,
234− installation: {
235− workspaceId: localInstallation.workspaceId,
236− },
237233 }).pipe(
238− Effect.mapError(toLocalInstallationBootstrapError),
234+ Effect.mapError(toLocalRuntimeBootstrapError),
239235 Effect.catchAll((error) =>
240236 closeScope(scope).pipe(
241237 Effect.zipRight(Effect.fail(error)),
@@ -262,6 +258,7 @@ export const createSqlControlPlaneRuntime = ( · 20 unmodified lines
262258 installation: {
263259 workspaceId: localInstallation.workspaceId,
264260 accountId: localInstallation.accountId,
261+ organizationId: localInstallation.organizationId,
265262 },
266263 loadedConfig: {
267264 ...loadedLocalConfig,
@@ -277,7 +274,7 @@ export const createSqlControlPlaneRuntime = ( · 9 unmodified lines
277274 resolveSecretMaterial,
278275 });
279276 const actorResolver =
280− options.actorResolver ?? createHeaderActorResolver(rows);
277+ options.actorResolver ?? createHeaderActorResolver();
281278
282279 const runtimeContext = Context.empty().pipe(
283280 Context.add(ControlPlaneActorResolver, actorResolver),
@@ -288,6 +285,7 @@ export const createSqlControlPlaneRuntime = ( · 4 unmodified lines
288285 installation: {
289286 workspaceId: localInstallation.workspaceId,
290287 accountId: localInstallation.accountId,
288+ organizationId: localInstallation.organizationId,
291289 },
292290 loadedConfig: {
293291 ...loadedLocalConfig,
packages/control-plane/src/runtime/local-installation.test.ts+8−15
@@ -2,17 +2,16 @@ import { existsSync, mkdtempSync } from "node:fs"; · 1 unmodified line
22 import { tmpdir } from "node:os";
33 import { join } from "node:path";
44 import { describe, expect, it } from "@effect/vitest";
5−import { assertTrue } from "@effect/vitest/utils";
65 import * as Effect from "effect/Effect";
76
8−import { createSqlControlPlaneRuntime } from "./index";
7+import { createControlPlaneRuntime } from "./index";
98 import { getOrProvisionLocalInstallation } from "./local-installation";
109 import { resolveLocalWorkspaceContext } from "./local-config";
1110
1211 const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-"));
1312
1413 const makeRuntime = Effect.acquireRelease(
15− createSqlControlPlaneRuntime({
14+ createControlPlaneRuntime({
1615 localDataDir: ":memory:",
1716 workspaceRoot: TEST_WORKSPACE_ROOT,
1817 }),
@@ -20,24 +19,18 @@ const makeRuntime = Effect.acquireRelease( · 1 unmodified line
2019 );
2120
2221 describe("local-installation", () => {
23− it.scoped("provisions a local account, organization, and workspace on first boot", () =>
22+ it.scoped("derives a stable local identity on first boot", () =>
2423 Effect.gen(function* () {
2524 const runtime = yield* makeRuntime;
2625 const installation = runtime.localInstallation;
2726
28− const account = yield* runtime.persistence.rows.accounts.getById(installation.accountId);
29− const organization = yield* runtime.persistence.rows.organizations.getById(
30− installation.organizationId,
31− );
32− const workspace = yield* runtime.persistence.rows.workspaces.getById(
33− installation.workspaceId,
34− );
35−
36− assertTrue(account._tag === "Some");
37− assertTrue(organization._tag === "Some");
38− assertTrue(workspace._tag === "Some");
27+ expect(installation.id.startsWith("local_")).toBe(true);
28+ expect(installation.accountId).toBe("acc_local_default");
29+ expect(installation.organizationId).toBe("org_local_personal");
30+ expect(installation.workspaceId.startsWith("ws_local_")).toBe(true);
3931 expect(existsSync(join(TEST_WORKSPACE_ROOT, ".executor", "executor.jsonc"))).toBe(false);
4032 }),
33+ 60_000,
4134 );
4235
4336 it.scoped("is idempotent when loading the default local installation", () =>
packages/control-plane/src/runtime/local-installation.ts+31−257
@@ -1,283 +1,57 @@
1−import { type SqlControlPlaneRows } from "#persistence";
1+import { createHash } from "node:crypto";
2+
3+import type { SqlControlPlaneRows } from "#persistence";
24 import {
35 AccountIdSchema,
46 InstallationIdSchema,
57 OrganizationIdSchema,
6− OrganizationMemberIdSchema,
78 WorkspaceIdSchema,
8− type Account,
99 type LocalInstallation,
10− type Organization,
11− type OrganizationMembership,
12− type Workspace,
1310 } from "#schema";
1411 import * as Effect from "effect/Effect";
15−import * as Option from "effect/Option";
16−
17−import {
18− defaultWorkspaceDisplayName,
19− type ResolvedLocalWorkspaceContext,
20−} from "./local-config";
2112
22−const LEGACY_LOCAL_INSTALLATION_ID = InstallationIdSchema.make("local_default");
23−const LOCAL_ACCOUNT_SUBJECT = "local:default";
24−const PERSONAL_ORGANIZATION_SLUG = "personal";
13+import type { ResolvedLocalWorkspaceContext } from "./local-config";
2514
26−const trimOrNull = (value: string | null | undefined): string | null => {
27− if (value == null) {
28− return null;
29− }
30− const trimmed = value.trim();
31− return trimmed.length > 0 ? trimmed : null;
32−};
15+const LOCAL_ACCOUNT_ID = AccountIdSchema.make("acc_local_default");
16+const LOCAL_ORGANIZATION_ID = OrganizationIdSchema.make("org_local_personal");
3317
34−const buildAccount = (now: number): Account => {
35− const id = AccountIdSchema.make(`acc_${crypto.randomUUID()}`);
18+const stableHash = (value: string): string =>
19+ createHash("sha256").update(value).digest("hex").slice(0, 16);
3620
37− return {
38− id,
39− provider: "local",
40− subject: LOCAL_ACCOUNT_SUBJECT,
41− email: null,
42− displayName: "Local User",
43− createdAt: now,
44− updatedAt: now,
45− };
46−};
47−
48−const buildOrganization = (
49− accountId: Account["id"],
50− now: number,
51−): Organization => ({
52− id: OrganizationIdSchema.make(`org_${crypto.randomUUID()}`),
53− slug: PERSONAL_ORGANIZATION_SLUG,
54− name: "Personal",
55− status: "active",
56− createdByAccountId: accountId,
57− createdAt: now,
58− updatedAt: now,
59−});
60−
61−const buildOwnerMembership = (
62− organizationId: Organization["id"],
63− accountId: Account["id"],
64− now: number,
65−): OrganizationMembership => ({
66− id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`),
67− organizationId,
68− accountId,
69− role: "owner",
70− status: "active",
71− billable: true,
72− invitedByAccountId: null,
73− joinedAt: now,
74− createdAt: now,
75− updatedAt: now,
76−});
77−
78−const buildWorkspace = (
79− organizationId: Organization["id"],
80− accountId: Account["id"],
81− workspaceName: string,
82− now: number,
83−): Workspace => ({
84− id: WorkspaceIdSchema.make(`ws_${crypto.randomUUID()}`),
85− organizationId,
86− name: workspaceName,
87− createdByAccountId: accountId,
88− createdAt: now,
89− updatedAt: now,
90−});
21+const normalizeSlashPath = (value: string): string =>
22+ value.replaceAll("\\", "/");
9123
92−const chooseWorkspaceName = (
93− desiredName: string,
94− existing: ReadonlyArray<Workspace>,
95−): string => {
96− const trimmed = trimOrNull(desiredName) ?? "Workspace";
97− const matching = existing.find((workspace) => workspace.name === trimmed);
98− if (!matching) {
99− return trimmed;
100− }
101− return `${trimmed}-${matching.id.slice(-6)}`;
102−};
103−
104−const ensureLocalAccount = (
105− rows: SqlControlPlaneRows,
106− now: number,
107−): Effect.Effect<Account, unknown> =>
108− Effect.gen(function* () {
109− const existing = yield* rows.accounts.getByProviderAndSubject("local", LOCAL_ACCOUNT_SUBJECT);
110− if (Option.isSome(existing)) {
111− return existing.value;
112− }
113−
114− const account = buildAccount(now);
115− yield* rows.accounts.upsert(account);
116− return account;
117− });
118−
119−const ensurePersonalOrganization = (
120− rows: SqlControlPlaneRows,
121− accountId: Account["id"],
122− now: number,
123−): Effect.Effect<Organization, unknown> =>
124− Effect.gen(function* () {
125− const existing = yield* rows.organizations.getBySlug(PERSONAL_ORGANIZATION_SLUG);
126− if (Option.isSome(existing)) {
127− return existing.value;
128− }
129−
130− const organization = buildOrganization(accountId, now);
131− const membership = buildOwnerMembership(organization.id, accountId, now);
132− yield* rows.organizations.insertWithOwnerMembership(organization, membership);
133− return organization;
134− });
135−
136−const ensureOwnerMembership = (
137− rows: SqlControlPlaneRows,
138− organizationId: Organization["id"],
139− accountId: Account["id"],
140− now: number,
141−): Effect.Effect<void, unknown> =>
142− Effect.gen(function* () {
143− const existing = yield* rows.organizationMemberships.getByOrganizationAndAccount(
144− organizationId,
145− accountId,
146− );
147− if (Option.isSome(existing)) {
148− return;
149− }
150−
151− yield* rows.organizationMemberships.upsert(
152− buildOwnerMembership(organizationId, accountId, now),
153− );
154− });
155−
156−const provisionWorkspaceForContext = (input: {
157− rows: SqlControlPlaneRows;
158− accountId: Account["id"];
159− organizationId: Organization["id"];
160− context: ResolvedLocalWorkspaceContext;
161− now: number;
162−}): Effect.Effect<Workspace, unknown> =>
163− Effect.gen(function* () {
164− const workspaces = yield* input.rows.workspaces.listByOrganizationId(input.organizationId);
165− const workspaceName = chooseWorkspaceName(
166− defaultWorkspaceDisplayName(input.context),
167− workspaces,
168− );
169− const workspace = buildWorkspace(
170− input.organizationId,
171− input.accountId,
172− workspaceName,
173− input.now,
174− );
175− yield* input.rows.workspaces.insert(workspace);
176− return workspace;
177− });
24+const deriveWorkspaceId = (context: ResolvedLocalWorkspaceContext) =>
25+ WorkspaceIdSchema.make(
26+ `ws_local_${stableHash(normalizeSlashPath(context.workspaceRoot))}`,
27+ );
17828
179−const createInstallationForWorkspace = (input: {
180− installationId: LocalInstallation["id"];
181− accountId: Account["id"];
182− organizationId: Organization["id"];
183− workspaceId: Workspace["id"];
184− now: number;
185−}): LocalInstallation => ({
186− id: input.installationId,
187− accountId: input.accountId,
188− organizationId: input.organizationId,
189− workspaceId: input.workspaceId,
190− createdAt: input.now,
191− updatedAt: input.now,
29+export const deriveLocalInstallation = (
30+ context: ResolvedLocalWorkspaceContext,
31+): LocalInstallation => ({
32+ id: InstallationIdSchema.make(context.installationId),
33+ accountId: LOCAL_ACCOUNT_ID,
34+ organizationId: LOCAL_ORGANIZATION_ID,
35+ workspaceId: deriveWorkspaceId(context),
36+ // Derived local identity is deterministic; timestamps are not authoritative state.
37+ createdAt: 0,
38+ updatedAt: 0,
19239 });
19340
19441 export const loadLocalInstallation = (
195− rows: SqlControlPlaneRows,
42+ _rows: SqlControlPlaneRows,
19643 context: ResolvedLocalWorkspaceContext,
197−): Effect.Effect<LocalInstallation | null, unknown> =>
198− rows.localInstallations.getById(InstallationIdSchema.make(context.installationId)).pipe(
199− Effect.map((result) => (Option.isSome(result) ? result.value : null)),
200− );
201−
202−const migrateLegacyLocalInstallation = (input: {
203− rows: SqlControlPlaneRows;
204− context: ResolvedLocalWorkspaceContext;
205− now: number;
206−}): Effect.Effect<LocalInstallation | null, unknown> =>
207− Effect.gen(function* () {
208− const legacy = yield* input.rows.localInstallations.getById(LEGACY_LOCAL_INSTALLATION_ID);
209− if (Option.isNone(legacy)) {
210− return null;
211− }
212−
213− const migrated = createInstallationForWorkspace({
214− installationId: InstallationIdSchema.make(input.context.installationId),
215− accountId: legacy.value.accountId,
216− organizationId: legacy.value.organizationId,
217− workspaceId: legacy.value.workspaceId,
218− now: input.now,
219− });
220− yield* input.rows.localInstallations.upsert(migrated);
221−
222− yield* input.rows.workspaces.update(legacy.value.workspaceId, {
223− name: defaultWorkspaceDisplayName(input.context),
224− updatedAt: input.now,
225− });
226−
227− return migrated;
228− });
44+): Effect.Effect<LocalInstallation, never> =>
45+ Effect.succeed(deriveLocalInstallation(context));
22946
23047 export const provisionLocalInstallation = (input: {
23148 rows: SqlControlPlaneRows;
23249 context: ResolvedLocalWorkspaceContext;
233−}): Effect.Effect<LocalInstallation, unknown> =>
234− Effect.gen(function* () {
235− const now = Date.now();
236− const installationId = InstallationIdSchema.make(input.context.installationId);
237−
238− const existing = yield* input.rows.localInstallations.getById(installationId);
239− if (Option.isSome(existing)) {
240− return existing.value;
241− }
242−
243− const migrated = yield* migrateLegacyLocalInstallation({
244− rows: input.rows,
245− context: input.context,
246− now,
247− });
248− if (migrated !== null) {
249− return migrated;
250− }
251−
252− const account = yield* ensureLocalAccount(input.rows, now);
253− const organization = yield* ensurePersonalOrganization(input.rows, account.id, now);
254− yield* ensureOwnerMembership(input.rows, organization.id, account.id, now);
255− const workspace = yield* provisionWorkspaceForContext({
256− rows: input.rows,
257− accountId: account.id,
258− organizationId: organization.id,
259− context: input.context,
260− now,
261− });
262− const installation = createInstallationForWorkspace({
263− installationId,
264− accountId: account.id,
265− organizationId: organization.id,
266− workspaceId: workspace.id,
267− now,
268− });
269−
270− yield* input.rows.localInstallations.upsert(installation);
271−
272− return installation;
273− });
50+}): Effect.Effect<LocalInstallation, never> =>
51+ loadLocalInstallation(input.rows, input.context);
27452
27553 export const getOrProvisionLocalInstallation = (input: {
27654 rows: SqlControlPlaneRows;
27755 context: ResolvedLocalWorkspaceContext;
278−}): Effect.Effect<LocalInstallation, unknown> =>
279− Effect.flatMap(loadLocalInstallation(input.rows, input.context), (existing) =>
280− existing
281− ? Effect.succeed(existing)
282− : provisionLocalInstallation(input),
283− );
56+}): Effect.Effect<LocalInstallation, never> =>
57+ loadLocalInstallation(input.rows, input.context);
packages/control-plane/src/runtime/local-operations.ts+1−21
@@ -187,27 +187,7 @@ export const getLocalInstallation = () => · 186 unmodified lines
187187 const context = runtimeLocalWorkspace?.context
188188 ?? (yield* Effect.promise(() => resolveLocalWorkspaceContext()));
189189
190− const installation = yield* loadLocalInstallation(store, context).pipe(
191− Effect.mapError((error) =>
192− error instanceof ControlPlanePersistenceError
193− ? localOps.installation.storage(error)
194− : localOps.installation.unknownStorage(
195− error,
196− "Failed loading local installation",
197− ),
198− ),
199− );
200−
201− if (installation === null) {
202− return yield* Effect.fail(
203− localOps.installation.notFound(
204− "Local installation not found",
205− "No local installation has been provisioned",
206− ),
207− );
208− }
209−
210− return installation;
190+ return yield* loadLocalInstallation(store, context);
211191 });
212192
213193 export const getSourceCredentialInteraction = (input: {
packages/control-plane/src/runtime/local-runtime-context.ts+22−1
@@ -2,7 +2,7 @@ import * as Context from "effect/Context"; · 1 unmodified line
22 import * as Effect from "effect/Effect";
33 import * as Option from "effect/Option";
44
5−import type { AccountId, WorkspaceId } from "#schema";
5+import type { AccountId, OrganizationId, WorkspaceId } from "#schema";
66 import type {
77 LoadedLocalExecutorConfig,
88 ResolvedLocalWorkspaceContext,
@@ -13,6 +13,7 @@ export type RuntimeLocalWorkspaceState = { · 4 unmodified lines
1313 installation: {
1414 workspaceId: WorkspaceId;
1515 accountId: AccountId;
16+ organizationId: OrganizationId;
1617 };
1718 loadedConfig: LoadedLocalExecutorConfig;
1819 };
@@ -27,3 +28,23 @@ export const getRuntimeLocalWorkspaceOption = () => · 8 unmodified lines
2728 ).pipe(
2829 Effect.map((option) => (Option.isSome(option) ? option.value : null)),
2930 ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>;
31+
32+export const requireRuntimeLocalWorkspace = (workspaceId?: WorkspaceId) =>
33+ Effect.flatMap(getRuntimeLocalWorkspaceOption(), (runtimeLocalWorkspace) => {
34+ if (runtimeLocalWorkspace === null) {
35+ return Effect.fail(new Error("Runtime local workspace is unavailable"));
36+ }
37+
38+ if (
39+ workspaceId !== undefined
40+ && runtimeLocalWorkspace.installation.workspaceId !== workspaceId
41+ ) {
42+ return Effect.fail(
43+ new Error(
44+ `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,
45+ ),
46+ );
47+ }
48+
49+ return Effect.succeed(runtimeLocalWorkspace);
50+ });
packages/control-plane/src/runtime/local-workspace-sync.ts+1−45
@@ -1,13 +1,7 @@
1−import type { SqlControlPlaneRows } from "#persistence";
2−import {
3− type Policy,
4− type Workspace,
5−} from "#schema";
1+import { type Policy } from "#schema";
62 import * as Effect from "effect/Effect";
7−import * as Option from "effect/Option";
83
94 import {
10− defaultWorkspaceDisplayName,
115 type LoadedLocalExecutorConfig,
126 type ResolvedLocalWorkspaceContext,
137 } from "./local-config";
@@ -45,33 +39,6 @@ export const derivePolicyConfigKey = ( · 31 unmodified lines
4539 return candidate;
4640 };
4741
48−const ensureWorkspaceMetadata = (input: {
49− rows: SqlControlPlaneRows;
50− installation: {
51− workspaceId: Workspace["id"];
52− };
53− context: ResolvedLocalWorkspaceContext;
54− config: LoadedLocalExecutorConfig["config"];
55−}): Effect.Effect<void, Error, never> =>
56− Effect.gen(function* () {
57− const workspace = yield* input.rows.workspaces.getById(input.installation.workspaceId);
58− if (Option.isNone(workspace)) {
59− return;
60− }
61−
62− const desiredName =
63− trimOrNull(input.config?.workspace?.name)
64− ?? defaultWorkspaceDisplayName(input.context);
65− if (workspace.value.name === desiredName) {
66− return;
67− }
68−
69− yield* input.rows.workspaces.update(workspace.value.id, {
70− name: desiredName,
71− updatedAt: Date.now(),
72− });
73− });
74−
7542 const pruneLocalWorkspaceState = (input: {
7643 context: ResolvedLocalWorkspaceContext;
7744 loadedConfig: LoadedLocalExecutorConfig;
@@ -122,21 +89,10 @@ const pruneLocalWorkspaceState = (input: { · 44 unmodified lines
12289 });
12390
12491 export const synchronizeLocalWorkspaceState = (input: {
125− rows: SqlControlPlaneRows;
12692 context: ResolvedLocalWorkspaceContext;
12793 loadedConfig: LoadedLocalExecutorConfig;
128− installation: {
129− workspaceId: Workspace["id"];
130− };
13194 }): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, never> =>
13295 Effect.gen(function* () {
133− yield* ensureWorkspaceMetadata({
134− rows: input.rows,
135− installation: input.installation,
136− context: input.context,
137− config: input.loadedConfig.config,
138− });
139−
14096 yield* pruneLocalWorkspaceState({
14197 context: input.context,
14298 loadedConfig: input.loadedConfig,
packages/control-plane/src/runtime/operations-shared.ts+0−55
@@ -1,21 +1,14 @@
11 import {
22 ControlPlaneBadRequestError,
3− ControlPlaneNotFoundError,
43 ControlPlaneStorageError,
54 } from "../api/errors";
65 import { ControlPlanePersistenceError } from "#persistence";
7−import type { Organization } from "#schema";
86 import * as Effect from "effect/Effect";
9−import * as Option from "effect/Option";
107
118 import {
129 asOperationErrors,
13− operationErrors,
1410 type OperationErrorsLike,
1511 } from "./operation-errors";
16−import type { ControlPlaneStoreShape } from "./store";
17−import { slugify } from "./slug";
18−
1912 export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
2013
2114 const isUniqueViolation = (error: ControlPlanePersistenceError): boolean =>
@@ -59,51 +52,3 @@ export const parseJsonString = ( · 37 unmodified lines
5952 );
6053 },
6154 });
62−
63−export const ensureUniqueOrganizationSlug = (
64− store: ControlPlaneStoreShape,
65− baseName: string,
66− operation = operationErrors("organizations.create"),
67−): Effect.Effect<string, ControlPlaneStorageError> =>
68− Effect.gen(function* () {
69− const normalized = slugify(baseName);
70− const seed = normalized.length > 0 ? normalized : "item";
71−
72− let counter = 0;
73− while (true) {
74− const candidate = counter === 0 ? seed : `${seed}-${counter + 1}`;
75−
76− const existing = yield* operation.child("slug_lookup").mapStorage(
77− store.organizations.getBySlug(candidate as Organization["slug"]),
78− );
79−
80− if (Option.isNone(existing)) {
81− return candidate;
82− }
83−
84− counter += 1;
85− }
86− });
87−
88−export const ensureOrganizationExists = (
89− store: ControlPlaneStoreShape,
90− operation: OperationErrorsLike,
91− organizationId: Organization["id"],
92−): Effect.Effect<Organization, ControlPlaneNotFoundError | ControlPlaneStorageError> =>
93− Effect.gen(function* () {
94− const errors = asOperationErrors(operation);
95− const organization = yield* errors.child("organization_lookup").mapStorage(
96− store.organizations.getById(organizationId),
97− );
98−
99− if (Option.isNone(organization)) {
100− return yield* Effect.fail(
101− errors.notFound(
102− "Organization not found",
103− `organizationId=${organizationId}`,
104− ),
105− );
106− }
107−
108− return organization.value;
109− });
packages/control-plane/src/runtime/organizations-operations.ts+0−434
@@ -1,434 +0,0 @@
1−import type {
2− CreateMembershipPayload,
3− UpdateMembershipPayload,
4−} from "../api/memberships/api";
5−import type {
6− CreateOrganizationPayload,
7− UpdateOrganizationPayload,
8−} from "../api/organizations/api";
9−import type {
10− CreateWorkspacePayload,
11− UpdateWorkspacePayload,
12−} from "../api/workspaces/api";
13−import {
14− OrganizationIdSchema,
15− OrganizationMemberIdSchema,
16− WorkspaceIdSchema,
17− type AccountId,
18− type Organization,
19− type OrganizationId,
20− type OrganizationMembership,
21− type Workspace,
22− type WorkspaceId,
23−} from "#schema";
24−import * as Effect from "effect/Effect";
25−import * as Option from "effect/Option";
26−
27−import {
28− ensureOrganizationExists,
29− ensureUniqueOrganizationSlug,
30− mapPersistenceError,
31− type Mutable,
32−} from "./operations-shared";
33−import {
34− operationErrors,
35−} from "./operation-errors";
36−import {
37− ControlPlaneStore,
38− type ControlPlaneStoreShape,
39−} from "./store";
40−
41−const organizationOps = {
42− list: operationErrors("organizations.list"),
43− create: operationErrors("organizations.create"),
44− get: operationErrors("organizations.get"),
45− update: operationErrors("organizations.update"),
46− remove: operationErrors("organizations.remove"),
47−} as const;
48−
49−const membershipOps = {
50− list: operationErrors("memberships.list"),
51− create: operationErrors("memberships.create"),
52− update: operationErrors("memberships.update"),
53− remove: operationErrors("memberships.remove"),
54−} as const;
55−
56−const workspaceOps = {
57− list: operationErrors("workspaces.list"),
58− create: operationErrors("workspaces.create"),
59− get: operationErrors("workspaces.get"),
60− update: operationErrors("workspaces.update"),
61− remove: operationErrors("workspaces.remove"),
62−} as const;
63−
64−const listOrganizationsWithStore = (
65− store: ControlPlaneStoreShape,
66− input: { accountId: AccountId },
67−) =>
68− Effect.gen(function* () {
69− const memberships = yield* organizationOps.list.child("memberships").mapStorage(
70− store.organizationMemberships.listByAccountId(input.accountId),
71− );
72−
73− const activeOrganizationIds = Array.from(
74− new Set(
75− memberships
76− .filter((membership) => membership.status === "active")
77− .map((membership) => membership.organizationId),
78− ),
79− );
80−
81− const organizations = yield* Effect.forEach(activeOrganizationIds, (organizationId) =>
82− organizationOps.list.child("organization").mapStorage(
83− store.organizations.getById(organizationId),
84− ).pipe(
85− Effect.map((result) => (Option.isSome(result) ? result.value : null)),
86− ));
87−
88− return organizations.filter((organization): organization is Organization => organization !== null);
89− });
90−
91−export const listOrganizations = (input: { accountId: AccountId }) =>
92− Effect.flatMap(ControlPlaneStore, (store) => listOrganizationsWithStore(store, input));
93−
94−export const createOrganization = (input: {
95− payload: CreateOrganizationPayload;
96− createdByAccountId?: Organization["createdByAccountId"];
97−}) =>
98− Effect.flatMap(ControlPlaneStore, (store) =>
99− Effect.gen(function* () {
100− const name = input.payload.name;
101− const now = Date.now();
102−
103− const slug = input.payload.slug
104− ? input.payload.slug
105− : yield* ensureUniqueOrganizationSlug(store, name, organizationOps.create);
106−
107− const organization: Organization = {
108− id: OrganizationIdSchema.make(`org_${crypto.randomUUID()}`),
109− slug,
110− name,
111− status: "active",
112− createdByAccountId: input.createdByAccountId ?? null,
113− createdAt: now,
114− updatedAt: now,
115− };
116−
117− const ownerMembership: OrganizationMembership | null = input.createdByAccountId
118− ? {
119− id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`),
120− organizationId: organization.id,
121− accountId: input.createdByAccountId,
122− role: "owner",
123− status: "active",
124− billable: true,
125− invitedByAccountId: null,
126− joinedAt: now,
127− createdAt: now,
128− updatedAt: now,
129− }
130− : null;
131−
132− yield* mapPersistenceError(
133− organizationOps.create,
134− store.organizations.insertWithOwnerMembership(organization, ownerMembership),
135− );
136−
137− return organization;
138− }));
139−
140−export const getOrganization = (input: {
141− organizationId: OrganizationId;
142− accountId: AccountId;
143−}) =>
144− Effect.flatMap(ControlPlaneStore, (store) =>
145− Effect.gen(function* () {
146− const membership = yield* organizationOps.get.child("membership").mapStorage(
147− store.organizationMemberships.getByOrganizationAndAccount(
148− input.organizationId,
149− input.accountId,
150− ),
151− );
152−
153− if (Option.isNone(membership) || membership.value.status !== "active") {
154− return yield* Effect.fail(
155− organizationOps.get.notFound(
156− "Organization not found",
157− `organizationId=${input.organizationId}`,
158− ),
159− );
160− }
161−
162− const existing = yield* organizationOps.get.mapStorage(
163− store.organizations.getById(input.organizationId),
164− );
165−
166− if (Option.isNone(existing)) {
167− return yield* Effect.fail(
168− organizationOps.get.notFound(
169− "Organization not found",
170− `organizationId=${input.organizationId}`,
171− ),
172− );
173− }
174−
175− return existing.value;
176− }));
177−
178−export const updateOrganization = (input: {
179− organizationId: OrganizationId;
180− payload: UpdateOrganizationPayload;
181−}) =>
182− Effect.flatMap(ControlPlaneStore, (store) =>
183− Effect.gen(function* () {
184− const patch: Partial<Omit<Mutable<Organization>, "id" | "createdAt">> = {
185− updatedAt: Date.now(),
186− };
187−
188− if (input.payload.name !== undefined) {
189− patch.name = input.payload.name;
190− }
191− if (input.payload.status !== undefined) {
192− patch.status = input.payload.status;
193− }
194−
195− const updated = yield* mapPersistenceError(
196− organizationOps.update,
197− store.organizations.update(input.organizationId, patch),
198− );
199−
200− if (Option.isNone(updated)) {
201− return yield* Effect.fail(
202− organizationOps.update.notFound(
203− "Organization not found",
204− `organizationId=${input.organizationId}`,
205− ),
206− );
207− }
208−
209− return updated.value;
210− }));
211−
212−export const removeOrganization = (input: { organizationId: OrganizationId }) =>
213− Effect.flatMap(ControlPlaneStore, (store) =>
214− organizationOps.remove.mapStorage(
215− store.organizations.removeTreeById(input.organizationId),
216− ).pipe(Effect.map((removed) => ({ removed })))
217− );
218−
219−export const listMemberships = (organizationId: OrganizationId) =>
220− Effect.flatMap(ControlPlaneStore, (store) =>
221− Effect.gen(function* () {
222− yield* ensureOrganizationExists(store, membershipOps.list, organizationId);
223−
224− return yield* membershipOps.list.mapStorage(
225− store.organizationMemberships.listByOrganizationId(organizationId),
226− );
227− }));
228−
229−export const createMembership = (input: {
230− organizationId: OrganizationId;
231− payload: CreateMembershipPayload;
232−}) =>
233− Effect.flatMap(ControlPlaneStore, (store) =>
234− Effect.gen(function* () {
235− yield* ensureOrganizationExists(store, membershipOps.create, input.organizationId);
236−
237− const now = Date.now();
238− const membership: OrganizationMembership = {
239− id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`),
240− organizationId: input.organizationId,
241− accountId: input.payload.accountId,
242− role: input.payload.role,
243− status: input.payload.status ?? "active",
244− billable: input.payload.billable ?? true,
245− invitedByAccountId: input.payload.invitedByAccountId ?? null,
246− joinedAt: (input.payload.status ?? "active") === "active" ? now : null,
247− createdAt: now,
248− updatedAt: now,
249− };
250−
251− yield* mapPersistenceError(
252− membershipOps.create,
253− store.organizationMemberships.upsert(membership),
254− );
255−
256− const stored = yield* membershipOps.create.mapStorage(
257− store.organizationMemberships.getByOrganizationAndAccount(
258− input.organizationId,
259− input.payload.accountId,
260− ),
261− );
262− if (Option.isNone(stored)) {
263− return yield* Effect.fail(
264− membershipOps.create.badRequest(
265− "Membership was not persisted",
266− `organizationId=${input.organizationId} accountId=${input.payload.accountId}`,
267− ),
268− );
269− }
270−
271− return stored.value;
272− }));
273−
274−export const updateMembership = (input: {
275− organizationId: OrganizationId;
276− accountId: AccountId;
277− payload: UpdateMembershipPayload;
278−}) =>
279− Effect.flatMap(ControlPlaneStore, (store) =>
280− Effect.gen(function* () {
281− yield* ensureOrganizationExists(store, membershipOps.update, input.organizationId);
282−
283− const existing = yield* membershipOps.update.mapStorage(
284− store.organizationMemberships.getByOrganizationAndAccount(
285− input.organizationId,
286− input.accountId,
287− ),
288− );
289−
290− if (Option.isNone(existing)) {
291− return yield* Effect.fail(
292− membershipOps.update.badRequest(
293− "Membership not found",
294− `organizationId=${input.organizationId} accountId=${input.accountId}`,
295− ),
296− );
297− }
298−
299− const current = existing.value;
300− const now = Date.now();
301− const next: OrganizationMembership = {
302− ...current,
303− role: input.payload.role ?? current.role,
304− status: input.payload.status ?? current.status,
305− billable: input.payload.billable ?? current.billable,
306− joinedAt:
307− (input.payload.status ?? current.status) === "active"
308− ? (current.joinedAt ?? now)
309− : current.joinedAt,
310− updatedAt: now,
311− };
312−
313− yield* mapPersistenceError(
314− membershipOps.update,
315− store.organizationMemberships.upsert(next),
316− );
317−
318− return next;
319− }));
320−
321−export const removeMembership = (input: {
322− organizationId: OrganizationId;
323− accountId: AccountId;
324−}) =>
325− Effect.flatMap(ControlPlaneStore, (store) =>
326− Effect.gen(function* () {
327− yield* ensureOrganizationExists(store, membershipOps.remove, input.organizationId);
328−
329− const removed = yield* membershipOps.remove.mapStorage(
330− store.organizationMemberships.removeByOrganizationAndAccount(
331− input.organizationId,
332− input.accountId,
333− ),
334− );
335−
336− return { removed };
337− }));
338−
339−export const listWorkspaces = (organizationId: OrganizationId) =>
340− Effect.flatMap(ControlPlaneStore, (store) =>
341− Effect.gen(function* () {
342− yield* ensureOrganizationExists(store, workspaceOps.list, organizationId);
343−
344− return yield* workspaceOps.list.mapStorage(
345− store.workspaces.listByOrganizationId(organizationId),
346− );
347− }));
348−
349−export const createWorkspace = (input: {
350− organizationId: OrganizationId;
351− payload: CreateWorkspacePayload;
352− createdByAccountId?: Workspace["createdByAccountId"];
353−}) =>
354− Effect.flatMap(ControlPlaneStore, (store) =>
355− Effect.gen(function* () {
356− yield* ensureOrganizationExists(store, workspaceOps.create, input.organizationId);
357− const name = input.payload.name;
358− const now = Date.now();
359−
360− const workspace: Workspace = {
361− id: WorkspaceIdSchema.make(`ws_${crypto.randomUUID()}`),
362− organizationId: input.organizationId,
363− name,
364− createdByAccountId: input.createdByAccountId ?? null,
365− createdAt: now,
366− updatedAt: now,
367− };
368−
369− yield* mapPersistenceError(
370− workspaceOps.create,
371− store.workspaces.insert(workspace),
372− );
373−
374− return workspace;
375− }));
376−
377−export const getWorkspace = (workspaceId: WorkspaceId) =>
378− Effect.flatMap(ControlPlaneStore, (store) =>
379− Effect.gen(function* () {
380− const existing = yield* workspaceOps.get.mapStorage(
381− store.workspaces.getById(workspaceId),
382− );
383−
384− if (Option.isNone(existing)) {
385− return yield* Effect.fail(
386− workspaceOps.get.notFound(
387− "Workspace not found",
388− `workspaceId=${workspaceId}`,
389− ),
390− );
391− }
392−
393− return existing.value;
394− }));
395−
396−export const updateWorkspace = (input: {
397− workspaceId: WorkspaceId;
398− payload: UpdateWorkspacePayload;
399−}) =>
400− Effect.flatMap(ControlPlaneStore, (store) =>
401− Effect.gen(function* () {
402− const patch: Partial<
403− Omit<Mutable<Workspace>, "id" | "organizationId" | "createdAt">
404− > = {
405− updatedAt: Date.now(),
406− };
407−
408− if (input.payload.name !== undefined) {
409− patch.name = input.payload.name;
410− }
411−
412− const updated = yield* mapPersistenceError(
413− workspaceOps.update,
414− store.workspaces.update(input.workspaceId, patch),
415− );
416−
417− if (Option.isNone(updated)) {
418− return yield* Effect.fail(
419− workspaceOps.update.notFound(
420− "Workspace not found",
421− `workspaceId=${input.workspaceId}`,
422− ),
423− );
424− }
425−
426− return updated.value;
427− }));
428−
429−export const removeWorkspace = (input: { workspaceId: WorkspaceId }) =>
430− Effect.flatMap(ControlPlaneStore, (store) =>
431− workspaceOps.remove.mapStorage(
432− store.workspaces.removeById(input.workspaceId),
433− ).pipe(Effect.map((removed) => ({ removed })))
434− );
packages/control-plane/src/runtime/policies-operations.ts+232−573
@@ -7,20 +7,18 @@ import type { · 6 unmodified lines
77 import {
88 PolicyIdSchema,
99 type LocalExecutorConfig,
10− type OrganizationId,
1110 type Policy,
1211 type PolicyId,
1312 type WorkspaceId,
1413 } from "#schema";
1514 import * as Effect from "effect/Effect";
16−import * as Option from "effect/Option";
1715
1816 import {
1917 loadLocalExecutorConfig,
2018 writeProjectLocalExecutorConfig,
2119 } from "./local-config";
2220 import {
23− getRuntimeLocalWorkspaceOption,
21+ requireRuntimeLocalWorkspace,
2422 } from "./local-runtime-context";
2523 import {
2624 loadLocalWorkspaceState,
@@ -29,17 +27,10 @@ import { · 2 unmodified lines
2927 import {
3028 derivePolicyConfigKey,
3129 } from "./local-workspace-sync";
32−import {
33− mapPersistenceError,
34− parseJsonString,
35− type Mutable,
36−} from "./operations-shared";
3730 import {
3831 type OperationErrors,
3932 operationErrors,
4033 } from "./operation-errors";
41−import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
42−
4334 const policyOps = {
4435 list: operationErrors("policies.list"),
4536 create: operationErrors("policies.create"),
@@ -57,15 +48,6 @@ type PolicyScopeContext = { · 11 unmodified lines
5748 const cloneJson = <T>(value: T): T =>
5849 JSON.parse(JSON.stringify(value)) as T;
5950
60−const trimOrNull = (value: string | null | undefined): string | null => {
61− if (value == null) {
62− return null;
63− }
64−
65− const trimmed = value.trim();
66− return trimmed.length > 0 ? trimmed : null;
67−};
68−
6951 const localPolicyIdForConfigKey = (input: {
7052 workspaceRoot: string;
7153 configKey: string;
@@ -121,24 +103,9 @@ const ensureLocalWorkspacePolicyCompatible = ( · 49 unmodified lines
121103 }
122104 });
123105
124−export const loadRuntimeLocalWorkspacePolicies = (input: {
125− store: ControlPlaneStoreShape;
126− workspaceId: WorkspaceId;
127−}) =>
106+export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) =>
128107 Effect.gen(function* () {
129− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
130− if (
131− runtimeLocalWorkspace === null
132− || runtimeLocalWorkspace.installation.workspaceId !== input.workspaceId
133− ) {
134− return null;
135− }
136−
137− const workspace = yield* input.store.workspaces.getById(input.workspaceId);
138− if (Option.isNone(workspace)) {
139− return null;
140− }
141−
108+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
142109 const loadedConfig = yield* Effect.tryPromise({
143110 try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context),
144111 catch: (cause) =>
@@ -160,8 +127,8 @@ export const loadRuntimeLocalWorkspacePolicies = (input: { · 15 unmodified lines
160127 }),
161128 configKey,
162129 scopeType: "workspace" as const,
163− organizationId: workspace.value.organizationId,
164− workspaceId: input.workspaceId,
130+ organizationId: runtimeLocalWorkspace.installation.organizationId,
131+ workspaceId,
165132 targetAccountId: null,
166133 clientId: null,
167134 resourceType: "tool_path" as const,
@@ -181,61 +148,31 @@ export const loadRuntimeLocalWorkspacePolicies = (input: { · 13 unmodified lines
181148 runtimeLocalWorkspace,
182149 loadedConfig,
183150 workspaceState,
184− workspace: workspace.value,
185151 policies,
186152 };
187153 });
188154
189155 const loadWorkspacePolicyContext = (
190− store: ControlPlaneStoreShape,
191156 operation: OperationErrors,
192157 workspaceId: WorkspaceId,
193158 ) =>
194159 Effect.gen(function* () {
195− const workspace = yield* operation.child("workspace").mapStorage(
196− store.workspaces.getById(workspaceId),
197− );
198− if (Option.isNone(workspace)) {
199− return yield* Effect.fail(
160+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId).pipe(
161+ Effect.mapError((cause) =>
200162 operation.notFound(
201163 "Workspace not found",
202− `workspaceId=${workspaceId}`,
164+ cause instanceof Error ? cause.message : String(cause),
203165 ),
204− );
205− }
166+ ),
167+ );
206168
207169 return {
208170 scopeType: "workspace",
209− organizationId: workspace.value.organizationId,
171+ organizationId: runtimeLocalWorkspace.installation.organizationId,
210172 workspaceId,
211173 } satisfies PolicyScopeContext;
212174 });
213175
214−const loadOrganizationPolicyContext = (
215− store: ControlPlaneStoreShape,
216− operation: OperationErrors,
217− organizationId: OrganizationId,
218−) =>
219− Effect.gen(function* () {
220− const organization = yield* operation.child("organization").mapStorage(
221− store.organizations.getById(organizationId),
222− );
223− if (Option.isNone(organization)) {
224− return yield* Effect.fail(
225− operation.notFound(
226− "Organization not found",
227− `organizationId=${organizationId}`,
228− ),
229− );
230− }
231−
232− return {
233− scopeType: "organization",
234− organizationId,
235− workspaceId: null,
236− } satisfies PolicyScopeContext;
237− });
238−
239176 const policyMatchesScope = (policy: Policy, scope: PolicyScopeContext): boolean =>
240177 policy.scopeType === scope.scopeType
241178 && policy.organizationId === scope.organizationId
@@ -270,527 +207,249 @@ const writeLocalPolicyFiles = (input: { · 28 unmodified lines
270207 ),
271208 );
272209
273−const createScopedPolicy = (input: {
274− scope: PolicyScopeContext;
275− payload: CreatePolicyPayload;
276−}) =>
277− Effect.flatMap(ControlPlaneStore, (store) =>
278− Effect.gen(function* () {
279− const now = Date.now();
280− const operation = policyOps.create;
281−
282− if (input.scope.workspaceId !== null) {
283− const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({
284− store,
285− workspaceId: input.scope.workspaceId,
286− }).pipe(
287− Effect.mapError((cause) =>
288− operation.unknownStorage(
289− cause,
290− "Failed loading local workspace policies",
291− ),
292− ),
293− );
294− if (localWorkspace !== null) {
295− yield* ensureLocalWorkspacePolicyCompatible(operation, input.payload);
296−
297− const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
298− const policies = {
299− ...(projectConfig.policies ?? {}),
300− };
301− const configKey = derivePolicyConfigKey({
302− configKey: null,
303− resourcePattern: input.payload.resourcePattern ?? "*",
304− effect: input.payload.effect ?? "allow",
305− approvalMode: input.payload.approvalMode ?? "auto",
306− }, new Set(Object.keys(policies)));
307− const id = localWorkspace.workspaceState.policies[configKey]?.id
308− ?? localPolicyIdForConfigKey({
309− workspaceRoot: localWorkspace.runtimeLocalWorkspace.context.workspaceRoot,
310− configKey,
311− });
312− policies[configKey] = {
313− match: input.payload.resourcePattern ?? "*",
314− action: input.payload.effect ?? "allow",
315− approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto",
316− ...(input.payload.enabled === false ? { enabled: false } : {}),
317− ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}),
318− };
319− const existingState = localWorkspace.workspaceState.policies[configKey];
320− const workspaceState = {
321− ...localWorkspace.workspaceState,
322− policies: {
323− ...localWorkspace.workspaceState.policies,
324− [configKey]: {
325− id,
326− createdAt: existingState?.createdAt ?? now,
327− updatedAt: now,
328− },
329− },
330− };
331− yield* writeLocalPolicyFiles({
332− operation,
333− context: localWorkspace.runtimeLocalWorkspace.context,
334− projectConfig: {
335− ...projectConfig,
336− policies,
337− },
338− workspaceState,
339− });
340−
341− return {
342− id,
343− configKey,
344− scopeType: "workspace",
345− organizationId: input.scope.organizationId,
346− workspaceId: input.scope.workspaceId,
347− targetAccountId: null,
348− clientId: null,
349− resourceType: "tool_path",
350− resourcePattern: policies[configKey]!.match,
351− matchType: "glob",
352− effect: policies[configKey]!.action,
353− approvalMode: policies[configKey]!.approval === "manual" ? "required" : "auto",
354− argumentConditionsJson: null,
355− priority: policies[configKey]!.priority ?? 0,
356− enabled: policies[configKey]!.enabled ?? true,
357− createdAt: workspaceState.policies[configKey]!.createdAt,
358− updatedAt: now,
359− } satisfies Policy;
360− }
361− }
362−
363− const policy: Policy = {
364− id: PolicyIdSchema.make(`pol_${crypto.randomUUID()}`),
365− configKey: null,
366− scopeType: input.scope.scopeType,
367− organizationId: input.scope.organizationId,
368− workspaceId: input.scope.workspaceId,
369− targetAccountId: input.payload.targetAccountId ?? null,
370− clientId: input.payload.clientId ?? null,
371− resourceType: input.payload.resourceType ?? "tool_path",
372− resourcePattern: input.payload.resourcePattern ?? "*",
373− matchType: input.payload.matchType ?? "glob",
374− effect: input.payload.effect ?? "allow",
375− approvalMode: input.payload.approvalMode ?? "auto",
376− argumentConditionsJson: input.payload.argumentConditionsJson ?? null,
377− priority: input.payload.priority ?? 0,
378− enabled: input.payload.enabled ?? true,
379− createdAt: now,
380− updatedAt: now,
381− };
382−
383− if (policy.argumentConditionsJson !== null) {
384− yield* parseJsonString(
385− operation,
386− "argumentConditionsJson",
387− policy.argumentConditionsJson,
388− );
389− }
390−
391− yield* mapPersistenceError(
392− operation,
393− store.policies.insert(policy),
394− );
395−
396− if (policy.workspaceId === null) {
397− return policy;
398− }
399−
400− return policy;
401− }));
402−
403−const getScopedPolicy = (input: {
404− scope: PolicyScopeContext;
405− policyId: PolicyId;
406−}) =>
407− Effect.flatMap(ControlPlaneStore, (store) =>
408− Effect.gen(function* () {
409− if (input.scope.workspaceId !== null) {
410− const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({
411− store,
412− workspaceId: input.scope.workspaceId,
413− }).pipe(
414− Effect.mapError((cause) =>
415− policyOps.get.unknownStorage(
416− cause,
417− "Failed loading local workspace policies",
418− ),
419− ),
420− );
421− if (localWorkspace !== null) {
422− const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
423− if (policy === null) {
424− return yield* Effect.fail(
425− policyOps.get.notFound(
426− "Policy not found",
427− `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`,
428− ),
429− );
430− }
431−
432− return policy;
433− }
434− }
435−
436− const existing = yield* policyOps.get.mapStorage(
437− store.policies.getById(input.policyId),
438− );
439−
440− if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) {
441− return yield* Effect.fail(
442− policyOps.get.notFound(
443− "Policy not found",
444− `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`,
445− ),
446− );
447− }
448−
449− return existing.value;
450− }));
451−
452−const updateScopedPolicy = (input: {
453− scope: PolicyScopeContext;
454− policyId: PolicyId;
455− payload: UpdatePolicyPayload;
456−}) =>
457− Effect.flatMap(ControlPlaneStore, (store) =>
458− Effect.gen(function* () {
459− if (input.scope.workspaceId !== null) {
460− const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({
461− store,
462− workspaceId: input.scope.workspaceId,
463− }).pipe(
464− Effect.mapError((cause) =>
465− policyOps.update.unknownStorage(
466− cause,
467− "Failed loading local workspace policies",
468− ),
469− ),
470− );
471− if (localWorkspace !== null) {
472− yield* ensureLocalWorkspacePolicyCompatible(policyOps.update, input.payload);
473− const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
474− if (existing === null || existing.configKey === null) {
475− return yield* Effect.fail(
476− policyOps.update.notFound(
477− "Policy not found",
478− `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`,
479− ),
480− );
481− }
482−
483− const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
484− const policies = {
485− ...(projectConfig.policies ?? {}),
486− };
487− const existingConfig = policies[existing.configKey] ?? {
488− match: existing.resourcePattern,
489− action: existing.effect,
490− approval: existing.approvalMode === "required" ? "manual" : "auto",
491− };
492− policies[existing.configKey] = {
493− ...existingConfig,
494− ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}),
495− ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}),
496− ...(input.payload.approvalMode !== undefined
497− ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" }
498− : {}),
499− ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}),
500− ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}),
501− };
502− const existingState = localWorkspace.workspaceState.policies[existing.configKey];
503− const updatedAt = Date.now();
504− const workspaceState = {
505− ...localWorkspace.workspaceState,
506− policies: {
507− ...localWorkspace.workspaceState.policies,
508− [existing.configKey]: {
509− id: existing.id,
510− createdAt: existingState?.createdAt ?? existing.createdAt,
511− updatedAt,
512− },
513− },
514− };
515− yield* writeLocalPolicyFiles({
516− operation: policyOps.update,
517− context: localWorkspace.runtimeLocalWorkspace.context,
518− projectConfig: {
519− ...projectConfig,
520− policies,
521− },
522− workspaceState,
523− });
524−
525− return {
526− ...existing,
527− resourcePattern: policies[existing.configKey]!.match,
528− effect: policies[existing.configKey]!.action,
529− approvalMode: policies[existing.configKey]!.approval === "manual" ? "required" : "auto",
530− priority: policies[existing.configKey]!.priority ?? 0,
531− enabled: policies[existing.configKey]!.enabled ?? true,
532− updatedAt,
533− } satisfies Policy;
534− }
535− }
536−
537− const existing = yield* policyOps.update.mapStorage(
538− store.policies.getById(input.policyId),
539− );
540− if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) {
541− return yield* Effect.fail(
542− policyOps.update.notFound(
543− "Policy not found",
544− `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`,
545− ),
546− );
547− }
548−
549− const patch: Partial<
550− Omit<Mutable<Policy>, "id" | "scopeType" | "organizationId" | "workspaceId" | "createdAt">
551− > = {
552− updatedAt: Date.now(),
553− };
554−
555− if (input.payload.targetAccountId !== undefined) {
556− patch.targetAccountId = input.payload.targetAccountId;
557− }
558− if (input.payload.clientId !== undefined) {
559− patch.clientId = input.payload.clientId;
560− }
561− if (input.payload.resourceType !== undefined) {
562− patch.resourceType = input.payload.resourceType;
563− }
564− if (input.payload.resourcePattern !== undefined) {
565− patch.resourcePattern = input.payload.resourcePattern;
566− }
567− if (input.payload.matchType !== undefined) {
568− patch.matchType = input.payload.matchType;
569− }
570− if (input.payload.effect !== undefined) {
571− patch.effect = input.payload.effect;
572− }
573− if (input.payload.approvalMode !== undefined) {
574− patch.approvalMode = input.payload.approvalMode;
575− }
576− if (input.payload.argumentConditionsJson !== undefined) {
577− if (input.payload.argumentConditionsJson !== null) {
578− yield* parseJsonString(
579− policyOps.update,
580− "argumentConditionsJson",
581− input.payload.argumentConditionsJson,
582− );
583− }
584− patch.argumentConditionsJson = input.payload.argumentConditionsJson;
585− }
586− if (input.payload.priority !== undefined) {
587− patch.priority = input.payload.priority;
588− }
589− if (input.payload.enabled !== undefined) {
590− patch.enabled = input.payload.enabled;
591− }
592−
593− const updated = yield* mapPersistenceError(
594− policyOps.update,
595− store.policies.update(input.policyId, patch),
596− );
597− if (Option.isNone(updated)) {
598− return yield* Effect.fail(
599− policyOps.update.notFound(
600− "Policy not found",
601− `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`,
602− ),
603− );
604− }
605−
606− if (updated.value.workspaceId === null) {
607− return updated.value;
608− }
609−
610− return updated.value;
611− }));
612−
613−const removeScopedPolicy = (input: {
614− scope: PolicyScopeContext;
615− policyId: PolicyId;
616−}) =>
617− Effect.flatMap(ControlPlaneStore, (store) =>
618− Effect.gen(function* () {
619− if (input.scope.workspaceId !== null) {
620− const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({
621− store,
622− workspaceId: input.scope.workspaceId,
623− }).pipe(
624− Effect.mapError((cause) =>
625− policyOps.remove.unknownStorage(
626− cause,
627− "Failed loading local workspace policies",
628− ),
629− ),
630− );
631− if (localWorkspace !== null) {
632− const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
633− if (existing === null || existing.configKey === null) {
634− return { removed: false };
635− }
636−
637− const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
638− const policies = {
639− ...(projectConfig.policies ?? {}),
640− };
641− delete policies[existing.configKey];
642− const {
643− [existing.configKey]: _removedPolicy,
644− ...remainingPolicies
645− } = localWorkspace.workspaceState.policies;
646− yield* writeLocalPolicyFiles({
647− operation: policyOps.remove,
648− context: localWorkspace.runtimeLocalWorkspace.context,
649− projectConfig: {
650− ...projectConfig,
651− policies,
652− },
653− workspaceState: {
654− ...localWorkspace.workspaceState,
655− policies: remainingPolicies,
656− },
657− });
658− return { removed: true };
659− }
660− }
661−
662− const existing = yield* policyOps.remove.mapStorage(
663− store.policies.getById(input.policyId),
664− );
665− if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) {
666− return { removed: false };
667− }
668−
669− const removed = yield* policyOps.remove.mapStorage(
670− store.policies.removeById(input.policyId),
671− );
672−
673− return { removed };
674− }));
675−
676−export const listOrganizationPolicies = (organizationId: OrganizationId) =>
677− Effect.flatMap(ControlPlaneStore, (store) =>
678− Effect.gen(function* () {
679− yield* loadOrganizationPolicyContext(store, policyOps.list, organizationId);
680− return yield* policyOps.list.mapStorage(
681− store.policies.listByOrganizationId(organizationId),
682− );
683− }));
684−
685210 export const listPolicies = (workspaceId: WorkspaceId) =>
686− Effect.flatMap(ControlPlaneStore, (store) =>
687− Effect.gen(function* () {
688− yield* loadWorkspacePolicyContext(store, policyOps.list, workspaceId);
689− const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({
690− store,
691− workspaceId,
692− }).pipe(
693− Effect.mapError((cause) =>
694− policyOps.list.unknownStorage(
695− cause,
696− "Failed loading local workspace policies",
697− ),
211+ Effect.gen(function* () {
212+ yield* loadWorkspacePolicyContext(policyOps.list, workspaceId);
213+ const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(workspaceId).pipe(
214+ Effect.mapError((cause) =>
215+ policyOps.list.unknownStorage(
216+ cause,
217+ "Failed loading local workspace policies",
698218 ),
699− );
700− if (localWorkspace !== null) {
701− return localWorkspace.policies;
702− }
703− return yield* policyOps.list.mapStorage(
704− store.policies.listByWorkspaceId(workspaceId),
705− );
706− }));
707−
708−export const createOrganizationPolicy = (input: {
709− organizationId: OrganizationId;
710− payload: CreatePolicyPayload;
711−}) =>
712− Effect.flatMap(ControlPlaneStore, (store) =>
713− Effect.flatMap(
714− loadOrganizationPolicyContext(store, policyOps.create, input.organizationId),
715− (scope) => createScopedPolicy({ scope, payload: input.payload }),
716− ));
219+ ),
220+ );
221+ return localWorkspace.policies;
222+ });
717223
718224 export const createPolicy = (input: {
719225 workspaceId: WorkspaceId;
720226 payload: CreatePolicyPayload;
721227 }) =>
722− Effect.flatMap(ControlPlaneStore, (store) =>
723− Effect.flatMap(
724− loadWorkspacePolicyContext(store, policyOps.create, input.workspaceId),
725− (scope) => createScopedPolicy({ scope, payload: input.payload }),
726− ));
228+ Effect.gen(function* () {
229+ const scope = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId);
230+ const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
231+ Effect.mapError((cause) =>
232+ policyOps.create.unknownStorage(
233+ cause,
234+ "Failed loading local workspace policies",
235+ ),
236+ ),
237+ );
238+ yield* ensureLocalWorkspacePolicyCompatible(policyOps.create, input.payload);
727239
728−export const getOrganizationPolicy = (input: {
729− organizationId: OrganizationId;
730− policyId: PolicyId;
731−}) =>
732− Effect.flatMap(ControlPlaneStore, (store) =>
733− Effect.flatMap(
734− loadOrganizationPolicyContext(store, policyOps.get, input.organizationId),
735− (scope) => getScopedPolicy({ scope, policyId: input.policyId }),
736− ));
240+ const now = Date.now();
241+ const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
242+ const policies = {
243+ ...(projectConfig.policies ?? {}),
244+ };
245+ const configKey = derivePolicyConfigKey({
246+ configKey: null,
247+ resourcePattern: input.payload.resourcePattern ?? "*",
248+ effect: input.payload.effect ?? "allow",
249+ approvalMode: input.payload.approvalMode ?? "auto",
250+ }, new Set(Object.keys(policies)));
251+ const id = localWorkspace.workspaceState.policies[configKey]?.id
252+ ?? localPolicyIdForConfigKey({
253+ workspaceRoot: localWorkspace.runtimeLocalWorkspace.context.workspaceRoot,
254+ configKey,
255+ });
256+
257+ policies[configKey] = {
258+ match: input.payload.resourcePattern ?? "*",
259+ action: input.payload.effect ?? "allow",
260+ approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto",
261+ ...(input.payload.enabled === false ? { enabled: false } : {}),
262+ ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}),
263+ };
264+
265+ const existingState = localWorkspace.workspaceState.policies[configKey];
266+ const workspaceState = {
267+ ...localWorkspace.workspaceState,
268+ policies: {
269+ ...localWorkspace.workspaceState.policies,
270+ [configKey]: {
271+ id,
272+ createdAt: existingState?.createdAt ?? now,
273+ updatedAt: now,
274+ },
275+ },
276+ };
277+ yield* writeLocalPolicyFiles({
278+ operation: policyOps.create,
279+ context: localWorkspace.runtimeLocalWorkspace.context,
280+ projectConfig: {
281+ ...projectConfig,
282+ policies,
283+ },
284+ workspaceState,
285+ });
286+
287+ return {
288+ id,
289+ configKey,
290+ scopeType: "workspace",
291+ organizationId: scope.organizationId,
292+ workspaceId: scope.workspaceId,
293+ targetAccountId: null,
294+ clientId: null,
295+ resourceType: "tool_path",
296+ resourcePattern: policies[configKey]!.match,
297+ matchType: "glob",
298+ effect: policies[configKey]!.action,
299+ approvalMode: policies[configKey]!.approval === "manual" ? "required" : "auto",
300+ argumentConditionsJson: null,
301+ priority: policies[configKey]!.priority ?? 0,
302+ enabled: policies[configKey]!.enabled ?? true,
303+ createdAt: workspaceState.policies[configKey]!.createdAt,
304+ updatedAt: now,
305+ } satisfies Policy;
306+ });
737307
738308 export const getPolicy = (input: {
739309 workspaceId: WorkspaceId;
740310 policyId: PolicyId;
741311 }) =>
742− Effect.flatMap(ControlPlaneStore, (store) =>
743− Effect.flatMap(
744− loadWorkspacePolicyContext(store, policyOps.get, input.workspaceId),
745− (scope) => getScopedPolicy({ scope, policyId: input.policyId }),
746− ));
747−
748−export const updateOrganizationPolicy = (input: {
749− organizationId: OrganizationId;
750− policyId: PolicyId;
751− payload: UpdatePolicyPayload;
752−}) =>
753− Effect.flatMap(ControlPlaneStore, (store) =>
754− Effect.flatMap(
755− loadOrganizationPolicyContext(store, policyOps.update, input.organizationId),
756− (scope) => updateScopedPolicy({
757− scope,
758− policyId: input.policyId,
759− payload: input.payload,
760− }),
761− ));
312+ Effect.gen(function* () {
313+ const scope = yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId);
314+ const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
315+ Effect.mapError((cause) =>
316+ policyOps.get.unknownStorage(
317+ cause,
318+ "Failed loading local workspace policies",
319+ ),
320+ ),
321+ );
322+ const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
323+ if (policy === null || !policyMatchesScope(policy, scope)) {
324+ return yield* Effect.fail(
325+ policyOps.get.notFound(
326+ "Policy not found",
327+ `scopeType=${scope.scopeType} organizationId=${scope.organizationId} workspaceId=${scope.workspaceId} policyId=${input.policyId}`,
328+ ),
329+ );
330+ }
331+ return policy;
332+ });
762333
763334 export const updatePolicy = (input: {
764335 workspaceId: WorkspaceId;
765336 policyId: PolicyId;
766337 payload: UpdatePolicyPayload;
767338 }) =>
768− Effect.flatMap(ControlPlaneStore, (store) =>
769− Effect.flatMap(
770− loadWorkspacePolicyContext(store, policyOps.update, input.workspaceId),
771− (scope) => updateScopedPolicy({
772− scope,
773− policyId: input.policyId,
774− payload: input.payload,
775− }),
776− ));
339+ Effect.gen(function* () {
340+ const scope = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId);
341+ const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
342+ Effect.mapError((cause) =>
343+ policyOps.update.unknownStorage(
344+ cause,
345+ "Failed loading local workspace policies",
346+ ),
347+ ),
348+ );
349+ yield* ensureLocalWorkspacePolicyCompatible(policyOps.update, input.payload);
350+ const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
351+ if (existing === null || existing.configKey === null || !policyMatchesScope(existing, scope)) {
352+ return yield* Effect.fail(
353+ policyOps.update.notFound(
354+ "Policy not found",
355+ `scopeType=${scope.scopeType} organizationId=${scope.organizationId} workspaceId=${scope.workspaceId} policyId=${input.policyId}`,
356+ ),
357+ );
358+ }
777359
778−export const removeOrganizationPolicy = (input: {
779− organizationId: OrganizationId;
780− policyId: PolicyId;
781−}) =>
782− Effect.flatMap(ControlPlaneStore, (store) =>
783− Effect.flatMap(
784− loadOrganizationPolicyContext(store, policyOps.remove, input.organizationId),
785− (scope) => removeScopedPolicy({ scope, policyId: input.policyId }),
786− ));
360+ const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
361+ const policies = {
362+ ...(projectConfig.policies ?? {}),
363+ };
364+ const existingConfig = policies[existing.configKey] ?? {
365+ match: existing.resourcePattern,
366+ action: existing.effect,
367+ approval: existing.approvalMode === "required" ? "manual" : "auto",
368+ };
369+ policies[existing.configKey] = {
370+ ...existingConfig,
371+ ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}),
372+ ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}),
373+ ...(input.payload.approvalMode !== undefined
374+ ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" }
375+ : {}),
376+ ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}),
377+ ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}),
378+ };
379+
380+ const updatedAt = Date.now();
381+ const existingState = localWorkspace.workspaceState.policies[existing.configKey];
382+ const workspaceState = {
383+ ...localWorkspace.workspaceState,
384+ policies: {
385+ ...localWorkspace.workspaceState.policies,
386+ [existing.configKey]: {
387+ id: existing.id,
388+ createdAt: existingState?.createdAt ?? existing.createdAt,
389+ updatedAt,
390+ },
391+ },
392+ };
393+ yield* writeLocalPolicyFiles({
394+ operation: policyOps.update,
395+ context: localWorkspace.runtimeLocalWorkspace.context,
396+ projectConfig: {
397+ ...projectConfig,
398+ policies,
399+ },
400+ workspaceState,
401+ });
402+
403+ return {
404+ ...existing,
405+ resourcePattern: policies[existing.configKey]!.match,
406+ effect: policies[existing.configKey]!.action,
407+ approvalMode: policies[existing.configKey]!.approval === "manual" ? "required" : "auto",
408+ priority: policies[existing.configKey]!.priority ?? 0,
409+ enabled: policies[existing.configKey]!.enabled ?? true,
410+ updatedAt,
411+ } satisfies Policy;
412+ });
787413
788414 export const removePolicy = (input: {
789415 workspaceId: WorkspaceId;
790416 policyId: PolicyId;
791417 }) =>
792− Effect.flatMap(ControlPlaneStore, (store) =>
793− Effect.flatMap(
794− loadWorkspacePolicyContext(store, policyOps.remove, input.workspaceId),
795− (scope) => removeScopedPolicy({ scope, policyId: input.policyId }),
796− ));
418+ Effect.gen(function* () {
419+ const scope = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId);
420+ const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
421+ Effect.mapError((cause) =>
422+ policyOps.remove.unknownStorage(
423+ cause,
424+ "Failed loading local workspace policies",
425+ ),
426+ ),
427+ );
428+ const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;
429+ if (existing === null || existing.configKey === null || !policyMatchesScope(existing, scope)) {
430+ return { removed: false };
431+ }
432+
433+ const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
434+ const policies = {
435+ ...(projectConfig.policies ?? {}),
436+ };
437+ delete policies[existing.configKey];
438+ const {
439+ [existing.configKey]: _removedPolicy,
440+ ...remainingPolicies
441+ } = localWorkspace.workspaceState.policies;
442+ yield* writeLocalPolicyFiles({
443+ operation: policyOps.remove,
444+ context: localWorkspace.runtimeLocalWorkspace.context,
445+ projectConfig: {
446+ ...projectConfig,
447+ policies,
448+ },
449+ workspaceState: {
450+ ...localWorkspace.workspaceState,
451+ policies: remainingPolicies,
452+ },
453+ });
454+ return { removed: true };
455+ });
packages/control-plane/src/runtime/source-auth-service.test.ts+76−60
@@ -1,4 +1,7 @@
1+import { mkdtempSync } from "node:fs";
12 import { createServer } from "node:http";
3+import { tmpdir } from "node:os";
4+import { join } from "node:path";
25
36 import { describe, expect, it } from "vitest";
47 import * as Schema from "effect/Schema";
@@ -26,6 +29,17 @@ import { · 21 unmodified lines
2629 createTerminalSourceAuthSessionPatch,
2730 } from "./source-auth-service";
2831 import { createLiveExecutionManager } from "./live-execution";
32+import {
33+ loadLocalExecutorConfig,
34+ resolveLocalWorkspaceContext,
35+} from "./local-config";
36+import {
37+ readLocalSourceArtifact,
38+} from "./local-source-artifacts";
39+import {
40+ RuntimeLocalWorkspaceService,
41+ type RuntimeLocalWorkspaceState,
42+} from "./local-runtime-context";
2943 import { createDefaultSecretMaterialResolver } from "./secret-material-providers";
3044 import { resolveSourceAuthMaterial } from "./source-auth-material";
3145
@@ -163,6 +177,38 @@ const makePersistence = async (): Promise<SqlControlPlanePersistence> => · 131 unmodified lines
163177 }),
164178 );
165179
180+const makeRuntimeLocalWorkspaceState = async (
181+ workspaceId: ReturnType<typeof WorkspaceIdSchema.make>,
182+ accountId: ReturnType<typeof AccountIdSchema.make>,
183+): Promise<RuntimeLocalWorkspaceState> =>
184+ Effect.runPromise(
185+ Effect.gen(function* () {
186+ const workspaceRoot = mkdtempSync(
187+ join(tmpdir(), "executor-source-auth-service-"),
188+ );
189+ const context = yield* Effect.tryPromise({
190+ try: () => resolveLocalWorkspaceContext({ workspaceRoot }),
191+ catch: (cause) =>
192+ cause instanceof Error ? cause : new Error(String(cause)),
193+ });
194+ const loadedConfig = yield* Effect.tryPromise({
195+ try: () => loadLocalExecutorConfig(context),
196+ catch: (cause) =>
197+ cause instanceof Error ? cause : new Error(String(cause)),
198+ });
199+
200+ return {
201+ context,
202+ installation: {
203+ workspaceId,
204+ accountId,
205+ organizationId: "org_local_personal" as never,
206+ },
207+ loadedConfig,
208+ } satisfies RuntimeLocalWorkspaceState;
209+ }),
210+ );
211+
166212 describe("source-auth-service", () => {
167213 const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema);
168214
@@ -225,36 +271,20 @@ describe("source-auth-service", () => { · 56 unmodified lines
225271
226272 const persistence = await makePersistence();
227273 try {
228− const now = Date.now();
229274 const workspaceId = WorkspaceIdSchema.make("ws_google_oauth");
230275 const accountId = AccountIdSchema.make("acc_google_oauth");
231−
232− await Effect.runPromise(
233− persistence.rows.organizations.insert({
234− id: "org_google_oauth" as any,
235− slug: "google-oauth",
236− name: "Google OAuth",
237− status: "active",
238− createdByAccountId: accountId,
239− createdAt: now,
240− updatedAt: now,
241− }),
242− );
243− await Effect.runPromise(
244− persistence.rows.workspaces.insert({
245− id: workspaceId,
246− organizationId: "org_google_oauth" as any,
247− name: "Workspace",
248− createdByAccountId: accountId,
249− createdAt: now,
250− updatedAt: now,
251− }),
276+ const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
277+ workspaceId,
278+ accountId,
252279 );
253280
254281 const service = createRuntimeSourceAuthService({
255282 rows: persistence.rows,
256283 liveExecutionManager: createLiveExecutionManager(),
257284 getLocalServerBaseUrl: () => "http://localhost:8788",
285+ localConfig: runtimeLocalWorkspace.loadedConfig.config,
286+ workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
287+ localWorkspaceState: runtimeLocalWorkspace,
258288 });
259289
260290 await withGoogleAuthTestServer({
@@ -290,7 +320,12 @@ describe("source-auth-service", () => { · 29 unmodified lines
290320 clientId: "google-test-client",
291321 clientSecret: "google-test-secret",
292322 },
293− }),
323+ }).pipe(
324+ Effect.provideService(
325+ RuntimeLocalWorkspaceService,
326+ runtimeLocalWorkspace,
327+ ),
328+ ),
294329 );
295330
296331 expect(addResult.kind).toBe("oauth_required");
@@ -461,22 +496,14 @@ describe("source-auth-service", () => { · 164 unmodified lines
461496 expect(Option.isNone(oldSecret)).toBe(true);
462497 }
463498
464− const recipeOperations = await Effect.runPromise(
465− persistence.rows.sources.getByWorkspaceAndId(
466− workspaceId,
467− connectedSource.id,
468− ),
469− );
470− expect(Option.isSome(recipeOperations)).toBe(true);
471− const storedSource = Option.getOrNull(recipeOperations)!;
472− const materializedOperations = await Effect.runPromise(
473− persistence.rows.sourceRecipeOperations.listByRevisionId(
474− storedSource.recipeRevisionId,
475− ),
476− );
477− expect(materializedOperations.length).toBeGreaterThan(0);
499+ const localArtifact = await readLocalSourceArtifact({
500+ context: runtimeLocalWorkspace.context,
501+ sourceId: connectedSource.id,
502+ });
503+ expect(localArtifact).not.toBeNull();
504+ expect(localArtifact?.operations.length).toBeGreaterThan(0);
478505 expect(
479− materializedOperations.some(
506+ localArtifact?.operations.some(
480507 (operation) => operation.toolId === "spreadsheets.values.get",
481508 ),
482509 ).toBe(true);
@@ -495,36 +522,20 @@ describe("source-auth-service", () => { · 12 unmodified lines
495522
496523 const persistence = await makePersistence();
497524 try {
498− const now = Date.now();
499525 const workspaceId = WorkspaceIdSchema.make("ws_google_oauth_web");
500526 const accountId = AccountIdSchema.make("acc_google_oauth_web");
501−
502− await Effect.runPromise(
503− persistence.rows.organizations.insert({
504− id: "org_google_oauth_web" as any,
505− slug: "google-oauth-web",
506− name: "Google OAuth Web",
507− status: "active",
508− createdByAccountId: accountId,
509− createdAt: now,
510− updatedAt: now,
511− }),
512− );
513− await Effect.runPromise(
514− persistence.rows.workspaces.insert({
515− id: workspaceId,
516− organizationId: "org_google_oauth_web" as any,
517− name: "Workspace",
518− createdByAccountId: accountId,
519− createdAt: now,
520− updatedAt: now,
521− }),
527+ const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
528+ workspaceId,
529+ accountId,
522530 );
523531
524532 const service = createRuntimeSourceAuthService({
525533 rows: persistence.rows,
526534 liveExecutionManager: createLiveExecutionManager(),
527535 getLocalServerBaseUrl: () => "http://127.0.0.1:8788",
536+ localConfig: runtimeLocalWorkspace.loadedConfig.config,
537+ workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
538+ localWorkspaceState: runtimeLocalWorkspace,
528539 });
529540
530541 await withGoogleAuthTestServer({
@@ -565,6 +576,11 @@ describe("source-auth-service", () => { · 34 unmodified lines
565576 {
566577 baseUrl: "https://app.executor.dev",
567578 },
579+ ).pipe(
580+ Effect.provideService(
581+ RuntimeLocalWorkspaceService,
582+ runtimeLocalWorkspace,
583+ ),
568584 ),
569585 );
570586
packages/control-plane/src/runtime/source-inspection.test.ts+162−121
@@ -1,11 +1,14 @@
1+import { mkdtempSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { join } from "node:path";
4+
15 import { describe, expect, it } from "vitest";
26 import * as Effect from "effect/Effect";
37
48 import {
59 AccountIdSchema,
6− OrganizationIdSchema,
10+ type LocalConfigSource,
711 SourceIdSchema,
8− SourceRecipeIdSchema,
912 SourceRecipeRevisionIdSchema,
1013 SourceRecipeSchemaBundleIdSchema,
1114 WorkspaceIdSchema,
@@ -17,11 +20,27 @@ import { · 5 unmodified lines
1720 type SqlControlPlanePersistence,
1821 } from "#persistence";
1922
23+import {
24+ loadLocalExecutorConfig,
25+ resolveLocalWorkspaceContext,
26+ writeProjectLocalExecutorConfig,
27+} from "./local-config";
28+import {
29+ buildLocalSourceArtifact,
30+ writeLocalSourceArtifact,
31+} from "./local-source-artifacts";
2032 import {
2133 getSourceInspection,
2234 getSourceInspectionSchemaBundle,
2335 getSourceInspectionToolDetail,
2436 } from "./source-inspection";
37+import {
38+ RuntimeLocalWorkspaceService,
39+ type RuntimeLocalWorkspaceState,
40+} from "./local-runtime-context";
41+import {
42+ writeLocalWorkspaceState,
43+} from "./local-workspace-state";
2544 import { ControlPlaneStore } from "./store";
2645
2746 const makePersistence = () =>
@@ -31,30 +50,37 @@ const makePersistence = () => · 3 unmodified lines
3150 }),
3251 );
3352
34−const seedWorkspace = async (input: {
35− persistence: SqlControlPlanePersistence;
36− workspaceId: ReturnType<typeof WorkspaceIdSchema.make>;
37− organizationId: ReturnType<typeof OrganizationIdSchema.make>;
38− accountId: ReturnType<typeof AccountIdSchema.make>;
39−}) => {
40− await Effect.runPromise(input.persistence.rows.organizations.insert({
41− id: input.organizationId,
42− slug: `org-${input.organizationId}`,
43− name: `Org ${input.organizationId}`,
44− status: "active",
45− createdByAccountId: input.accountId,
46− createdAt: 1000,
47− updatedAt: 1000,
48− }));
49− await Effect.runPromise(input.persistence.rows.workspaces.insert({
50− id: input.workspaceId,
51− organizationId: input.organizationId,
52− name: `Workspace ${input.workspaceId}`,
53− createdByAccountId: input.accountId,
54− createdAt: 1000,
55− updatedAt: 1000,
56− }));
57−};
53+const makeRuntimeLocalWorkspaceState = (
54+ workspaceId: ReturnType<typeof WorkspaceIdSchema.make>,
55+ accountId: ReturnType<typeof AccountIdSchema.make>,
56+) =>
57+ Effect.runPromise(
58+ Effect.gen(function* () {
59+ const workspaceRoot = mkdtempSync(
60+ join(tmpdir(), "executor-source-inspection-"),
61+ );
62+ const context = yield* Effect.tryPromise({
63+ try: () => resolveLocalWorkspaceContext({ workspaceRoot }),
64+ catch: (cause) =>
65+ cause instanceof Error ? cause : new Error(String(cause)),
66+ });
67+ const loadedConfig = yield* Effect.tryPromise({
68+ try: () => loadLocalExecutorConfig(context),
69+ catch: (cause) =>
70+ cause instanceof Error ? cause : new Error(String(cause)),
71+ });
72+
73+ return {
74+ context,
75+ installation: {
76+ workspaceId,
77+ accountId,
78+ organizationId: "org_local_personal" as never,
79+ },
80+ loadedConfig,
81+ } satisfies RuntimeLocalWorkspaceState;
82+ }),
83+ );
5884
5985 const makeSource = (input: {
6086 workspaceId: Source["workspaceId"];
@@ -133,16 +159,39 @@ const makeOperation = ( · 72 unmodified lines
133159 ...overrides,
134160 });
135161
162+const configSourceFromSource = (source: Source): LocalConfigSource => ({
163+ kind: "openapi",
164+ ...(source.name !== source.id ? { name: source.name } : {}),
165+ ...(source.namespace && source.namespace !== source.id
166+ ? { namespace: source.namespace }
167+ : {}),
168+ connection: {
169+ endpoint: source.endpoint,
170+ },
171+ binding: {
172+ specUrl:
173+ typeof source.binding.specUrl === "string"
174+ ? source.binding.specUrl
175+ : "https://example.com/openapi.json",
176+ defaultHeaders:
177+ (source.binding.defaultHeaders as Record<string, string> | null | undefined)
178+ ?? null,
179+ },
180+});
181+
136182 describe("source-inspection", () => {
137183 it("returns a lightweight inspection bundle and loads rich detail on demand", async () => {
138184 const persistence = await makePersistence();
139185 try {
140186 const workspaceId = WorkspaceIdSchema.make("ws_source_inspection");
141− const organizationId = OrganizationIdSchema.make("org_source_inspection");
142187 const accountId = AccountIdSchema.make("acc_source_inspection");
143188 const sourceId = SourceIdSchema.make("src_source_inspection");
144− const recipeId = SourceRecipeIdSchema.make("src_recipe_inspection");
145− const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_inspection");
189+ const recipeRevisionId =
190+ SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization");
191+ const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
192+ workspaceId,
193+ accountId,
194+ );
146195 const hugeDocument = JSON.stringify({
147196 openapi: "3.0.3",
148197 info: {
@@ -162,103 +211,83 @@ describe("source-inspection", () => { · 13 unmodified lines
162211 ),
163212 });
164213
165− await seedWorkspace({
166− persistence,
214+ const source = makeSource({
167215 workspaceId,
168− organizationId,
169− accountId,
216+ sourceId,
170217 });
171− await Effect.runPromise(persistence.rows.sourceRecipes.upsert({
172− id: recipeId,
173− kind: "http_api",
174− adapterKey: "openapi",
175− providerKey: "generic_http",
176− name: "Cloudflare API",
177− summary: null,
178− visibility: "workspace",
179− latestRevisionId: recipeRevisionId,
180− createdAt: 1000,
181− updatedAt: 1000,
182− }));
183− await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({
184− id: recipeRevisionId,
185− recipeId,
186− revisionNumber: 1,
187− sourceConfigJson: JSON.stringify({
188− kind: "openapi",
189− endpoint: "https://api.cloudflare.com/client/v4",
190− specUrl: "https://example.com/openapi.json",
191− }),
192− manifestJson: JSON.stringify({
193− sourceHash: "manifest_hash",
218+ await writeProjectLocalExecutorConfig({
219+ context: runtimeLocalWorkspace.context,
220+ config: {
221+ sources: {
222+ [sourceId]: configSourceFromSource(source),
223+ },
224+ },
225+ });
226+ await writeLocalSourceArtifact({
227+ context: runtimeLocalWorkspace.context,
228+ sourceId,
229+ artifact: buildLocalSourceArtifact({
230+ source: {
231+ ...source,
232+ sourceHash: "manifest_hash",
233+ },
234+ materialization: {
235+ manifestJson: JSON.stringify({
236+ sourceHash: "manifest_hash",
237+ }),
238+ manifestHash: "manifest_hash",
239+ sourceHash: "manifest_hash",
240+ documents: [{
241+ id: "src_recipe_doc_inspection",
242+ recipeRevisionId,
243+ documentKind: "openapi",
244+ documentKey: "https://example.com/openapi.json",
245+ contentText: hugeDocument,
246+ contentHash: "doc_hash",
247+ fetchedAt: 1000,
248+ createdAt: 1000,
249+ updatedAt: 1000,
250+ }],
251+ schemaBundles: [{
252+ id: SourceRecipeSchemaBundleIdSchema.make(
253+ "src_recipe_bundle_inspection",
254+ ),
255+ recipeRevisionId,
256+ bundleKind: "json_schema_ref_map",
257+ refsJson: JSON.stringify({
258+ "#/components/schemas/Pagination": {
259+ type: "object",
260+ properties: {
261+ page: { type: "number" },
262+ },
263+ },
264+ }),
265+ contentHash: "bundle_hash",
266+ createdAt: 1000,
267+ updatedAt: 1000,
268+ }],
269+ operations: [makeOperation({
270+ recipeRevisionId,
271+ })],
272+ },
194273 }),
195− manifestHash: "manifest_hash",
196− materializationHash: "materialization_hash",
197− createdAt: 1000,
198− updatedAt: 1000,
199− }));
200− await Effect.runPromise(persistence.rows.sourceRecipeDocuments.replaceForRevision({
201− recipeRevisionId,
202− documents: [{
203− id: "src_recipe_doc_inspection",
204− recipeRevisionId,
205− documentKind: "openapi",
206− documentKey: "https://example.com/openapi.json",
207− contentText: hugeDocument,
208− contentHash: "doc_hash",
209− fetchedAt: 1000,
210− createdAt: 1000,
211− updatedAt: 1000,
212− }],
213− }));
214− await Effect.runPromise(persistence.rows.sourceRecipeSchemaBundles.replaceForRevision({
215− recipeRevisionId,
216− bundles: [{
217− id: SourceRecipeSchemaBundleIdSchema.make("src_recipe_bundle_inspection"),
218− recipeRevisionId,
219− bundleKind: "json_schema_ref_map",
220− refsJson: JSON.stringify({
221− "#/components/schemas/Pagination": {
222− type: "object",
223− properties: {
224− page: { type: "number" },
225− },
226− },
227− }),
228− contentHash: "bundle_hash",
229− createdAt: 1000,
230− updatedAt: 1000,
231− }],
232− }));
233− await Effect.runPromise(persistence.rows.sourceRecipeOperations.replaceForRevision({
234− recipeRevisionId,
235− operations: [makeOperation()],
236− }));
237− await Effect.runPromise(persistence.rows.sources.insert({
238− id: sourceId,
239− workspaceId,
240− recipeId,
241− recipeRevisionId,
242− name: "Cloudflare API",
243− kind: "openapi",
244− endpoint: "https://api.cloudflare.com/client/v4",
245− status: "connected",
246− enabled: true,
247− namespace: "cloudflare.api",
248− importAuthPolicy: "reuse_runtime",
249− bindingConfigJson: JSON.stringify({
250− adapterKey: "openapi",
274+ });
275+ await writeLocalWorkspaceState({
276+ context: runtimeLocalWorkspace.context,
277+ state: {
251278 version: 1,
252− payload: {
253− specUrl: "https://example.com/openapi.json",
254− defaultHeaders: null,
279+ sources: {
280+ [sourceId]: {
281+ status: "connected",
282+ lastError: null,
283+ sourceHash: "manifest_hash",
284+ createdAt: 1000,
285+ updatedAt: 1000,
286+ },
255287 },
256− }),
257− sourceHash: null,
258− lastError: null,
259− createdAt: 1000,
260− updatedAt: 1000,
261− }));
288+ policies: {},
289+ },
290+ });
262291
263292 const inspection = await Effect.runPromise(
264293 getSourceInspection({
@@ -266,6 +295,10 @@ describe("source-inspection", () => { · 1 unmodified line
266295 sourceId,
267296 }).pipe(
268297 Effect.provideService(ControlPlaneStore, persistence.rows),
298+ Effect.provideService(
299+ RuntimeLocalWorkspaceService,
300+ runtimeLocalWorkspace,
301+ ),
269302 ),
270303 );
271304
@@ -282,6 +315,10 @@ describe("source-inspection", () => { · 10 unmodified lines
282315 toolPath: "cloudflare.api.zones.listZones",
283316 }).pipe(
284317 Effect.provideService(ControlPlaneStore, persistence.rows),
318+ Effect.provideService(
319+ RuntimeLocalWorkspaceService,
320+ runtimeLocalWorkspace,
321+ ),
285322 ),
286323 );
287324
@@ -300,6 +337,10 @@ describe("source-inspection", () => { · 12 unmodified lines
300337 schemaBundleId: "src_recipe_bundle_inspection",
301338 }).pipe(
302339 Effect.provideService(ControlPlaneStore, persistence.rows),
340+ Effect.provideService(
341+ RuntimeLocalWorkspaceService,
342+ runtimeLocalWorkspace,
343+ ),
303344 ),
304345 );
305346
packages/control-plane/src/runtime/source-materialization.ts+67−105
@@ -11,7 +11,7 @@ import { · 10 unmodified lines
1111 writeLocalSourceArtifact,
1212 } from "./local-source-artifacts";
1313 import {
14− getRuntimeLocalWorkspaceOption,
14+ requireRuntimeLocalWorkspace,
1515 } from "./local-runtime-context";
1616 import {
1717 loadLocalWorkspaceState,
@@ -29,7 +29,6 @@ import { · 11 unmodified lines
2929 import type {
3030 ResolveSecretMaterial as ResolveSourceSecretMaterial,
3131 } from "./secret-material-providers";
32−import { persistRecipeMaterialization } from "./source-recipe-support";
3332
3433 const shouldIndexSource = (source: Source): boolean =>
3534 source.enabled
@@ -43,71 +42,9 @@ export const syncSourceMaterialization = (input: { · 7 unmodified lines
4342 resolveSecretMaterial: ResolveSourceSecretMaterial;
4443 }): Effect.Effect<void, Error, never> =>
4544 Effect.gen(function* () {
46− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
47− if (
48− runtimeLocalWorkspace !== null
49− && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId
50− ) {
51− if (!shouldIndexSource(input.source)) {
52− const state = yield* Effect.tryPromise({
53− try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context),
54− catch: (cause) =>
55− cause instanceof Error ? cause : new Error(String(cause)),
56− });
57− const existingSourceState = state.sources[input.source.id];
58− const nextState: LocalWorkspaceState = {
59− ...state,
60− sources: {
61− ...state.sources,
62− [input.source.id]: {
63− status: (input.source.enabled ? input.source.status : "draft") as SourceStatus,
64− lastError: null,
65− sourceHash: input.source.sourceHash,
66− createdAt: existingSourceState?.createdAt ?? input.source.createdAt,
67− updatedAt: Date.now(),
68− },
69− },
70− };
71− yield* Effect.tryPromise({
72− try: () =>
73− writeLocalWorkspaceState({
74− context: runtimeLocalWorkspace.context,
75− state: nextState,
76− }),
77− catch: (cause) =>
78− cause instanceof Error ? cause : new Error(String(cause)),
79− });
80− return;
81− }
82−
83− const adapter = getSourceAdapterForSource(input.source);
84− const materialization = yield* adapter.materializeSource({
85− source: input.source,
86− resolveSecretMaterial: input.resolveSecretMaterial,
87− resolveAuthMaterialForSlot: (slot) =>
88− resolveSourceAuthMaterial({
89− rows: input.rows,
90− source: input.source,
91− slot,
92− actorAccountId: input.actorAccountId,
93− resolveSecretMaterial: input.resolveSecretMaterial,
94− }),
95− });
96−
97− yield* Effect.tryPromise({
98− try: () =>
99− writeLocalSourceArtifact({
100− context: runtimeLocalWorkspace.context,
101− sourceId: input.source.id,
102− artifact: buildLocalSourceArtifact({
103− source: input.source,
104− materialization,
105− }),
106− }),
107− catch: (cause) =>
108− cause instanceof Error ? cause : new Error(String(cause)),
109− });
45+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
11046
47+ if (!shouldIndexSource(input.source)) {
11148 const state = yield* Effect.tryPromise({
11249 try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context),
11350 catch: (cause) =>
@@ -119,9 +56,9 @@ export const syncSourceMaterialization = (input: { · 5 unmodified lines
11956 sources: {
12057 ...state.sources,
12158 [input.source.id]: {
122− status: "connected",
59+ status: (input.source.enabled ? input.source.status : "draft") as SourceStatus,
12360 lastError: null,
124− sourceHash: materialization.sourceHash,
61+ sourceHash: input.source.sourceHash,
12562 createdAt: existingSourceState?.createdAt ?? input.source.createdAt,
12663 updatedAt: Date.now(),
12764 },
@@ -130,19 +67,15 @@ export const syncSourceMaterialization = (input: { · 2 unmodified lines
13067 yield* Effect.tryPromise({
13168 try: () =>
13269 writeLocalWorkspaceState({
133− context: runtimeLocalWorkspace.context,
134− state: nextState,
135− }),
70+ context: runtimeLocalWorkspace.context,
71+ state: nextState,
72+ }),
13673 catch: (cause) =>
13774 cause instanceof Error ? cause : new Error(String(cause)),
13875 });
13976 return;
14077 }
14178
142− if (!shouldIndexSource(input.source)) {
143− return;
144− }
145−
14679 const adapter = getSourceAdapterForSource(input.source);
14780 const materialization = yield* adapter.materializeSource({
14881 source: input.source,
@@ -156,10 +89,47 @@ export const syncSourceMaterialization = (input: { · 7 unmodified lines
15689 resolveSecretMaterial: input.resolveSecretMaterial,
15790 }),
15891 });
159− yield* persistRecipeMaterialization({
160− rows: input.rows,
161− source: input.source,
162− materialization,
92+ yield* Effect.tryPromise({
93+ try: () =>
94+ writeLocalSourceArtifact({
95+ context: runtimeLocalWorkspace.context,
96+ sourceId: input.source.id,
97+ artifact: buildLocalSourceArtifact({
98+ source: input.source,
99+ materialization,
100+ }),
101+ }),
102+ catch: (cause) =>
103+ cause instanceof Error ? cause : new Error(String(cause)),
104+ });
105+
106+ const state = yield* Effect.tryPromise({
107+ try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context),
108+ catch: (cause) =>
109+ cause instanceof Error ? cause : new Error(String(cause)),
110+ });
111+ const existingSourceState = state.sources[input.source.id];
112+ const nextState: LocalWorkspaceState = {
113+ ...state,
114+ sources: {
115+ ...state.sources,
116+ [input.source.id]: {
117+ status: "connected",
118+ lastError: null,
119+ sourceHash: materialization.sourceHash,
120+ createdAt: existingSourceState?.createdAt ?? input.source.createdAt,
121+ updatedAt: Date.now(),
122+ },
123+ },
124+ };
125+ yield* Effect.tryPromise({
126+ try: () =>
127+ writeLocalWorkspaceState({
128+ context: runtimeLocalWorkspace.context,
129+ state: nextState,
130+ }),
131+ catch: (cause) =>
132+ cause instanceof Error ? cause : new Error(String(cause)),
163133 });
164134 });
165135
@@ -171,32 +141,24 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines
171141 >[0]["manifestEntries"];
172142 }): Effect.Effect<void, Error, never> =>
173143 Effect.gen(function* () {
174− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
175− if (
176− runtimeLocalWorkspace !== null
177− && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId
178− ) {
179− const materialization = materializationFromMcpManifestEntries({
180− recipeRevisionId: "src_recipe_rev_materialization" as never,
181− endpoint: input.source.endpoint,
182− manifestEntries: input.manifestEntries,
183− });
144+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
145+ const materialization = materializationFromMcpManifestEntries({
146+ recipeRevisionId: "src_recipe_rev_materialization" as never,
147+ endpoint: input.source.endpoint,
148+ manifestEntries: input.manifestEntries,
149+ });
184150
185− yield* Effect.tryPromise({
186− try: () =>
187− writeLocalSourceArtifact({
188− context: runtimeLocalWorkspace.context,
189− sourceId: input.source.id,
190− artifact: buildLocalSourceArtifact({
191− source: input.source,
192− materialization,
193− }),
151+ yield* Effect.tryPromise({
152+ try: () =>
153+ writeLocalSourceArtifact({
154+ context: runtimeLocalWorkspace.context,
155+ sourceId: input.source.id,
156+ artifact: buildLocalSourceArtifact({
157+ source: input.source,
158+ materialization,
194159 }),
195− catch: (cause) =>
196− cause instanceof Error ? cause : new Error(String(cause)),
197− });
198− return;
199− }
200−
201− return yield* persistMcpRecipeRevisionFromManifestEntries(input);
160+ }),
161+ catch: (cause) =>
162+ cause instanceof Error ? cause : new Error(String(cause)),
163+ });
202164 });
packages/control-plane/src/runtime/source-recipes-runtime.test.ts+371−245
@@ -1,3 +1,7 @@
1+import { mkdtempSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { join } from "node:path";
4+
15 import { describe, expect, it } from "vitest";
26 import * as Effect from "effect/Effect";
37 import {
@@ -8,8 +12,8 @@ import { · 4 unmodified lines
812
913 import {
1014 AccountIdSchema,
11− OrganizationIdSchema,
1215 SourceIdSchema,
16+ type LocalConfigSource,
1317 SourceRecipeIdSchema,
1418 SourceRecipeRevisionIdSchema,
1519 WorkspaceIdSchema,
@@ -21,6 +25,22 @@ import { · 5 unmodified lines
2125 type SqlControlPlanePersistence,
2226 } from "#persistence";
2327
28+import {
29+ loadLocalExecutorConfig,
30+ resolveLocalWorkspaceContext,
31+ writeProjectLocalExecutorConfig,
32+} from "./local-config";
33+import {
34+ buildLocalSourceArtifact,
35+ writeLocalSourceArtifact,
36+} from "./local-source-artifacts";
37+import {
38+ RuntimeLocalWorkspaceService,
39+ type RuntimeLocalWorkspaceState,
40+} from "./local-runtime-context";
41+import {
42+ writeLocalWorkspaceState,
43+} from "./local-workspace-state";
2444 import {
2545 expandRecipeTools,
2646 loadSourceWithRecipe,
@@ -38,6 +58,45 @@ const makePersistence = () => · 11 unmodified lines
3858 }),
3959 );
4060
61+const makeRuntimeLocalWorkspaceState = (
62+ workspaceId: ReturnType<typeof WorkspaceIdSchema.make>,
63+) =>
64+ Effect.runPromise(
65+ Effect.gen(function* () {
66+ const workspaceRoot = mkdtempSync(
67+ join(tmpdir(), "executor-source-recipes-runtime-"),
68+ );
69+ const context = yield* Effect.tryPromise({
70+ try: () => resolveLocalWorkspaceContext({ workspaceRoot }),
71+ catch: (cause) =>
72+ cause instanceof Error ? cause : new Error(String(cause)),
73+ });
74+ const loadedConfig = yield* Effect.tryPromise({
75+ try: () => loadLocalExecutorConfig(context),
76+ catch: (cause) =>
77+ cause instanceof Error ? cause : new Error(String(cause)),
78+ });
79+
80+ return {
81+ context,
82+ installation: {
83+ workspaceId,
84+ accountId: AccountIdSchema.make("acc_local_source_recipes"),
85+ organizationId: "org_local_personal" as never,
86+ },
87+ loadedConfig,
88+ } satisfies RuntimeLocalWorkspaceState;
89+ }),
90+ );
91+
92+const withRuntimeLocalWorkspace = <A, E>(
93+ effect: Effect.Effect<A, E, never>,
94+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
95+) =>
96+ effect.pipe(
97+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
98+ );
99+
41100 const openApiBindingConfigJson = (specUrl: string): string =>
42101 JSON.stringify({
43102 adapterKey: "openapi",
@@ -221,30 +280,128 @@ const makeMcpOperation = ( · 177 unmodified lines
221280 ...overrides,
222281 });
223282
224−const seedWorkspace = async (input: {
225− persistence: SqlControlPlanePersistence;
226− workspaceId: ReturnType<typeof WorkspaceIdSchema.make>;
227− organizationId: ReturnType<typeof OrganizationIdSchema.make>;
228− accountId: ReturnType<typeof AccountIdSchema.make>;
283+const configSourceFromSource = (source: Source): LocalConfigSource => {
284+ switch (source.kind) {
285+ case "openapi":
286+ return {
287+ kind: "openapi" as const,
288+ ...(source.name !== source.id ? { name: source.name } : {}),
289+ ...(source.namespace && source.namespace !== source.id
290+ ? { namespace: source.namespace }
291+ : {}),
292+ ...(source.enabled ? {} : { enabled: false }),
293+ connection: {
294+ endpoint: source.endpoint,
295+ },
296+ binding: {
297+ specUrl:
298+ typeof source.binding.specUrl === "string"
299+ ? source.binding.specUrl
300+ : "https://example.com/openapi.json",
301+ defaultHeaders:
302+ (source.binding.defaultHeaders as Record<string, string> | null | undefined)
303+ ?? null,
304+ },
305+ };
306+ case "graphql":
307+ return {
308+ kind: "graphql" as const,
309+ ...(source.name !== source.id ? { name: source.name } : {}),
310+ ...(source.namespace && source.namespace !== source.id
311+ ? { namespace: source.namespace }
312+ : {}),
313+ ...(source.enabled ? {} : { enabled: false }),
314+ connection: {
315+ endpoint: source.endpoint,
316+ },
317+ binding: {
318+ defaultHeaders:
319+ (source.binding.defaultHeaders as Record<string, string> | null | undefined)
320+ ?? null,
321+ },
322+ };
323+ case "mcp":
324+ return {
325+ kind: "mcp" as const,
326+ ...(source.name !== source.id ? { name: source.name } : {}),
327+ ...(source.namespace && source.namespace !== source.id
328+ ? { namespace: source.namespace }
329+ : {}),
330+ ...(source.enabled ? {} : { enabled: false }),
331+ connection: {
332+ endpoint: source.endpoint,
333+ },
334+ binding: {
335+ transport:
336+ (source.binding.transport as "auto" | "streamable-http" | "sse" | null | undefined)
337+ ?? null,
338+ queryParams:
339+ (source.binding.queryParams as Record<string, string> | null | undefined)
340+ ?? null,
341+ headers:
342+ (source.binding.headers as Record<string, string> | null | undefined)
343+ ?? null,
344+ },
345+ };
346+ default:
347+ throw new Error(`Unsupported source kind in test fixture: ${source.kind}`);
348+ }
349+};
350+
351+const seedLocalWorkspaceSources = async (input: {
352+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
353+ fixtures: ReadonlyArray<{
354+ source: Source;
355+ materialization: {
356+ manifestJson: string | null;
357+ manifestHash: string | null;
358+ sourceHash: string | null;
359+ documents: ReadonlyArray<any>;
360+ schemaBundles: ReadonlyArray<any>;
361+ operations: ReadonlyArray<StoredSourceRecipeOperationRecord>;
362+ };
363+ }>;
229364 }) => {
230− const now = 1000;
231− await Effect.runPromise(input.persistence.rows.organizations.insert({
232− id: input.organizationId,
233− slug: `org-${input.organizationId}`,
234− name: `Org ${input.organizationId}`,
235− status: "active",
236− createdByAccountId: input.accountId,
237− createdAt: now,
238− updatedAt: now,
239− }));
240− await Effect.runPromise(input.persistence.rows.workspaces.insert({
241− id: input.workspaceId,
242− organizationId: input.organizationId,
243− name: `Workspace ${input.workspaceId}`,
244− createdByAccountId: input.accountId,
245− createdAt: now,
246− updatedAt: now,
247− }));
365+ await writeProjectLocalExecutorConfig({
366+ context: input.runtimeLocalWorkspace.context,
367+ config: {
368+ sources: Object.fromEntries(
369+ input.fixtures.map(({ source }) => [source.id, configSourceFromSource(source)]),
370+ ),
371+ },
372+ });
373+
374+ await Promise.all(
375+ input.fixtures.map(({ source, materialization }) =>
376+ writeLocalSourceArtifact({
377+ context: input.runtimeLocalWorkspace.context,
378+ sourceId: source.id,
379+ artifact: buildLocalSourceArtifact({
380+ source,
381+ materialization,
382+ }),
383+ })),
384+ );
385+
386+ await writeLocalWorkspaceState({
387+ context: input.runtimeLocalWorkspace.context,
388+ state: {
389+ version: 1,
390+ sources: Object.fromEntries(
391+ input.fixtures.map(({ source }) => [
392+ source.id,
393+ {
394+ status: source.status,
395+ lastError: source.lastError,
396+ sourceHash: source.sourceHash,
397+ createdAt: source.createdAt,
398+ updatedAt: source.updatedAt,
399+ },
400+ ]),
401+ ),
402+ policies: {},
403+ },
404+ });
248405 };
249406
250407 describe("source-recipes-runtime", () => {
@@ -432,10 +589,8 @@ describe("source-recipes-runtime", () => { · 181 unmodified lines
432589 const persistence = await makePersistence();
433590 try {
434591 const workspaceId = WorkspaceIdSchema.make("ws_shared_revision");
435− const organizationId = OrganizationIdSchema.make("org_shared_revision");
436− const accountId = AccountIdSchema.make("acc_shared_revision");
437− const recipeId = SourceRecipeIdSchema.make("src_recipe_shared_revision");
438− const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_shared_revision");
592+ const runtimeLocalWorkspace =
593+ await makeRuntimeLocalWorkspaceState(workspaceId);
439594 const openApiDocument = JSON.stringify({
440595 openapi: "3.0.3",
441596 info: {
@@ -475,101 +630,68 @@ describe("source-recipes-runtime", () => { · 33 unmodified lines
475630 definition,
476631 });
477632
478− await seedWorkspace({
479− persistence,
480− workspaceId,
481− organizationId,
482− accountId,
633+ await seedLocalWorkspaceSources({
634+ runtimeLocalWorkspace,
635+ fixtures: ["GitHub One", "GitHub Two"].map((name, index) => ({
636+ source: makeSource({
637+ id: SourceIdSchema.make(`src_shared_revision_${index}`),
638+ workspaceId,
639+ name,
640+ sourceHash: manifest.sourceHash,
641+ createdAt: 1000 + index,
642+ updatedAt: 1000 + index,
643+ }),
644+ materialization: {
645+ manifestJson: JSON.stringify(manifest),
646+ manifestHash: manifest.sourceHash,
647+ sourceHash: manifest.sourceHash,
648+ documents: [{
649+ id: `src_recipe_doc_shared_revision_${index}`,
650+ recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),
651+ documentKind: "openapi",
652+ documentKey: "https://api.github.com/openapi.json",
653+ contentText: openApiDocument,
654+ contentHash: manifest.sourceHash,
655+ fetchedAt: 1000,
656+ createdAt: 1000,
657+ updatedAt: 1000,
658+ }],
659+ schemaBundles: [],
660+ operations: [makeOperation({
661+ id: `src_recipe_op_shared_revision_${index}`,
662+ recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),
663+ operationKey: definition.toolId,
664+ toolId: definition.toolId,
665+ title: definition.name,
666+ description: definition.description,
667+ searchText: `${definition.toolId} ${definition.name}`.toLowerCase(),
668+ inputSchemaJson: presentation.inputSchemaJson ?? null,
669+ outputSchemaJson: presentation.outputSchemaJson ?? null,
670+ providerDataJson: presentation.providerDataJson,
671+ })],
672+ },
673+ })),
483674 });
484− await Effect.runPromise(persistence.rows.sourceRecipes.upsert({
485− id: recipeId,
486− kind: "http_api",
487− adapterKey: "openapi",
488− providerKey: "generic_http",
489− name: "GitHub",
490− summary: null,
491− visibility: "workspace",
492− latestRevisionId: recipeRevisionId,
493− createdAt: 1000,
494− updatedAt: 1000,
495− }));
496− await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({
497− id: recipeRevisionId,
498− recipeId,
499− revisionNumber: 1,
500− sourceConfigJson: JSON.stringify({
501− kind: "openapi",
502− endpoint: "https://api.github.com",
503− specUrl: "https://api.github.com/openapi.json",
504− defaultHeaders: null,
505− }),
506− manifestJson: JSON.stringify(manifest),
507− manifestHash: manifest.sourceHash,
508− materializationHash: manifest.sourceHash,
509− createdAt: 1000,
510− updatedAt: 1000,
511− }));
512− await Effect.runPromise(persistence.rows.sourceRecipeDocuments.replaceForRevision({
513− recipeRevisionId,
514− documents: [{
515− id: "src_recipe_doc_shared_revision",
516− recipeRevisionId,
517− documentKind: "openapi",
518− documentKey: "https://api.github.com/openapi.json",
519− contentText: openApiDocument,
520− contentHash: manifest.sourceHash,
521− fetchedAt: 1000,
522− createdAt: 1000,
523− updatedAt: 1000,
524− }],
525− }));
526− await Effect.runPromise(persistence.rows.sourceRecipeOperations.replaceForRevision({
527− recipeRevisionId,
528− operations: [makeOperation({
529− id: "src_recipe_op_shared_revision",
530− recipeRevisionId,
531− operationKey: definition.toolId,
532− toolId: definition.toolId,
533− title: definition.name,
534− description: definition.description,
535− searchText: `${definition.toolId} ${definition.name}`.toLowerCase(),
536− inputSchemaJson: presentation.inputSchemaJson ?? null,
537− outputSchemaJson: presentation.outputSchemaJson ?? null,
538− providerDataJson: presentation.providerDataJson,
539− })],
540− }));
541−
542− for (const [index, name] of ["GitHub One", "GitHub Two"].entries()) {
543− await Effect.runPromise(persistence.rows.sources.insert({
544− id: SourceIdSchema.make(`src_shared_revision_${index}`),
545− workspaceId,
546− recipeId,
547− recipeRevisionId,
548− name,
549− kind: "openapi",
550− endpoint: "https://api.github.com",
551− status: "connected",
552− enabled: true,
553− namespace: "github",
554− importAuthPolicy: "reuse_runtime",
555− bindingConfigJson: openApiBindingConfigJson("https://api.github.com/openapi.json"),
556− sourceHash: manifest.sourceHash,
557− lastError: null,
558− createdAt: 1000 + index,
559− updatedAt: 1000 + index,
560− }));
561− }
562675
563− const recipes = await Effect.runPromise(loadWorkspaceSourceRecipes({
564− rows: persistence.rows,
565− workspaceId,
566− }));
676+ const recipes = await Effect.runPromise(
677+ withRuntimeLocalWorkspace(
678+ loadWorkspaceSourceRecipes({
679+ rows: persistence.rows,
680+ workspaceId,
681+ }),
682+ runtimeLocalWorkspace,
683+ ),
684+ );
567685
568686 expect(recipes).toHaveLength(2);
569687 expect(recipes[0]?.documents).toHaveLength(1);
570688 expect(recipes[0]?.operations).toHaveLength(1);
571− expect(recipes[0]?.documents).toBe(recipes[1]?.documents);
572− expect(recipes[0]?.operations).toBe(recipes[1]?.operations);
689+ expect(recipes[0]?.documents[0]?.contentText).toBe(
690+ recipes[1]?.documents[0]?.contentText,
691+ );
692+ expect(recipes[0]?.operations[0]?.providerDataJson).toBe(
693+ recipes[1]?.operations[0]?.providerDataJson,
694+ );
573695 } finally {
574696 await persistence.close();
575697 }
@@ -579,68 +701,44 @@ describe("source-recipes-runtime", () => { · 3 unmodified lines
579701 const persistence = await makePersistence();
580702 try {
581703 const workspaceId = WorkspaceIdSchema.make("ws_empty_recipe_rows");
582− const organizationId = OrganizationIdSchema.make("org_empty_recipe_rows");
583− const accountId = AccountIdSchema.make("acc_empty_recipe_rows");
584704 const sourceId = SourceIdSchema.make("src_empty_recipe_rows");
585− const recipeId = SourceRecipeIdSchema.make("src_recipe_empty_recipe_rows");
586− const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_empty_recipe_rows");
587−
588− await seedWorkspace({
589− persistence,
590− workspaceId,
591− organizationId,
592− accountId,
705+ const runtimeLocalWorkspace =
706+ await makeRuntimeLocalWorkspaceState(workspaceId);
707+
708+ await seedLocalWorkspaceSources({
709+ runtimeLocalWorkspace,
710+ fixtures: [{
711+ source: makeSource({
712+ id: sourceId,
713+ workspaceId,
714+ name: "GraphQL Demo",
715+ kind: "graphql",
716+ endpoint: "https://example.com/graphql",
717+ namespace: "graphql",
718+ binding: {
719+ defaultHeaders: null,
720+ },
721+ }),
722+ materialization: {
723+ manifestJson: null,
724+ manifestHash: null,
725+ sourceHash: null,
726+ documents: [],
727+ schemaBundles: [],
728+ operations: [],
729+ },
730+ }],
593731 });
594− await Effect.runPromise(persistence.rows.sourceRecipes.upsert({
595− id: recipeId,
596− kind: "http_api",
597− adapterKey: "graphql",
598− providerKey: "generic_graphql",
599− name: "GraphQL Demo",
600− summary: null,
601− visibility: "workspace",
602− latestRevisionId: recipeRevisionId,
603− createdAt: 1000,
604− updatedAt: 1000,
605− }));
606− await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({
607− id: recipeRevisionId,
608− recipeId,
609− revisionNumber: 1,
610− sourceConfigJson: JSON.stringify({
611− kind: "graphql",
612− endpoint: "https://example.com/graphql",
613− defaultHeaders: null,
614− }),
615− manifestJson: null,
616− manifestHash: null,
617− materializationHash: null,
618− createdAt: 1000,
619− updatedAt: 1000,
620− }));
621− await Effect.runPromise(persistence.rows.sources.insert({
622− id: sourceId,
623− workspaceId,
624− recipeId,
625− recipeRevisionId,
626− name: "GraphQL Demo",
627− kind: "graphql",
628− endpoint: "https://example.com/graphql",
629− status: "connected",
630− enabled: true,
631− namespace: "graphql",
632− importAuthPolicy: "reuse_runtime",
633− bindingConfigJson: graphqlBindingConfigJson(),
634− sourceHash: null,
635− lastError: null,
636− createdAt: 1000,
637− updatedAt: 1000,
638− }));
639732
640− const recipes = await Effect.runPromise(loadWorkspaceSourceRecipes({
641− rows: persistence.rows,
642− workspaceId,
643− }));
733+ const recipes = await Effect.runPromise(
734+ withRuntimeLocalWorkspace(
735+ loadWorkspaceSourceRecipes({
736+ rows: persistence.rows,
737+ workspaceId,
738+ }),
739+ runtimeLocalWorkspace,
740+ ),
741+ );
644742
645743 expect(recipes).toHaveLength(1);
646744 expect(recipes[0]?.documents).toEqual([]);
@@ -654,84 +752,112 @@ describe("source-recipes-runtime", () => { · 7 unmodified lines
654752 it("fails clearly when loading a missing source, missing revision, or invalid manifest", async () => {
655753 const persistence = await makePersistence();
656754 try {
657− await expect(Effect.runPromise(loadSourceWithRecipe({
658− rows: persistence.rows,
659− workspaceId: WorkspaceIdSchema.make("ws_missing_source"),
660− sourceId: SourceIdSchema.make("src_missing_source"),
661− }))).rejects.toThrow("Source not found");
755+ const missingWorkspaceId = WorkspaceIdSchema.make("ws_missing_source");
756+ const missingRuntimeLocalWorkspace =
757+ await makeRuntimeLocalWorkspaceState(missingWorkspaceId);
758+
759+ await expect(
760+ Effect.runPromise(
761+ withRuntimeLocalWorkspace(
762+ loadSourceWithRecipe({
763+ rows: persistence.rows,
764+ workspaceId: missingWorkspaceId,
765+ sourceId: SourceIdSchema.make("src_missing_source"),
766+ }),
767+ missingRuntimeLocalWorkspace,
768+ ),
769+ ),
770+ ).rejects.toThrow("Source not found");
662771
663772 const workspaceId = WorkspaceIdSchema.make("ws_bad_recipe_runtime");
664− const organizationId = OrganizationIdSchema.make("org_bad_recipe_runtime");
665− const accountId = AccountIdSchema.make("acc_bad_recipe_runtime");
666773 const sourceId = SourceIdSchema.make("src_bad_recipe_runtime");
667− const recipeId = SourceRecipeIdSchema.make("src_recipe_bad_recipe_runtime");
668− const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_bad_recipe_runtime");
669−
670− await seedWorkspace({
671− persistence,
672− workspaceId,
673− organizationId,
674− accountId,
774+ const runtimeLocalWorkspace =
775+ await makeRuntimeLocalWorkspaceState(workspaceId);
776+
777+ await writeProjectLocalExecutorConfig({
778+ context: runtimeLocalWorkspace.context,
779+ config: {
780+ sources: {
781+ [sourceId]: configSourceFromSource(
782+ makeSource({
783+ id: sourceId,
784+ workspaceId,
785+ name: "Broken GitHub",
786+ }),
787+ ),
788+ },
789+ },
675790 });
676− await Effect.runPromise(persistence.rows.sourceRecipes.upsert({
677− id: recipeId,
678− kind: "http_api",
679− adapterKey: "openapi",
680− providerKey: "generic_http",
681− name: "Broken GitHub",
682− summary: null,
683− visibility: "workspace",
684− latestRevisionId: recipeRevisionId,
685− createdAt: 1000,
686− updatedAt: 1000,
687− }));
688− await Effect.runPromise(persistence.rows.sources.insert({
791+ await writeLocalWorkspaceState({
792+ context: runtimeLocalWorkspace.context,
793+ state: {
794+ version: 1,
795+ sources: {
796+ [sourceId]: {
797+ status: "connected",
798+ lastError: null,
799+ sourceHash: null,
800+ createdAt: 1000,
801+ updatedAt: 1000,
802+ },
803+ },
804+ policies: {},
805+ },
806+ });
807+
808+ await expect(
809+ Effect.runPromise(
810+ withRuntimeLocalWorkspace(
811+ loadSourceWithRecipe({
812+ rows: persistence.rows,
813+ workspaceId,
814+ sourceId,
815+ }),
816+ runtimeLocalWorkspace,
817+ ),
818+ ),
819+ ).rejects.toThrow("Recipe artifact missing");
820+
821+ const invalidManifestSource = makeSource({
689822 id: sourceId,
690823 workspaceId,
691− recipeId,
692− recipeRevisionId,
693824 name: "Broken GitHub",
694− kind: "openapi",
695− endpoint: "https://api.github.com",
696− status: "connected",
697− enabled: true,
698− namespace: "github",
699− importAuthPolicy: "reuse_runtime",
700− bindingConfigJson: openApiBindingConfigJson("https://api.github.com/openapi.json"),
701− sourceHash: null,
702− lastError: null,
703− createdAt: 1000,
704− updatedAt: 1000,
705− }));
706−
707− await expect(Effect.runPromise(loadSourceWithRecipe({
708− rows: persistence.rows,
709− workspaceId,
825+ });
826+ const invalidArtifact = buildLocalSourceArtifact({
827+ source: invalidManifestSource,
828+ materialization: {
829+ manifestJson: "{}",
830+ manifestHash: "manifest_hash_invalid",
831+ sourceHash: "manifest_hash_invalid",
832+ documents: [],
833+ schemaBundles: [],
834+ operations: [],
835+ },
836+ });
837+ await writeLocalSourceArtifact({
838+ context: runtimeLocalWorkspace.context,
710839 sourceId,
711− }))).rejects.toThrow("Recipe revision missing");
712−
713− await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({
714− id: recipeRevisionId,
715− recipeId,
716− revisionNumber: 1,
717− sourceConfigJson: JSON.stringify({
718− kind: "openapi",
719− endpoint: "https://api.github.com",
720− specUrl: "https://api.github.com/openapi.json",
721− defaultHeaders: null,
722− }),
723− manifestJson: "{bad-json",
724− manifestHash: null,
725− materializationHash: null,
726− createdAt: 1000,
727− updatedAt: 1000,
728− }));
840+ artifact: {
841+ ...invalidArtifact,
842+ revision: {
843+ ...invalidArtifact.revision,
844+ manifestJson: "{bad-json",
845+ },
846+ },
847+ });
729848
730− await expect(Effect.runPromise(loadSourceWithRecipe({
731− rows: persistence.rows,
732− workspaceId,
733− sourceId,
734− }))).rejects.toThrow(`Invalid OpenAPI manifest for ${sourceId}`);
849+ await expect(
850+ Effect.runPromise(
851+ withRuntimeLocalWorkspace(
852+ loadSourceWithRecipe({
853+ rows: persistence.rows,
854+ workspaceId,
855+ sourceId,
856+ }),
857+ runtimeLocalWorkspace,
858+ ),
859+ ),
860+ ).rejects.toThrow(`Invalid OpenAPI manifest for ${sourceId}`);
735861 } finally {
736862 await persistence.close();
737863 }
packages/control-plane/src/runtime/source-recipes-runtime.ts+78−198
@@ -13,13 +13,12 @@ import type { · 12 unmodified lines
1313 WorkspaceId,
1414 } from "#schema";
1515 import * as Effect from "effect/Effect";
16−import * as Option from "effect/Option";
1716
1817 import {
1918 readLocalSourceArtifact,
2019 } from "./local-source-artifacts";
2120 import {
22− getRuntimeLocalWorkspaceOption,
21+ requireRuntimeLocalWorkspace,
2322 } from "./local-runtime-context";
2423 import { namespaceFromSourceName } from "./source-names";
2524 import {
@@ -162,133 +161,61 @@ export const loadWorkspaceSourceRecipes = (input: { · 136 unmodified lines
162161 actorAccountId?: AccountId | null;
163162 }): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> =>
164163 Effect.gen(function* () {
165− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
166− if (
167− runtimeLocalWorkspace !== null
168− && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId
169− ) {
170− const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, {
171− actorAccountId: input.actorAccountId,
172− });
173−
174− const localRecipes = yield* Effect.forEach(sources, (source) =>
175− Effect.gen(function* () {
176− const artifact = yield* Effect.tryPromise({
177− try: () =>
178− readLocalSourceArtifact({
179− context: runtimeLocalWorkspace.context,
180− sourceId: source.id,
181− }),
182− catch: (cause) =>
183− cause instanceof Error ? cause : new Error(String(cause)),
184− });
185− if (artifact === null) {
186− return null;
187− }
188−
189− const sourceRecord: StoredSourceRecord = {
190− id: source.id,
191− workspaceId: source.workspaceId,
192− recipeId: artifact.recipeId,
193− recipeRevisionId: artifact.revision.id,
194− name: source.name,
195− kind: source.kind,
196− endpoint: source.endpoint,
197− status: source.status,
198− enabled: source.enabled,
199− namespace: source.namespace,
200− importAuthPolicy: source.importAuthPolicy,
201− bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
202− sourceHash: source.sourceHash,
203− lastError: source.lastError,
204− createdAt: source.createdAt,
205− updatedAt: source.updatedAt,
206− };
207− const manifest = yield* parseManifestForRecipe({
208− source,
209− revision: artifact.revision,
210− });
211−
212− return {
213− source,
214− sourceRecord,
215− revision: artifact.revision,
216− documents: artifact.documents,
217− schemaBundles: artifact.schemaBundles,
218− operations: artifact.operations,
219− manifest,
220− } satisfies LoadedSourceRecipe;
221− }),
222− );
223−
224− return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null);
225− }
226−
227− const sourceRecords = yield* input.rows.sources.listByWorkspaceId(input.workspaceId);
164+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
228165 const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, {
229166 actorAccountId: input.actorAccountId,
230167 });
231−
232− const sourceById = new Map(sources.map((source) => [source.id, source]));
233− const relevantSourceRecords = sourceRecords.filter((sourceRecord) => sourceById.has(sourceRecord.id));
234− const revisionIds = [...new Set(relevantSourceRecords.map((sourceRecord) => sourceRecord.recipeRevisionId))];
235−
236− const revisions = yield* input.rows.sourceRecipeRevisions.listByIds(revisionIds);
237− const revisionById = new Map(revisions.map((revision) => [revision.id, revision]));
238− const documents = yield* input.rows.sourceRecipeDocuments.listByRevisionIds(revisionIds);
239− const schemaBundles = yield* input.rows.sourceRecipeSchemaBundles.listByRevisionIds(revisionIds);
240− const documentsByRevisionId = new Map<string, StoredSourceRecipeDocumentRecord[]>();
241− for (const document of documents) {
242− const existing = documentsByRevisionId.get(document.recipeRevisionId) ?? [];
243− existing.push(document);
244− documentsByRevisionId.set(document.recipeRevisionId, existing);
245− }
246− const schemaBundlesByRevisionId = new Map<string, StoredSourceRecipeSchemaBundleRecord[]>();
247− for (const schemaBundle of schemaBundles) {
248− const existing = schemaBundlesByRevisionId.get(schemaBundle.recipeRevisionId) ?? [];
249− existing.push(schemaBundle);
250− schemaBundlesByRevisionId.set(schemaBundle.recipeRevisionId, existing);
251− }
252− const operations = yield* input.rows.sourceRecipeOperations.listByRevisionIds(revisionIds);
253− const operationsByRevisionId = new Map<string, StoredSourceRecipeOperationRecord[]>();
254− for (const operation of operations) {
255− const existing = operationsByRevisionId.get(operation.recipeRevisionId) ?? [];
256− existing.push(operation);
257− operationsByRevisionId.set(operation.recipeRevisionId, existing);
258− }
259−
260− return yield* Effect.forEach(relevantSourceRecords, (sourceRecord) =>
168+ const localRecipes = yield* Effect.forEach(sources, (source) =>
261169 Effect.gen(function* () {
262− const source = sourceById.get(sourceRecord.id);
263− if (!source) {
264− return yield* Effect.fail(
265− new Error(`Projected source missing for ${sourceRecord.id}`),
266− );
170+ const artifact = yield* Effect.tryPromise({
171+ try: () =>
172+ readLocalSourceArtifact({
173+ context: runtimeLocalWorkspace.context,
174+ sourceId: source.id,
175+ }),
176+ catch: (cause) =>
177+ cause instanceof Error ? cause : new Error(String(cause)),
178+ });
179+ if (artifact === null) {
180+ return null;
267181 }
268182
269− const revision = revisionById.get(sourceRecord.recipeRevisionId);
270− if (!revision) {
271− return yield* Effect.fail(
272− new Error(`Recipe revision missing for source ${sourceRecord.id}`),
273− );
274− }
183+ const sourceRecord: StoredSourceRecord = {
184+ id: source.id,
185+ workspaceId: source.workspaceId,
186+ recipeId: artifact.recipeId,
187+ recipeRevisionId: artifact.revision.id,
188+ name: source.name,
189+ kind: source.kind,
190+ endpoint: source.endpoint,
191+ status: source.status,
192+ enabled: source.enabled,
193+ namespace: source.namespace,
194+ importAuthPolicy: source.importAuthPolicy,
195+ bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
196+ sourceHash: source.sourceHash,
197+ lastError: source.lastError,
198+ createdAt: source.createdAt,
199+ updatedAt: source.updatedAt,
200+ };
275201
276202 const manifest = yield* parseManifestForRecipe({
277203 source,
278− revision,
204+ revision: artifact.revision,
279205 });
280206
281207 return {
282208 source,
283209 sourceRecord,
284− revision,
285− documents: documentsByRevisionId.get(sourceRecord.recipeRevisionId) ?? [],
286− schemaBundles: schemaBundlesByRevisionId.get(sourceRecord.recipeRevisionId) ?? [],
287− operations: operationsByRevisionId.get(sourceRecord.recipeRevisionId) ?? [],
210+ revision: artifact.revision,
211+ documents: artifact.documents,
212+ schemaBundles: artifact.schemaBundles,
213+ operations: artifact.operations,
288214 manifest,
289215 } satisfies LoadedSourceRecipe;
290216 }),
291217 );
218+ return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null);
292219 });
293220
294221 export const loadSourceWithRecipe = (input: {
@@ -298,104 +225,57 @@ export const loadSourceWithRecipe = (input: { · 3 unmodified lines
298225 actorAccountId?: AccountId | null;
299226 }): Effect.Effect<LoadedSourceRecipe, Error, never> =>
300227 Effect.gen(function* () {
301− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
302− if (
303− runtimeLocalWorkspace !== null
304− && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId
305− ) {
306− const source = yield* loadSourceById(input.rows, {
307− workspaceId: input.workspaceId,
308− sourceId: input.sourceId,
309− actorAccountId: input.actorAccountId,
310− });
311−
312− const artifact = yield* Effect.tryPromise({
313− try: () =>
314− readLocalSourceArtifact({
315− context: runtimeLocalWorkspace.context,
316− sourceId: source.id,
317− }),
318− catch: (cause) =>
319− cause instanceof Error ? cause : new Error(String(cause)),
320− });
321− if (artifact === null) {
322− return yield* Effect.fail(
323− new Error(`Recipe artifact missing for source ${input.sourceId}`),
324− );
325− }
326−
327− const sourceRecord: StoredSourceRecord = {
328− id: source.id,
329− workspaceId: source.workspaceId,
330− recipeId: artifact.recipeId,
331− recipeRevisionId: artifact.revision.id,
332− name: source.name,
333− kind: source.kind,
334− endpoint: source.endpoint,
335− status: source.status,
336− enabled: source.enabled,
337− namespace: source.namespace,
338− importAuthPolicy: source.importAuthPolicy,
339− bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
340− sourceHash: source.sourceHash,
341− lastError: source.lastError,
342− createdAt: source.createdAt,
343− updatedAt: source.updatedAt,
344− };
345− const manifest = yield* parseManifestForRecipe({
346− source,
347− revision: artifact.revision,
348− });
349−
350− return {
351− source,
352− sourceRecord,
353− revision: artifact.revision,
354− documents: artifact.documents,
355− schemaBundles: artifact.schemaBundles,
356− operations: artifact.operations,
357− manifest,
358− } satisfies LoadedSourceRecipe;
359− }
360−
361− const sourceRecord = yield* input.rows.sources.getByWorkspaceAndId(
362− input.workspaceId,
363− input.sourceId,
364− );
365− if (Option.isNone(sourceRecord)) {
366− return yield* Effect.fail(
367− new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
368− );
369− }
370−
228+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
371229 const source = yield* loadSourceById(input.rows, {
372230 workspaceId: input.workspaceId,
373231 sourceId: input.sourceId,
374232 actorAccountId: input.actorAccountId,
375233 });
376− const revision = yield* input.rows.sourceRecipeRevisions.getById(sourceRecord.value.recipeRevisionId);
377− if (Option.isNone(revision)) {
234+ const artifact = yield* Effect.tryPromise({
235+ try: () =>
236+ readLocalSourceArtifact({
237+ context: runtimeLocalWorkspace.context,
238+ sourceId: source.id,
239+ }),
240+ catch: (cause) =>
241+ cause instanceof Error ? cause : new Error(String(cause)),
242+ });
243+ if (artifact === null) {
378244 return yield* Effect.fail(
379− new Error(`Recipe revision missing for source ${input.sourceId}`),
245+ new Error(`Recipe artifact missing for source ${input.sourceId}`),
380246 );
381247 }
382− const [documents, schemaBundles, operations, manifest] = yield* Effect.all([
383− input.rows.sourceRecipeDocuments.listByRevisionId(sourceRecord.value.recipeRevisionId),
384− input.rows.sourceRecipeSchemaBundles.listByRevisionId(sourceRecord.value.recipeRevisionId),
385− input.rows.sourceRecipeOperations.listByRevisionId(sourceRecord.value.recipeRevisionId),
386− parseManifestForRecipe({
387− source,
388− revision: revision.value,
389− }),
390− ]);
248+
249+ const sourceRecord: StoredSourceRecord = {
250+ id: source.id,
251+ workspaceId: source.workspaceId,
252+ recipeId: artifact.recipeId,
253+ recipeRevisionId: artifact.revision.id,
254+ name: source.name,
255+ kind: source.kind,
256+ endpoint: source.endpoint,
257+ status: source.status,
258+ enabled: source.enabled,
259+ namespace: source.namespace,
260+ importAuthPolicy: source.importAuthPolicy,
261+ bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source),
262+ sourceHash: source.sourceHash,
263+ lastError: source.lastError,
264+ createdAt: source.createdAt,
265+ updatedAt: source.updatedAt,
266+ };
267+ const manifest = yield* parseManifestForRecipe({
268+ source,
269+ revision: artifact.revision,
270+ });
391271
392272 return {
393273 source,
394− sourceRecord: sourceRecord.value,
395− revision: revision.value,
396− documents,
397− schemaBundles,
398− operations,
274+ sourceRecord,
275+ revision: artifact.revision,
276+ documents: artifact.documents,
277+ schemaBundles: artifact.schemaBundles,
278+ operations: artifact.operations,
399279 manifest,
400280 } satisfies LoadedSourceRecipe;
401281 });
packages/control-plane/src/runtime/source-store.test.ts+130−348
@@ -1,10 +1,13 @@
1+import { mkdtempSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { join } from "node:path";
4+
15 import { describe, expect, it } from "@effect/vitest";
26
37 import {
48 AccountIdSchema,
59 decodeBuiltInAuthArtifactConfig,
610 McpSourceAuthSessionDataJsonSchema,
7− OrganizationIdSchema,
811 SecretMaterialIdSchema,
912 SourceAuthSessionIdSchema,
1013 SourceIdSchema,
@@ -21,9 +24,16 @@ import { · 10 unmodified lines
2124 type SqlControlPlanePersistence,
2225 } from "../persistence";
2326 import {
24− stableSourceRecipeId,
25− stableSourceRecipeRevisionId,
26−} from "./source-definitions";
27+ loadLocalExecutorConfig,
28+ resolveLocalWorkspaceContext,
29+} from "./local-config";
30+import {
31+ RuntimeLocalWorkspaceService,
32+ type RuntimeLocalWorkspaceState,
33+} from "./local-runtime-context";
34+import {
35+ loadLocalWorkspaceState,
36+} from "./local-workspace-state";
2737 import { persistSource, removeSourceById } from "./source-store";
2838
2939 const makePersistence: Effect.Effect<SqlControlPlanePersistence, unknown, Scope.Scope> =
@@ -40,6 +50,42 @@ const makePersistence: Effect.Effect<SqlControlPlanePersistence, unknown, Scope. · 10 unmodified lines
4050
4151 const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema);
4252
53+const makeRuntimeLocalWorkspace = (input: {
54+ workspaceId: Source["workspaceId"];
55+ accountId: ReturnType<typeof AccountIdSchema.make>;
56+}) =>
57+ Effect.gen(function* () {
58+ const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-source-store-"));
59+ const context = yield* Effect.tryPromise({
60+ try: () => resolveLocalWorkspaceContext({ workspaceRoot }),
61+ catch: (cause) =>
62+ cause instanceof Error ? cause : new Error(String(cause)),
63+ });
64+ const loadedConfig = yield* Effect.tryPromise({
65+ try: () => loadLocalExecutorConfig(context),
66+ catch: (cause) =>
67+ cause instanceof Error ? cause : new Error(String(cause)),
68+ });
69+
70+ return {
71+ context,
72+ installation: {
73+ workspaceId: input.workspaceId,
74+ accountId: input.accountId,
75+ organizationId: "org_local_personal" as never,
76+ },
77+ loadedConfig,
78+ } satisfies RuntimeLocalWorkspaceState;
79+ });
80+
81+const withRuntimeLocalWorkspace = <A, E>(
82+ effect: Effect.Effect<A, E, never>,
83+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
84+) =>
85+ effect.pipe(
86+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
87+ );
88+
4389 const makeOpenApiSource = (input: {
4490 workspaceId: Source["workspaceId"];
4591 sourceId: Source["id"];
@@ -78,28 +124,13 @@ describe("source-store", () => { · 32 unmodified lines
78124 const persistence = yield* makePersistence;
79125 const now = Date.now();
80126 const accountId = AccountIdSchema.make("acc_source_store");
81− const organizationId = OrganizationIdSchema.make("org_source_store");
82127 const workspaceId = WorkspaceIdSchema.make("ws_source_store");
83− const sourceId = SourceIdSchema.make("src_source_store");
128+ const sourceId = SourceIdSchema.make("github");
84129 const firstTokenId = SecretMaterialIdSchema.make("sec_source_store_first");
85130 const secondTokenId = SecretMaterialIdSchema.make("sec_source_store_second");
86−
87− yield* persistence.rows.organizations.insert({
88− id: organizationId,
89− slug: "source-store",
90− name: "Source Store",
91− status: "active",
92− createdByAccountId: accountId,
93− createdAt: now,
94− updatedAt: now,
95− });
96− yield* persistence.rows.workspaces.insert({
97− id: workspaceId,
98− organizationId,
99− name: "Primary",
100− createdByAccountId: accountId,
101− createdAt: now,
102− updatedAt: now,
131+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({
132+ workspaceId,
133+ accountId,
103134 });
104135
105136 yield* persistence.rows.secretMaterials.upsert({
@@ -119,22 +150,25 @@ describe("source-store", () => { · 13 unmodified lines
119150 updatedAt: now,
120151 });
121152
122− yield* persistSource(
123− persistence.rows,
124− makeOpenApiSource({
125− workspaceId,
126− sourceId,
127− now,
128− auth: {
129− kind: "bearer",
130− headerName: "Authorization",
131− prefix: "Bearer ",
132− token: {
133− providerId: "postgres",
134− handle: firstTokenId,
153+ yield* withRuntimeLocalWorkspace(
154+ persistSource(
155+ persistence.rows,
156+ makeOpenApiSource({
157+ workspaceId,
158+ sourceId,
159+ now,
160+ auth: {
161+ kind: "bearer",
162+ headerName: "Authorization",
163+ prefix: "Bearer ",
164+ token: {
165+ providerId: "postgres",
166+ handle: firstTokenId,
167+ },
135168 },
136− },
137− }),
169+ }),
170+ ),
171+ runtimeLocalWorkspace,
138172 );
139173
140174 yield* persistence.rows.sourceAuthSessions.upsert({
@@ -167,23 +201,26 @@ describe("source-store", () => { · 26 unmodified lines
167201 updatedAt: now,
168202 });
169203
170− yield* persistSource(
171− persistence.rows,
172− makeOpenApiSource({
173− workspaceId,
174− sourceId,
175− now,
176− updatedAt: now + 1,
177− auth: {
178− kind: "bearer",
179− headerName: "Authorization",
180− prefix: "Bearer ",
181− token: {
182− providerId: "postgres",
183− handle: secondTokenId,
204+ yield* withRuntimeLocalWorkspace(
205+ persistSource(
206+ persistence.rows,
207+ makeOpenApiSource({
208+ workspaceId,
209+ sourceId,
210+ now,
211+ updatedAt: now + 1,
212+ auth: {
213+ kind: "bearer",
214+ headerName: "Authorization",
215+ prefix: "Bearer ",
216+ token: {
217+ providerId: "postgres",
218+ handle: secondTokenId,
219+ },
184220 },
185− },
186− }),
221+ }),
222+ ),
223+ runtimeLocalWorkspace,
187224 );
188225
189226 expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(firstTokenId))).toBe(true);
@@ -196,16 +233,30 @@ describe("source-store", () => { · 6 unmodified lines
196233 : null,
197234 ).toBe(secondTokenId);
198235
199− const removed = yield* removeSourceById(persistence.rows, {
200− workspaceId,
201− sourceId,
202− });
236+ const removed = yield* withRuntimeLocalWorkspace(
237+ removeSourceById(persistence.rows, {
238+ workspaceId,
239+ sourceId,
240+ }),
241+ runtimeLocalWorkspace,
242+ );
203243 expect(removed).toBe(true);
204244
205245 expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(secondTokenId))).toBe(true);
206− expect(yield* persistence.rows.sources.listByWorkspaceId(workspaceId)).toHaveLength(0);
207246 expect(yield* persistence.rows.authArtifacts.listByWorkspaceId(workspaceId)).toHaveLength(0);
208247 expect(yield* persistence.rows.sourceAuthSessions.listByWorkspaceId(workspaceId)).toHaveLength(0);
248+ const config = yield* Effect.tryPromise({
249+ try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context),
250+ catch: (cause) =>
251+ cause instanceof Error ? cause : new Error(String(cause)),
252+ });
253+ expect(config.config?.sources?.[sourceId]).toBeUndefined();
254+ const workspaceState = yield* Effect.tryPromise({
255+ try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context),
256+ catch: (cause) =>
257+ cause instanceof Error ? cause : new Error(String(cause)),
258+ });
259+ expect(workspaceState.sources[sourceId]).toBeUndefined();
209260 }),
210261 );
211262
@@ -214,27 +265,12 @@ describe("source-store", () => { · 2 unmodified lines
214265 const persistence = yield* makePersistence;
215266 const now = Date.now();
216267 const accountId = AccountIdSchema.make("acc_actor_scoped");
217− const organizationId = OrganizationIdSchema.make("org_actor_scoped");
218268 const workspaceId = WorkspaceIdSchema.make("ws_actor_scoped");
219− const sourceId = SourceIdSchema.make("src_actor_scoped");
269+ const sourceId = SourceIdSchema.make("github");
220270 const sharedTokenId = SecretMaterialIdSchema.make("sec_actor_scoped_shared");
221−
222− yield* persistence.rows.organizations.insert({
223− id: organizationId,
224− slug: "actor-scoped",
225− name: "Actor Scoped",
226− status: "active",
227− createdByAccountId: accountId,
228− createdAt: now,
229− updatedAt: now,
230− });
231− yield* persistence.rows.workspaces.insert({
232− id: workspaceId,
233− organizationId,
234− name: "Primary",
235− createdByAccountId: accountId,
236− createdAt: now,
237− updatedAt: now,
271+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({
272+ workspaceId,
273+ accountId,
238274 });
239275 yield* persistence.rows.secretMaterials.upsert({
240276 id: sharedTokenId,
@@ -260,16 +296,22 @@ describe("source-store", () => { · 19 unmodified lines
260296 },
261297 });
262298
263− yield* persistSource(persistence.rows, source);
264− yield* persistSource(
265− persistence.rows,
266− {
267− ...source,
268− updatedAt: now + 1,
269− },
270− {
271− actorAccountId: accountId,
272− },
299+ yield* withRuntimeLocalWorkspace(
300+ persistSource(persistence.rows, source),
301+ runtimeLocalWorkspace,
302+ );
303+ yield* withRuntimeLocalWorkspace(
304+ persistSource(
305+ persistence.rows,
306+ {
307+ ...source,
308+ updatedAt: now + 1,
309+ },
310+ {
311+ actorAccountId: accountId,
312+ },
313+ ),
314+ runtimeLocalWorkspace,
273315 );
274316
275317 const authArtifacts = yield* persistence.rows.authArtifacts.listByWorkspaceAndSourceId({
@@ -282,264 +324,4 @@ describe("source-store", () => { · 6 unmodified lines
282324 expect(new Set(authArtifacts.map((artifact) => artifact.id)).size).toBe(2);
283325 }),
284326 );
285−
286− it.scoped("cleans up orphaned recipe data when the source config changes", () =>
287− Effect.gen(function* () {
288− const persistence = yield* makePersistence;
289− const now = Date.now();
290− const accountId = AccountIdSchema.make("acc_recipe_rewrite");
291− const organizationId = OrganizationIdSchema.make("org_recipe_rewrite");
292− const workspaceId = WorkspaceIdSchema.make("ws_recipe_rewrite");
293− const sourceId = SourceIdSchema.make("src_recipe_rewrite");
294−
295− yield* persistence.rows.organizations.insert({
296− id: organizationId,
297− slug: "recipe-rewrite",
298− name: "Recipe Rewrite",
299− status: "active",
300− createdByAccountId: accountId,
301− createdAt: now,
302− updatedAt: now,
303− });
304− yield* persistence.rows.workspaces.insert({
305− id: workspaceId,
306− organizationId,
307− name: "Primary",
308− createdByAccountId: accountId,
309− createdAt: now,
310− updatedAt: now,
311− });
312−
313− const initialSource = makeOpenApiSource({
314− workspaceId,
315− sourceId,
316− now,
317− auth: { kind: "none" },
318− });
319− const initialRecipeId = stableSourceRecipeId(initialSource);
320− const initialRecipeRevisionId = stableSourceRecipeRevisionId(initialSource);
321−
322− yield* persistSource(persistence.rows, initialSource);
323− yield* persistence.rows.sourceRecipeDocuments.replaceForRevision({
324− recipeRevisionId: initialRecipeRevisionId,
325− documents: [{
326− id: "src_recipe_doc_recipe_rewrite",
327− recipeRevisionId: initialRecipeRevisionId,
328− documentKind: "openapi",
329− documentKey:
330− typeof initialSource.binding.specUrl === "string"
331− ? initialSource.binding.specUrl
332− : initialSource.endpoint,
333− contentText: "{}",
334− contentHash: "hash_recipe_rewrite",
335− fetchedAt: now,
336− createdAt: now,
337− updatedAt: now,
338− }],
339− });
340− yield* persistence.rows.sourceRecipeOperations.replaceForRevision({
341− recipeRevisionId: initialRecipeRevisionId,
342− operations: [{
343− id: "src_recipe_op_recipe_rewrite",
344− recipeRevisionId: initialRecipeRevisionId,
345− operationKey: "getRepo",
346− transportKind: "http",
347− toolId: "getRepo",
348− title: "Get Repo",
349− description: "Read a repository",
350− operationKind: "read",
351− searchText: "github get repo",
352− inputSchemaJson: null,
353− outputSchemaJson: null,
354− providerKind: "openapi",
355− providerDataJson: JSON.stringify({
356− kind: "openapi",
357− toolId: "getRepo",
358− rawToolId: "repos_getRepo",
359− operationId: "repos.getRepo",
360− group: "repos",
361− leaf: "getRepo",
362− tags: ["repos"],
363− method: "get",
364− path: "/repos/{owner}/{repo}",
365− operationHash: "hash_recipe_rewrite",
366− invocation: {
367− method: "get",
368− pathTemplate: "/repos/{owner}/{repo}",
369− parameters: [],
370− requestBody: null,
371− },
372− }),
373− createdAt: now,
374− updatedAt: now,
375− }],
376− });
377−
378− const updatedSource = makeOpenApiSource({
379− workspaceId,
380− sourceId,
381− now,
382− updatedAt: now + 1,
383− endpoint: "https://api.example.com",
384− specUrl: "https://api.example.com/openapi.json",
385− auth: { kind: "none" },
386− });
387− const nextRecipeId = stableSourceRecipeId(updatedSource);
388− const nextRecipeRevisionId = stableSourceRecipeRevisionId(updatedSource);
389−
390− yield* persistSource(persistence.rows, updatedSource);
391−
392− expect(Option.isNone(yield* persistence.rows.sourceRecipes.getById(initialRecipeId))).toBe(true);
393− expect(
394− Option.isNone(yield* persistence.rows.sourceRecipeRevisions.getById(initialRecipeRevisionId)),
395− ).toBe(true);
396− expect(
397− yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(initialRecipeRevisionId),
398− ).toEqual([]);
399− expect(
400− yield* persistence.rows.sourceRecipeOperations.listByRevisionId(initialRecipeRevisionId),
401− ).toEqual([]);
402− expect(Option.isSome(yield* persistence.rows.sourceRecipes.getById(nextRecipeId))).toBe(true);
403− expect(
404− Option.isSome(yield* persistence.rows.sourceRecipeRevisions.getById(nextRecipeRevisionId)),
405− ).toBe(true);
406− }),
407− );
408−
409− it.scoped("retains shared recipe data until the last source reference is removed", () =>
410− Effect.gen(function* () {
411− const persistence = yield* makePersistence;
412− const now = Date.now();
413− const accountId = AccountIdSchema.make("acc_shared_recipe");
414− const organizationId = OrganizationIdSchema.make("org_shared_recipe");
415− const workspaceId = WorkspaceIdSchema.make("ws_shared_recipe");
416− const firstSourceId = SourceIdSchema.make("src_shared_recipe_one");
417− const secondSourceId = SourceIdSchema.make("src_shared_recipe_two");
418−
419− yield* persistence.rows.organizations.insert({
420− id: organizationId,
421− slug: "shared-recipe",
422− name: "Shared Recipe",
423− status: "active",
424− createdByAccountId: accountId,
425− createdAt: now,
426− updatedAt: now,
427− });
428− yield* persistence.rows.workspaces.insert({
429− id: workspaceId,
430− organizationId,
431− name: "Primary",
432− createdByAccountId: accountId,
433− createdAt: now,
434− updatedAt: now,
435− });
436−
437− const firstSource = makeOpenApiSource({
438− workspaceId,
439− sourceId: firstSourceId,
440− now,
441− name: "GitHub One",
442− auth: { kind: "none" },
443− });
444− const secondSource = makeOpenApiSource({
445− workspaceId,
446− sourceId: secondSourceId,
447− now,
448− updatedAt: now + 1,
449− name: "GitHub Two",
450− auth: { kind: "none" },
451− });
452− const sharedRecipeId = stableSourceRecipeId(firstSource);
453− const sharedRecipeRevisionId = stableSourceRecipeRevisionId(firstSource);
454−
455− yield* persistSource(persistence.rows, firstSource);
456− yield* persistSource(persistence.rows, secondSource);
457− yield* persistence.rows.sourceRecipeDocuments.replaceForRevision({
458− recipeRevisionId: sharedRecipeRevisionId,
459− documents: [{
460− id: "src_recipe_doc_shared_recipe",
461− recipeRevisionId: sharedRecipeRevisionId,
462− documentKind: "openapi",
463− documentKey:
464− typeof firstSource.binding.specUrl === "string"
465− ? firstSource.binding.specUrl
466− : firstSource.endpoint,
467− contentText: "{}",
468− contentHash: "hash_shared_recipe",
469− fetchedAt: now,
470− createdAt: now,
471− updatedAt: now,
472− }],
473− });
474− yield* persistence.rows.sourceRecipeOperations.replaceForRevision({
475− recipeRevisionId: sharedRecipeRevisionId,
476− operations: [{
477− id: "src_recipe_op_shared_recipe",
478− recipeRevisionId: sharedRecipeRevisionId,
479− operationKey: "getRepo",
480− transportKind: "http",
481− toolId: "getRepo",
482− title: "Get Repo",
483− description: "Read a repository",
484− operationKind: "read",
485− searchText: "github get repo",
486− inputSchemaJson: null,
487− outputSchemaJson: null,
488− providerKind: "openapi",
489− providerDataJson: JSON.stringify({
490− kind: "openapi",
491− toolId: "getRepo",
492− rawToolId: "repos_getRepo",
493− operationId: "repos.getRepo",
494− group: "repos",
495− leaf: "getRepo",
496− tags: ["repos"],
497− method: "get",
498− path: "/repos/{owner}/{repo}",
499− operationHash: "hash_shared_recipe",
500− invocation: {
501− method: "get",
502− pathTemplate: "/repos/{owner}/{repo}",
503− parameters: [],
504− requestBody: null,
505− },
506− }),
507− createdAt: now,
508− updatedAt: now,
509− }],
510− });
511−
512− yield* removeSourceById(persistence.rows, {
513− workspaceId,
514− sourceId: firstSourceId,
515− });
516−
517− expect(Option.isSome(yield* persistence.rows.sourceRecipes.getById(sharedRecipeId))).toBe(true);
518− expect(
519− Option.isSome(yield* persistence.rows.sourceRecipeRevisions.getById(sharedRecipeRevisionId)),
520− ).toBe(true);
521− expect(
522− yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(sharedRecipeRevisionId),
523− ).toHaveLength(1);
524− expect(
525− yield* persistence.rows.sourceRecipeOperations.listByRevisionId(sharedRecipeRevisionId),
526− ).toHaveLength(1);
527−
528− yield* removeSourceById(persistence.rows, {
529− workspaceId,
530− sourceId: secondSourceId,
531− });
532−
533− expect(Option.isNone(yield* persistence.rows.sourceRecipes.getById(sharedRecipeId))).toBe(true);
534− expect(
535− Option.isNone(yield* persistence.rows.sourceRecipeRevisions.getById(sharedRecipeRevisionId)),
536− ).toBe(true);
537− expect(
538− yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(sharedRecipeRevisionId),
539− ).toEqual([]);
540− expect(
541− yield* persistence.rows.sourceRecipeOperations.listByRevisionId(sharedRecipeRevisionId),
542− ).toEqual([]);
543− }),
544− );
545327 });
packages/control-plane/src/runtime/source-store.ts+178−428
@@ -6,26 +6,23 @@ import type { · 5 unmodified lines
66 LocalConfigSource,
77 Source,
88 SourceId,
9− SourceRecipeId,
10− SourceRecipeRevisionId,
119 WorkspaceId,
1210 } from "#schema";
1311 import { SourceIdSchema } from "#schema";
1412 import { type SqlControlPlaneRows } from "#persistence";
1513 import * as Effect from "effect/Effect";
16−import * as Option from "effect/Option";
1714
1815 import {
19− createSourceRecipeRecord,
20− createSourceRecipeRevisionRecord,
21− projectSourceFromStorage,
22− projectSourcesFromStorage,
2316 stableSourceRecipeId,
2417 stableSourceRecipeRevisionId,
2518 splitSourceForStorage,
2619 } from "./source-definitions";
20+import {
21+ sourceAuthFromAuthArtifact,
22+} from "./auth-artifacts";
2723 import {
2824 loadLocalExecutorConfig,
25+ type LoadedLocalExecutorConfig,
2926 type ResolvedLocalWorkspaceContext,
3027 writeProjectLocalExecutorConfig,
3128 } from "./local-config";
@@ -34,7 +31,7 @@ import { · 2 unmodified lines
3431 removeLocalSourceArtifact,
3532 } from "./local-source-artifacts";
3633 import {
37− getRuntimeLocalWorkspaceOption,
34+ requireRuntimeLocalWorkspace,
3835 } from "./local-runtime-context";
3936 import {
4037 loadLocalWorkspaceState,
@@ -136,7 +133,7 @@ const deriveLocalSourceId = ( · 95 unmodified lines
136133 return SourceIdSchema.make(candidate);
137134 };
138135
139−const resolveLocalConfigSecretProviderAlias = (config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]): string | null => {
136+const resolveLocalConfigSecretProviderAlias = (config: LoadedLocalExecutorConfig["config"]): string | null => {
140137 const defaultAlias = trimOrNull(config?.secrets?.defaults?.env);
141138 if (defaultAlias !== null && config?.secrets?.providers?.[defaultAlias]) {
142139 return defaultAlias;
@@ -147,7 +144,7 @@ const resolveLocalConfigSecretProviderAlias = (config: Awaited<ReturnType<typeof · 4 unmodified lines
147144
148145 const sourceAuthFromConfigInput = (input: {
149146 auth: unknown;
150− config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"];
147+ config: LoadedLocalExecutorConfig["config"];
151148 existing: Source["auth"] | null;
152149 }): Source["auth"] => {
153150 if (input.auth === undefined) {
@@ -175,9 +172,11 @@ const sourceAuthFromConfigInput = (input: { · 21 unmodified lines
175172 };
176173 const providerAlias = trimOrNull(explicit.provider);
177174 const providerId = providerAlias
178− ? toConfigSecretProviderId(providerAlias)
175+ ? (providerAlias === "params" ? "params" : toConfigSecretProviderId(providerAlias))
179176 : explicit.source === "env"
180177 ? "env"
178+ : explicit.source === "params"
179+ ? "params"
181180 : null;
182181 const handle = trimOrNull(explicit.id);
183182 if (providerId && handle) {
@@ -199,7 +198,7 @@ const sourceAuthFromConfigInput = (input: { · 15 unmodified lines
199198 const configAuthFromSource = (input: {
200199 source: Source;
201200 existingConfigAuth: LocalConfigSecretInput | undefined;
202− config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"];
201+ config: LoadedLocalExecutorConfig["config"];
203202 }): LocalConfigSecretInput | undefined => {
204203 if (input.source.auth.kind !== "bearer") {
205204 return input.existingConfigAuth;
@@ -209,6 +208,14 @@ const configAuthFromSource = (input: { · 3 unmodified lines
209208 return input.source.auth.token.handle;
210209 }
211210
211+ if (input.source.auth.token.providerId === "params") {
212+ return {
213+ source: "params",
214+ provider: "params",
215+ id: input.source.auth.token.handle,
216+ };
217+ }
218+
212219 const provider = fromConfigSecretProviderId(input.source.auth.token.providerId);
213220 if (provider !== null) {
214221 const configuredProvider = input.config?.secrets?.providers?.[provider];
@@ -226,11 +233,7 @@ const configAuthFromSource = (input: { · 11 unmodified lines
226233
227234 const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) =>
228235 Effect.gen(function* () {
229− const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
230− if (runtimeLocalWorkspace === null || runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
231− return null;
232− }
233−
236+ const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
234237 const loadedConfig = yield* Effect.tryPromise({
235238 try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context),
236239 catch: (cause) =>
@@ -253,7 +256,7 @@ const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) => · 16 unmodified lines
253256 const buildLocalSourceRecord = (input: {
254257 context: ResolvedLocalWorkspaceContext;
255258 workspaceId: WorkspaceId;
256− loadedConfig: Awaited<ReturnType<typeof loadLocalExecutorConfig>>;
259+ loadedConfig: LoadedLocalExecutorConfig;
257260 workspaceState: LocalWorkspaceState;
258261 sourceId: SourceId;
259262 actorAccountId?: AccountId | null;
@@ -335,11 +338,23 @@ const buildLocalSourceRecord = (input: { · 75 unmodified lines
335338 updatedAt: baseSource.updatedAt,
336339 };
337340
338− const source = yield* projectSourceFromStorage({
339− sourceRecord,
340− runtimeAuthArtifact,
341− importAuthArtifact,
342− });
341+ const source: Source = {
342+ ...baseSource,
343+ auth:
344+ runtimeAuthArtifact === null
345+ ? baseSource.auth
346+ : sourceAuthFromAuthArtifact(runtimeAuthArtifact),
347+ importAuth:
348+ baseSource.importAuthPolicy === "separate"
349+ ? importAuthArtifact === null
350+ ? baseSource.importAuth
351+ : sourceAuthFromAuthArtifact(importAuthArtifact)
352+ : { kind: "none" },
353+ sourceHash: sourceRecord.sourceHash,
354+ lastError: sourceRecord.lastError,
355+ createdAt: sourceRecord.createdAt,
356+ updatedAt: sourceRecord.updatedAt,
357+ };
343358
344359 return {
345360 source,
@@ -356,49 +371,23 @@ export const loadSourcesInWorkspace = ( · 10 unmodified lines
356371 ) =>
357372 Effect.gen(function* () {
358373 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);
359− if (localWorkspace !== null) {
360− const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
361− return yield* Effect.forEach(
362− Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}),
363− (sourceId) =>
364− Effect.map(
365− buildLocalSourceRecord({
366− context: localWorkspace.context,
367− workspaceId,
368− loadedConfig: localWorkspace.loadedConfig,
369− workspaceState: localWorkspace.workspaceState,
370− sourceId: SourceIdSchema.make(sourceId),
371− actorAccountId: options.actorAccountId,
372− authArtifacts,
373− }),
374− ({ source }) => source,
375− ),
376− );
377− }
378−
379− const sourceRecords = yield* rows.sources.listByWorkspaceId(workspaceId);
380374 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
381− const filteredAuthArtifacts = sourceRecords.flatMap((sourceRecord) => {
382− const matches = authArtifacts.filter((artifact) => artifact.sourceId === sourceRecord.id);
383− const preferred = selectPreferredAuthArtifact({
384− authArtifacts: matches,
385− actorAccountId: options.actorAccountId,
386− slot: "runtime",
387− });
388− const preferredImport = selectPreferredAuthArtifact({
389− authArtifacts: matches,
390− actorAccountId: options.actorAccountId,
391− slot: "import",
392− });
393− return [preferred, preferredImport].filter(
394− (artifact): artifact is AuthArtifact => artifact !== null,
395− );
396− });
397−
398− return yield* projectSourcesFromStorage({
399− sourceRecords,
400− authArtifacts: filteredAuthArtifacts,
401− });
375+ return yield* Effect.forEach(
376+ Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}),
377+ (sourceId) =>
378+ Effect.map(
379+ buildLocalSourceRecord({
380+ context: localWorkspace.context,
381+ workspaceId,
382+ loadedConfig: localWorkspace.loadedConfig,
383+ workspaceState: localWorkspace.workspaceState,
384+ sourceId: SourceIdSchema.make(sourceId),
385+ actorAccountId: options.actorAccountId,
386+ authArtifacts,
387+ }),
388+ ({ source }) => source,
389+ ),
390+ );
402391 });
403392
404393 export const loadSourceById = (rows: SqlControlPlaneRows, input: {
@@ -408,64 +397,30 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 3 unmodified lines
408397 }) =>
409398 Effect.gen(function* () {
410399 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
411− if (localWorkspace !== null) {
412− const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
413− if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
414− return yield* Effect.fail(
415− new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
416− );
417− }
418−
419− const localSource = yield* buildLocalSourceRecord({
420− context: localWorkspace.context,
421− workspaceId: input.workspaceId,
422− loadedConfig: localWorkspace.loadedConfig,
423− workspaceState: localWorkspace.workspaceState,
424− sourceId: input.sourceId,
425− actorAccountId: input.actorAccountId,
426− authArtifacts,
427− });
428−
429− return localSource.source;
430− }
431−
432− const sourceRecord = yield* rows.sources.getByWorkspaceAndId(
433− input.workspaceId,
434− input.sourceId,
435− );
436−
437− if (Option.isNone(sourceRecord)) {
400+ const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
401+ if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
438402 return yield* Effect.fail(
439403 new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
440404 );
441405 }
442406
443− const authArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({
407+ const localSource = yield* buildLocalSourceRecord({
408+ context: localWorkspace.context,
444409 workspaceId: input.workspaceId,
410+ loadedConfig: localWorkspace.loadedConfig,
411+ workspaceState: localWorkspace.workspaceState,
445412 sourceId: input.sourceId,
446− });
447− const authArtifact = selectPreferredAuthArtifact({
448− authArtifacts,
449413 actorAccountId: input.actorAccountId,
450− slot: "runtime",
451− });
452− const importAuthArtifact = selectPreferredAuthArtifact({
453414 authArtifacts,
454− actorAccountId: input.actorAccountId,
455− slot: "import",
456415 });
457416
458− return yield* projectSourceFromStorage({
459− sourceRecord: sourceRecord.value,
460− runtimeAuthArtifact: authArtifact,
461− importAuthArtifact,
462− });
417+ return localSource.source;
463418 });
464419
465420 const configSourceFromLocalSource = (input: {
466421 source: Source;
467422 existingConfigAuth: LocalConfigSecretInput | undefined;
468− config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"];
423+ config: LoadedLocalExecutorConfig["config"];
469424 }): LocalConfigSource => {
470425 const auth = configAuthFromSource({
471426 source: input.source,
@@ -554,113 +509,61 @@ const removeAuthArtifactsForSource = (rows: SqlControlPlaneRows, input: { · 82 unmodified lines
554509 return existingAuthArtifacts.length;
555510 });
556511
557−const cleanupOrphanedRecipeData = (rows: SqlControlPlaneRows, input: {
558− recipeId: SourceRecipeId;
559− recipeRevisionId: SourceRecipeRevisionId;
560−}) =>
561− Effect.gen(function* () {
562− const revisionReferenceCount = yield* rows.sources.countByRecipeRevisionId(
563− input.recipeRevisionId,
564− );
565− if (revisionReferenceCount === 0) {
566− yield* rows.sourceRecipeDocuments.removeByRevisionId(input.recipeRevisionId);
567− yield* rows.sourceRecipeSchemaBundles.removeByRevisionId(input.recipeRevisionId);
568− yield* rows.sourceRecipeOperations.removeByRevisionId(input.recipeRevisionId);
569− }
570−
571− const recipeReferenceCount = yield* rows.sources.countByRecipeId(input.recipeId);
572− if (recipeReferenceCount > 0) {
573− return;
574− }
575−
576− const recipeRevisions = yield* rows.sourceRecipeRevisions.listByRecipeId(input.recipeId);
577− yield* Effect.forEach(
578− recipeRevisions,
579− (recipeRevision) =>
580− Effect.all([
581− rows.sourceRecipeDocuments.removeByRevisionId(recipeRevision.id),
582− rows.sourceRecipeSchemaBundles.removeByRevisionId(recipeRevision.id),
583− rows.sourceRecipeOperations.removeByRevisionId(recipeRevision.id),
584− ]),
585− { discard: true },
586− );
587− yield* rows.sourceRecipeRevisions.removeByRecipeId(input.recipeId);
588− yield* rows.sourceRecipes.removeById(input.recipeId);
589− });
590−
591512 export const removeSourceById = (rows: SqlControlPlaneRows, input: {
592513 workspaceId: WorkspaceId;
593514 sourceId: Source["id"];
594515 }) =>
595516 Effect.gen(function* () {
596517 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
597− if (localWorkspace !== null) {
598− if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
599− return false;
600− }
601−
602− const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
603− const sources = {
604− ...(projectConfig.sources ?? {}),
605− };
606− delete sources[input.sourceId];
607− yield* Effect.tryPromise({
608− try: () =>
609− writeProjectLocalExecutorConfig({
610− context: localWorkspace.context,
611− config: {
612− ...projectConfig,
613− sources,
614− },
615− }),
616− catch: (cause) =>
617− cause instanceof Error ? cause : new Error(String(cause)),
618− });
619−
620− const {
621− [input.sourceId]: _removedSource,
622− ...remainingSources
623− } = localWorkspace.workspaceState.sources;
624− const workspaceState: LocalWorkspaceState = {
625− ...localWorkspace.workspaceState,
626− sources: remainingSources,
627− };
628− yield* Effect.tryPromise({
629− try: () =>
630− writeLocalWorkspaceState({
631− context: localWorkspace.context,
632− state: workspaceState,
633− }),
634− catch: (cause) =>
635− cause instanceof Error ? cause : new Error(String(cause)),
636− });
637− yield* Effect.tryPromise({
638− try: () =>
639− removeLocalSourceArtifact({
640− context: localWorkspace.context,
641− sourceId: input.sourceId,
642− }),
643− catch: (cause) =>
644− cause instanceof Error ? cause : new Error(String(cause)),
645− });
646−
647− yield* rows.sourceAuthSessions.removeByWorkspaceAndSourceId(
648− input.workspaceId,
649− input.sourceId,
650− );
651− yield* rows.sourceOauthClients.removeByWorkspaceAndSourceId({
652− workspaceId: input.workspaceId,
653− sourceId: input.sourceId,
654− });
655− yield* removeAuthArtifactsForSource(rows, input);
656− return true;
657− }
658−
659− const sourceRecord = yield* rows.sources.getByWorkspaceAndId(input.workspaceId, input.sourceId);
660− if (Option.isNone(sourceRecord)) {
518+ if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
661519 return false;
662520 }
663521
522+ const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
523+ const sources = {
524+ ...(projectConfig.sources ?? {}),
525+ };
526+ delete sources[input.sourceId];
527+ yield* Effect.tryPromise({
528+ try: () =>
529+ writeProjectLocalExecutorConfig({
530+ context: localWorkspace.context,
531+ config: {
532+ ...projectConfig,
533+ sources,
534+ },
535+ }),
536+ catch: (cause) =>
537+ cause instanceof Error ? cause : new Error(String(cause)),
538+ });
539+
540+ const {
541+ [input.sourceId]: _removedSource,
542+ ...remainingSources
543+ } = localWorkspace.workspaceState.sources;
544+ const workspaceState: LocalWorkspaceState = {
545+ ...localWorkspace.workspaceState,
546+ sources: remainingSources,
547+ };
548+ yield* Effect.tryPromise({
549+ try: () =>
550+ writeLocalWorkspaceState({
551+ context: localWorkspace.context,
552+ state: workspaceState,
553+ }),
554+ catch: (cause) =>
555+ cause instanceof Error ? cause : new Error(String(cause)),
556+ });
557+ yield* Effect.tryPromise({
558+ try: () =>
559+ removeLocalSourceArtifact({
560+ context: localWorkspace.context,
561+ sourceId: input.sourceId,
562+ }),
563+ catch: (cause) =>
564+ cause instanceof Error ? cause : new Error(String(cause)),
565+ });
566+
664567 yield* rows.sourceAuthSessions.removeByWorkspaceAndSourceId(
665568 input.workspaceId,
666569 input.sourceId,
@@ -670,15 +573,6 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 3 unmodified lines
670573 sourceId: input.sourceId,
671574 });
672575 yield* removeAuthArtifactsForSource(rows, input);
673− const removed = yield* rows.sources.removeByWorkspaceAndId(input.workspaceId, input.sourceId);
674− if (!removed) {
675− return false;
676− }
677−
678− yield* cleanupOrphanedRecipeData(rows, {
679− recipeId: sourceRecord.value.recipeId,
680− recipeRevisionId: sourceRecord.value.recipeRevisionId,
681− });
682576
683577 return true;
684578 });
@@ -692,157 +586,20 @@ export const persistSource = ( · 7 unmodified lines
692586 ) =>
693587 Effect.gen(function* () {
694588 const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId);
695− if (localWorkspace !== null) {
696− const nextSource = {
697− ...source,
698− id:
699− localWorkspace.loadedConfig.config?.sources?.[source.id]
700− || localWorkspace.workspaceState.sources[source.id]
701− ? source.id
702− : deriveLocalSourceId(
703− source,
704− new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})),
705− ),
706− } satisfies Source;
707− const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({
708− workspaceId: nextSource.workspaceId,
709− sourceId: nextSource.id,
710− });
711− const existingRuntimeAuthArtifact = selectExactAuthArtifact({
712− authArtifacts: existingAuthArtifacts,
713− actorAccountId: options.actorAccountId,
714− slot: "runtime",
715− });
716− const existingImportAuthArtifact = selectExactAuthArtifact({
717− authArtifacts: existingAuthArtifacts,
718− actorAccountId: options.actorAccountId,
719− slot: "import",
720− });
721− const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
722− const sources = {
723− ...(projectConfig.sources ?? {}),
724− };
725− const existingConfigSource = sources[nextSource.id];
726− sources[nextSource.id] = configSourceFromLocalSource({
727− source: nextSource,
728− existingConfigAuth: existingConfigSource?.connection.auth,
729− config: localWorkspace.loadedConfig.config,
730− });
731− yield* Effect.tryPromise({
732− try: () =>
733− writeProjectLocalExecutorConfig({
734− context: localWorkspace.context,
735− config: {
736− ...projectConfig,
737− sources,
738− },
739− }),
740− catch: (cause) =>
741− cause instanceof Error ? cause : new Error(String(cause)),
742− });
743−
744− const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({
745− source: nextSource,
746− recipeId: stableSourceRecipeId(nextSource),
747− recipeRevisionId: stableSourceRecipeRevisionId(nextSource),
748− actorAccountId: options.actorAccountId,
749− existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null,
750− existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null,
751− });
752−
753− if (runtimeAuthArtifact === null) {
754− if (existingRuntimeAuthArtifact !== null) {
755− yield* removeAuthLeaseAndSecrets(rows, {
756− authArtifactId: existingRuntimeAuthArtifact.id,
757− });
758− }
759− yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
760− workspaceId: nextSource.workspaceId,
761− sourceId: nextSource.id,
762− actorAccountId: options.actorAccountId ?? null,
763− slot: "runtime",
764− });
765− } else {
766− yield* rows.authArtifacts.upsert(runtimeAuthArtifact);
767− if (
768− existingRuntimeAuthArtifact !== null
769− && existingRuntimeAuthArtifact.id !== runtimeAuthArtifact.id
770− ) {
771− yield* removeAuthLeaseAndSecrets(rows, {
772− authArtifactId: existingRuntimeAuthArtifact.id,
773− });
774− }
775− }
776−
777− yield* cleanupAuthArtifactSecretRefs(rows, {
778− previous: existingRuntimeAuthArtifact ?? null,
779− next: runtimeAuthArtifact,
780− });
781−
782− if (importAuthArtifact === null) {
783− if (existingImportAuthArtifact !== null) {
784− yield* removeAuthLeaseAndSecrets(rows, {
785− authArtifactId: existingImportAuthArtifact.id,
786− });
787− }
788− yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
789− workspaceId: nextSource.workspaceId,
790− sourceId: nextSource.id,
791− actorAccountId: options.actorAccountId ?? null,
792− slot: "import",
793− });
794− } else {
795− yield* rows.authArtifacts.upsert(importAuthArtifact);
796− if (
797− existingImportAuthArtifact !== null
798− && existingImportAuthArtifact.id !== importAuthArtifact.id
799− ) {
800− yield* removeAuthLeaseAndSecrets(rows, {
801− authArtifactId: existingImportAuthArtifact.id,
802− });
803− }
804− }
805−
806− yield* cleanupAuthArtifactSecretRefs(rows, {
807− previous: existingImportAuthArtifact ?? null,
808− next: importAuthArtifact,
809− });
810−
811− const existingSourceState = localWorkspace.workspaceState.sources[nextSource.id];
812− const workspaceState: LocalWorkspaceState = {
813− ...localWorkspace.workspaceState,
814− sources: {
815− ...localWorkspace.workspaceState.sources,
816− [nextSource.id]: {
817− status: nextSource.status,
818− lastError: nextSource.lastError,
819− sourceHash: nextSource.sourceHash,
820− createdAt: existingSourceState?.createdAt ?? nextSource.createdAt,
821− updatedAt: nextSource.updatedAt,
822− },
823− },
824− };
825− yield* Effect.tryPromise({
826− try: () =>
827− writeLocalWorkspaceState({
828− context: localWorkspace.context,
829− state: workspaceState,
830− }),
831− catch: (cause) =>
832− cause instanceof Error ? cause : new Error(String(cause)),
833− });
834−
835− return yield* loadSourceById(rows, {
836− workspaceId: source.workspaceId,
837− sourceId: nextSource.id,
838− actorAccountId: options.actorAccountId,
839− });
840− }
841−
842− const existing = yield* rows.sources.getByWorkspaceAndId(source.workspaceId, source.id);
589+ const nextSource = {
590+ ...source,
591+ id:
592+ localWorkspace.loadedConfig.config?.sources?.[source.id]
593+ || localWorkspace.workspaceState.sources[source.id]
594+ ? source.id
595+ : deriveLocalSourceId(
596+ source,
597+ new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})),
598+ ),
599+ } satisfies Source;
843600 const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({
844− workspaceId: source.workspaceId,
845− sourceId: source.id,
601+ workspaceId: nextSource.workspaceId,
602+ sourceId: nextSource.id,
846603 });
847604 const existingRuntimeAuthArtifact = selectExactAuthArtifact({
848605 authArtifacts: existingAuthArtifacts,
@@ -854,73 +611,38 @@ export const persistSource = ( · 5 unmodified lines
854611 actorAccountId: options.actorAccountId,
855612 slot: "import",
856613 });
857−
858− const nextRecipeId = stableSourceRecipeId(source);
859− const nextRecipeRevisionId = Option.isSome(existing) && existing.value.recipeId === nextRecipeId
860− ? existing.value.recipeRevisionId
861− : stableSourceRecipeRevisionId(source);
862− const existingTargetRevision = yield* rows.sourceRecipeRevisions.getById(nextRecipeRevisionId);
863− const nextRevision = createSourceRecipeRevisionRecord({
864− source,
865− recipeId: nextRecipeId,
866− recipeRevisionId: nextRecipeRevisionId,
867− revisionNumber: Option.isSome(existingTargetRevision)
868− ? existingTargetRevision.value.revisionNumber
869− : 1,
870− manifestJson: Option.isSome(existingTargetRevision)
871− ? existingTargetRevision.value.manifestJson
872− : null,
873− manifestHash: Option.isSome(existingTargetRevision)
874− ? existingTargetRevision.value.manifestHash
875− : null,
876− materializationHash: Option.isSome(existingTargetRevision)
877− ? existingTargetRevision.value.materializationHash
878− : null,
614+ const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
615+ const sources = {
616+ ...(projectConfig.sources ?? {}),
617+ };
618+ const existingConfigSource = sources[nextSource.id];
619+ sources[nextSource.id] = configSourceFromLocalSource({
620+ source: nextSource,
621+ existingConfigAuth: existingConfigSource?.connection.auth,
622+ config: localWorkspace.loadedConfig.config,
879623 });
880−
881− const nextRecipe = createSourceRecipeRecord({
882− source,
883− recipeId: nextRecipeId,
884− latestRevisionId: nextRevision.id,
624+ yield* Effect.tryPromise({
625+ try: () =>
626+ writeProjectLocalExecutorConfig({
627+ context: localWorkspace.context,
628+ config: {
629+ ...projectConfig,
630+ sources,
631+ },
632+ }),
633+ catch: (cause) =>
634+ cause instanceof Error ? cause : new Error(String(cause)),
885635 });
886636
887− const { sourceRecord, runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({
888− source,
889− recipeId: nextRecipe.id,
890− recipeRevisionId: nextRevision.id,
637+ const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({
638+ source: nextSource,
639+ recipeId: stableSourceRecipeId(nextSource),
640+ recipeRevisionId: stableSourceRecipeRevisionId(nextSource),
891641 actorAccountId: options.actorAccountId,
892642 existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null,
893643 existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null,
894644 });
895645
896− if (Option.isNone(existing)) {
897− yield* rows.sources.insert(sourceRecord);
898− } else {
899− const {
900− id: _id,
901− workspaceId: _workspaceId,
902− createdAt: _createdAt,
903− ...patch
904− } = sourceRecord;
905− yield* rows.sources.update(source.workspaceId, source.id, patch);
906− }
907−
908− yield* rows.sourceRecipes.upsert(nextRecipe);
909− yield* rows.sourceRecipeRevisions.upsert(nextRevision);
910−
911− if (
912− Option.isSome(existing)
913− && (
914− existing.value.recipeId !== nextRecipeId
915− || existing.value.recipeRevisionId !== nextRecipeRevisionId
916− )
917− ) {
918− yield* cleanupOrphanedRecipeData(rows, {
919− recipeId: existing.value.recipeId,
920− recipeRevisionId: existing.value.recipeRevisionId,
921− });
922− }
923−
924646 if (runtimeAuthArtifact === null) {
925647 if (existingRuntimeAuthArtifact !== null) {
926648 yield* removeAuthLeaseAndSecrets(rows, {
@@ -928,8 +650,8 @@ export const persistSource = ( · 1 unmodified line
928650 });
929651 }
930652 yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
931− workspaceId: source.workspaceId,
932− sourceId: source.id,
653+ workspaceId: nextSource.workspaceId,
654+ sourceId: nextSource.id,
933655 actorAccountId: options.actorAccountId ?? null,
934656 slot: "runtime",
935657 });
@@ -957,8 +679,8 @@ export const persistSource = ( · 21 unmodified lines
957679 });
958680 }
959681 yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
960− workspaceId: source.workspaceId,
961− sourceId: source.id,
682+ workspaceId: nextSource.workspaceId,
683+ sourceId: nextSource.id,
962684 actorAccountId: options.actorAccountId ?? null,
963685 slot: "import",
964686 });
@@ -979,5 +701,33 @@ export const persistSource = ( · 14 unmodified lines
979701 next: importAuthArtifact,
980702 });
981703
982− return source;
704+ const existingSourceState = localWorkspace.workspaceState.sources[nextSource.id];
705+ const workspaceState: LocalWorkspaceState = {
706+ ...localWorkspace.workspaceState,
707+ sources: {
708+ ...localWorkspace.workspaceState.sources,
709+ [nextSource.id]: {
710+ status: nextSource.status,
711+ lastError: nextSource.lastError,
712+ sourceHash: nextSource.sourceHash,
713+ createdAt: existingSourceState?.createdAt ?? nextSource.createdAt,
714+ updatedAt: nextSource.updatedAt,
715+ },
716+ },
717+ };
718+ yield* Effect.tryPromise({
719+ try: () =>
720+ writeLocalWorkspaceState({
721+ context: localWorkspace.context,
722+ state: workspaceState,
723+ }),
724+ catch: (cause) =>
725+ cause instanceof Error ? cause : new Error(String(cause)),
726+ });
727+
728+ return yield* loadSourceById(rows, {
729+ workspaceId: nextSource.workspaceId,
730+ sourceId: nextSource.id,
731+ actorAccountId: options.actorAccountId,
732+ });
983733 });
packages/control-plane/src/runtime/test-http-client.ts+3−3
@@ -15,10 +15,10 @@ import { · 14 unmodified lines
1515
1616 import {
1717 ControlPlaneAuthHeaders,
18− type SqlControlPlaneRuntime,
18+ type ControlPlaneRuntime,
1919 } from "./index";
2020
21−const createClientLayer = (runtime: SqlControlPlaneRuntime) => {
21+const createClientLayer = (runtime: ControlPlaneRuntime) => {
2222 const apiLayer = createControlPlaneApiLayer(runtime.runtimeLayer);
2323
2424 return HttpApiBuilder.serve().pipe(
@@ -48,7 +48,7 @@ type ControlPlaneClient = Effect.Effect.Success< · 23 unmodified lines
4848
4949 export const withControlPlaneClient = <A, E>(
5050 input: {
51− runtime: SqlControlPlaneRuntime;
51+ runtime: ControlPlaneRuntime;
5252 accountId?: string;
5353 },
5454 f: (client: ControlPlaneClient) => Effect.Effect<A, E, never>,
packages/control-plane/src/runtime/workspace-execution-environment.test.ts+377−196
@@ -1,5 +1,8 @@
11 import { randomUUID } from "node:crypto";
2+import { mkdtempSync } from "node:fs";
23 import { createServer } from "node:http";
4+import { tmpdir } from "node:os";
5+import { join } from "node:path";
36
47 import {
58 HttpApi,
@@ -45,7 +48,6 @@ import * as Effect from "effect/Effect"; · 39 unmodified lines
4548 import * as Exit from "effect/Exit";
4649 import * as Layer from "effect/Layer";
4750 import * as Scope from "effect/Scope";
48−import * as Option from "effect/Option";
4951 import { Schema } from "effect";
5052 import * as z from "zod/v4";
5153
@@ -59,9 +61,28 @@ import { · 7 unmodified lines
5961 createRuntimeSourceAuthService,
6062 type RuntimeSourceAuthService,
6163 } from "./source-auth-service";
64+import {
65+ loadLocalExecutorConfig,
66+ resolveLocalWorkspaceContext,
67+ writeProjectLocalExecutorConfig,
68+} from "./local-config";
6269 import { createLiveExecutionManager } from "./live-execution";
63−import { persistSource } from "./source-store";
64−import { persistMcpRecipeMaterializationFromManifest } from "./source-materialization";
70+import {
71+ RuntimeLocalWorkspaceService,
72+ type RuntimeLocalWorkspaceState,
73+} from "./local-runtime-context";
74+import {
75+ buildLocalSourceArtifact,
76+ readLocalSourceArtifact,
77+ writeLocalSourceArtifact,
78+} from "./local-source-artifacts";
79+import {
80+ loadLocalWorkspaceState,
81+ writeLocalWorkspaceState,
82+} from "./local-workspace-state";
83+import {
84+ materializationFromMcpManifestEntries,
85+} from "./source-adapters/mcp";
6586 import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment";
6687
6788 type CountedMcpServer = {
@@ -579,7 +600,7 @@ const makeGraphqlServer = Effect.acquireRelease( · 511 unmodified lines
579600 );
580601
581602 const persistConnectedEchoTool = (input: {
582− persistence: SqlControlPlanePersistence;
603+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
583604 endpoint: string;
584605 workspaceId: WorkspaceId;
585606 sourceId: SourceId;
@@ -610,32 +631,85 @@ const persistConnectedEchoTool = (input: { · 24 unmodified lines
610631 updatedAt: now,
611632 };
612633
613− yield* persistSource(input.persistence.rows, source);
614− yield* persistMcpRecipeMaterializationFromManifest({
615− rows: input.persistence.rows,
616− source,
617− manifestEntries: [
618− {
619− toolId: "echo",
620− toolName: "echo",
621− description: "Echo the provided string",
622− inputSchemaJson: JSON.stringify({
623− type: "object",
624− properties: {
625− value: {
626− type: "string",
634+ yield* Effect.tryPromise({
635+ try: () =>
636+ writeProjectLocalExecutorConfig({
637+ context: input.runtimeLocalWorkspace.context,
638+ config: {
639+ sources: {
640+ [input.sourceId]: {
641+ kind: "mcp",
642+ name: source.name,
643+ ...(source.namespace ? { namespace: source.namespace } : {}),
644+ connection: {
645+ endpoint: source.endpoint,
646+ },
647+ binding: {
648+ transport: "streamable-http",
649+ },
627650 },
628651 },
629− required: ["value"],
630− additionalProperties: false,
652+ },
653+ }),
654+ catch: (cause) =>
655+ cause instanceof Error ? cause : new Error(String(cause)),
656+ });
657+ yield* Effect.tryPromise({
658+ try: () =>
659+ writeLocalSourceArtifact({
660+ context: input.runtimeLocalWorkspace.context,
661+ sourceId: source.id,
662+ artifact: buildLocalSourceArtifact({
663+ source,
664+ materialization: materializationFromMcpManifestEntries({
665+ recipeRevisionId: "src_recipe_rev_materialization" as never,
666+ endpoint: source.endpoint,
667+ manifestEntries: [
668+ {
669+ toolId: "echo",
670+ toolName: "echo",
671+ description: "Echo the provided string",
672+ inputSchemaJson: JSON.stringify({
673+ type: "object",
674+ properties: {
675+ value: { type: "string" },
676+ },
677+ required: ["value"],
678+ additionalProperties: false,
679+ }),
680+ },
681+ ],
682+ }),
631683 }),
632− },
633− ],
684+ }),
685+ catch: (cause) =>
686+ cause instanceof Error ? cause : new Error(String(cause)),
634687 });
635− });
688+ yield* Effect.tryPromise({
689+ try: () =>
690+ writeLocalWorkspaceState({
691+ context: input.runtimeLocalWorkspace.context,
692+ state: {
693+ version: 1,
694+ sources: {
695+ [input.sourceId]: {
696+ status: "connected",
697+ lastError: null,
698+ sourceHash: null,
699+ createdAt: now,
700+ updatedAt: now,
701+ },
702+ },
703+ policies: {},
704+ },
705+ }),
706+ catch: (cause) =>
707+ cause instanceof Error ? cause : new Error(String(cause)),
708+ });
709+ }) as Effect.Effect<void, Error, never>;
636710
637711 const persistConnectedGithubOpenApiSource = (input: {
638− persistence: SqlControlPlanePersistence;
712+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
639713 workspaceId: WorkspaceId;
640714 sourceId: SourceId;
641715 endpoint?: string;
@@ -831,7 +905,22 @@ const persistConnectedGithubOpenApiSource = (input: { · 189 unmodified lines
831905 updatedAt: now,
832906 };
833907
834− yield* persistSource(input.persistence.rows, source);
908+ const configAuth =
909+ source.auth.kind === "bearer"
910+ ? source.auth.token.providerId === "params"
911+ ? {
912+ source: "params" as const,
913+ provider: "params",
914+ id: source.auth.token.handle,
915+ }
916+ : source.auth.token.providerId === "env"
917+ ? source.auth.token.handle
918+ : {
919+ source: "env" as const,
920+ provider: "env",
921+ id: source.auth.token.handle,
922+ }
923+ : undefined;
835924
836925 const manifest = yield* extractOpenApiManifest(
837926 source.name,
@@ -842,131 +931,189 @@ const persistConnectedGithubOpenApiSource = (input: { · 4 unmodified lines
842931 ),
843932 );
844933 const definitions = compileOpenApiToolDefinitions(manifest);
845− const sourceRecord =
846− yield* input.persistence.rows.sources.getByWorkspaceAndId(
847− input.workspaceId,
848− input.sourceId,
849− );
850− if (Option.isNone(sourceRecord)) {
851− return yield* Effect.fail(
852− new Error(`Missing stored source ${input.sourceId}`),
853− );
854− }
855−
856− yield* input.persistence.rows.sourceRecipeRevisions.update(
857− sourceRecord.value.recipeRevisionId,
858− {
859− manifestJson: JSON.stringify(manifest),
860− manifestHash: manifest.sourceHash,
861− updatedAt: now,
862− },
863− );
864− yield* input.persistence.rows.sourceRecipeDocuments.replaceForRevision({
865− recipeRevisionId: sourceRecord.value.recipeRevisionId,
866− documents: [
867− {
868− id: `src_recipe_doc_${randomUUID()}`,
869− recipeRevisionId: sourceRecord.value.recipeRevisionId,
870− documentKind: "openapi",
871− documentKey:
872− typeof source.binding.specUrl === "string"
873− ? source.binding.specUrl
874− : source.endpoint,
875− contentText: openApiDocumentText,
876− contentHash: manifest.sourceHash,
877− fetchedAt: now,
878− createdAt: now,
879− updatedAt: now,
880− },
881− ],
934+ yield* Effect.tryPromise({
935+ try: () =>
936+ writeProjectLocalExecutorConfig({
937+ context: input.runtimeLocalWorkspace.context,
938+ config: {
939+ sources: {
940+ [input.sourceId]: {
941+ kind: "openapi",
942+ name: source.name,
943+ ...(source.namespace ? { namespace: source.namespace } : {}),
944+ connection: {
945+ endpoint: source.endpoint,
946+ ...(configAuth !== undefined ? { auth: configAuth } : {}),
947+ },
948+ binding: {
949+ specUrl: String(source.binding.specUrl),
950+ defaultHeaders: null,
951+ },
952+ },
953+ },
954+ },
955+ }),
956+ catch: (cause) =>
957+ cause instanceof Error ? cause : new Error(String(cause)),
882958 });
883959 const refEntries = Object.entries(manifest.refHintTable ?? {});
884− yield* input.persistence.rows.sourceRecipeSchemaBundles.replaceForRevision({
885− recipeRevisionId: sourceRecord.value.recipeRevisionId,
886− bundles:
887− refEntries.length > 0
888− ? [
889− {
890− id: SourceRecipeSchemaBundleIdSchema.make(
891− `src_recipe_bundle_${randomUUID()}`,
892− ),
893− recipeRevisionId: sourceRecord.value.recipeRevisionId,
894− bundleKind: "json_schema_ref_map",
895− refsJson: JSON.stringify(
896− Object.fromEntries(
897− refEntries.map(([ref, rawValue]) => {
898− try {
899− return [ref, JSON.parse(rawValue) as unknown];
900− } catch {
901− return [ref, rawValue];
902− }
903− }),
960+ yield* Effect.tryPromise({
961+ try: () =>
962+ writeLocalSourceArtifact({
963+ context: input.runtimeLocalWorkspace.context,
964+ sourceId: source.id,
965+ artifact: buildLocalSourceArtifact({
966+ source: {
967+ ...source,
968+ sourceHash: manifest.sourceHash,
969+ },
970+ materialization: {
971+ manifestJson: JSON.stringify(manifest),
972+ manifestHash: manifest.sourceHash,
973+ sourceHash: manifest.sourceHash,
974+ documents: [
975+ {
976+ id: `src_recipe_doc_${randomUUID()}`,
977+ recipeRevisionId: "src_recipe_rev_materialization" as never,
978+ documentKind: "openapi",
979+ documentKey:
980+ typeof source.binding.specUrl === "string"
981+ ? source.binding.specUrl
982+ : source.endpoint,
983+ contentText: openApiDocumentText,
984+ contentHash: manifest.sourceHash,
985+ fetchedAt: now,
986+ createdAt: now,
987+ updatedAt: now,
988+ },
989+ ],
990+ schemaBundles:
991+ refEntries.length > 0
992+ ? [
993+ {
994+ id: SourceRecipeSchemaBundleIdSchema.make(
995+ `src_recipe_bundle_${randomUUID()}`,
996+ ),
997+ recipeRevisionId: "src_recipe_rev_materialization" as never,
998+ bundleKind: "json_schema_ref_map",
999+ refsJson: JSON.stringify(
1000+ Object.fromEntries(
1001+ refEntries.map(([ref, rawValue]) => {
1002+ try {
1003+ return [ref, JSON.parse(rawValue) as unknown];
1004+ } catch {
1005+ return [ref, rawValue];
1006+ }
1007+ }),
1008+ ),
1009+ ),
1010+ contentHash: manifest.sourceHash,
1011+ createdAt: now,
1012+ updatedAt: now,
1013+ },
1014+ ]
1015+ : [],
1016+ operations: definitions.map((definition) => {
1017+ const presentation = buildOpenApiToolPresentation({
1018+ definition,
1019+ });
1020+ const method = definition.method.toUpperCase();
1021+
1022+ return {
1023+ id: `src_recipe_op_${randomUUID()}`,
1024+ recipeRevisionId: "src_recipe_rev_materialization" as never,
1025+ operationKey: definition.toolId,
1026+ transportKind: "http",
1027+ toolId: definition.toolId,
1028+ title: definition.name,
1029+ description: definition.description,
1030+ operationKind:
1031+ method === "GET" || method === "HEAD"
1032+ ? "read"
1033+ : method === "DELETE"
1034+ ? "delete"
1035+ : "write",
1036+ searchText: normalizeSearchText(
1037+ definition.toolId,
1038+ definition.name,
1039+ definition.description,
1040+ definition.rawToolId,
1041+ definition.operationId ?? undefined,
1042+ definition.method,
1043+ definition.path,
1044+ definition.group,
1045+ definition.leaf,
1046+ definition.tags.join(" "),
9041047 ),
905− ),
906− contentHash: manifest.sourceHash,
1048+ inputSchemaJson: presentation.inputSchemaJson ?? null,
1049+ outputSchemaJson: presentation.outputSchemaJson ?? null,
1050+ providerKind: "openapi",
1051+ providerDataJson: presentation.providerDataJson,
1052+ createdAt: now,
1053+ updatedAt: now,
1054+ };
1055+ }),
1056+ },
1057+ }),
1058+ }),
1059+ catch: (cause) =>
1060+ cause instanceof Error ? cause : new Error(String(cause)),
1061+ });
1062+ yield* Effect.tryPromise({
1063+ try: () =>
1064+ writeLocalWorkspaceState({
1065+ context: input.runtimeLocalWorkspace.context,
1066+ state: {
1067+ version: 1,
1068+ sources: {
1069+ [input.sourceId]: {
1070+ status: "connected",
1071+ lastError: null,
1072+ sourceHash: manifest.sourceHash,
9071073 createdAt: now,
9081074 updatedAt: now,
9091075 },
910− ]
911− : [],
912− });
913− yield* input.persistence.rows.sourceRecipeOperations.replaceForRevision({
914− recipeRevisionId: sourceRecord.value.recipeRevisionId,
915− operations: definitions.map((definition) => {
916− const presentation = buildOpenApiToolPresentation({
917− definition,
918− });
919− const method = definition.method.toUpperCase();
920−
921− return {
922− id: `src_recipe_op_${randomUUID()}`,
923− recipeRevisionId: sourceRecord.value.recipeRevisionId,
924− operationKey: definition.toolId,
925− transportKind: "http",
926− toolId: definition.toolId,
927− title: definition.name,
928− description: definition.description,
929− operationKind:
930− method === "GET" || method === "HEAD"
931− ? "read"
932− : method === "DELETE"
933− ? "delete"
934− : "write",
935− searchText: normalizeSearchText(
936− definition.toolId,
937− definition.name,
938− definition.description,
939− definition.rawToolId,
940− definition.operationId ?? undefined,
941− definition.method,
942− definition.path,
943− definition.group,
944− definition.leaf,
945− definition.tags.join(" "),
946− ),
947− inputSchemaJson: presentation.inputSchemaJson ?? null,
948− outputSchemaJson: presentation.outputSchemaJson ?? null,
949− providerKind: "openapi",
950− providerDataJson: presentation.providerDataJson,
951− createdAt: now,
952− updatedAt: now,
953− };
954− }),
1076+ },
1077+ policies: {},
1078+ },
1079+ }),
1080+ catch: (cause) =>
1081+ cause instanceof Error ? cause : new Error(String(cause)),
9551082 });
956− yield* input.persistence.rows.sources.update(
957− input.workspaceId,
958− input.sourceId,
959− {
960− sourceHash: manifest.sourceHash,
961− updatedAt: now,
962− },
963− );
9641083
9651084 return;
1085+ }) as Effect.Effect<void, Error, never>;
1086+
1087+const makeRuntimeLocalWorkspaceState = (workspaceId: WorkspaceId): Effect.Effect<RuntimeLocalWorkspaceState, Error, never> =>
1088+ Effect.gen(function* () {
1089+ const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-workspace-env-"));
1090+ const context = yield* Effect.tryPromise({
1091+ try: () => resolveLocalWorkspaceContext({ workspaceRoot }),
1092+ catch: (cause) =>
1093+ cause instanceof Error ? cause : new Error(String(cause)),
1094+ });
1095+ const loadedConfig = yield* Effect.tryPromise({
1096+ try: () => loadLocalExecutorConfig(context),
1097+ catch: (cause) =>
1098+ cause instanceof Error ? cause : new Error(String(cause)),
1099+ });
1100+ return {
1101+ context,
1102+ installation: {
1103+ workspaceId,
1104+ accountId: AccountIdSchema.make("acc_local_test"),
1105+ organizationId: "org_local_personal" as never,
1106+ },
1107+ loadedConfig,
1108+ } satisfies RuntimeLocalWorkspaceState;
9661109 });
9671110
968−const makeResolver = (persistence: SqlControlPlanePersistence) =>
969− createWorkspaceExecutionEnvironmentResolver({
1111+const makeResolver = (
1112+ persistence: SqlControlPlanePersistence,
1113+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
1114+) =>
1115+ ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) =>
1116+ createWorkspaceExecutionEnvironmentResolver({
9701117 rows: persistence.rows,
9711118 sourceAuthService: {
9721119 getLocalServerBaseUrl: () => null,
@@ -983,19 +1130,25 @@ const makeResolver = (persistence: SqlControlPlanePersistence) => · 10 unmodified lines
9831130 completeSourceCredentialSetup: () =>
9841131 Effect.fail(new Error("not implemented in test")),
9851132 } as RuntimeSourceAuthService,
986− });
1133+ })(input).pipe(
1134+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
1135+ ));
9871136
9881137 const makeResolverWithLiveSourceAuth = (
9891138 persistence: SqlControlPlanePersistence,
1139+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
9901140 ) =>
991− createWorkspaceExecutionEnvironmentResolver({
1141+ ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) =>
1142+ createWorkspaceExecutionEnvironmentResolver({
9921143 rows: persistence.rows,
9931144 sourceAuthService: createRuntimeSourceAuthService({
9941145 rows: persistence.rows,
9951146 liveExecutionManager: createLiveExecutionManager(),
9961147 getLocalServerBaseUrl: () => "http://127.0.0.1:8788",
9971148 }),
998− });
1149+ })(input).pipe(
1150+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
1151+ ));
9991152
10001153 describe("workspace-execution-environment", () => {
10011154 it.scoped("discovers persisted tools without live MCP listing", () =>
@@ -1004,8 +1157,9 @@ describe("workspace-execution-environment", () => { · 2 unmodified lines
10041157 const persistence = yield* makePersistence;
10051158 const workspaceId = WorkspaceIdSchema.make("ws_catalog");
10061159 const sourceId = SourceIdSchema.make("src_catalog");
1160+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
10071161 yield* persistConnectedEchoTool({
1008− persistence,
1162+ runtimeLocalWorkspace,
10091163 endpoint: server.endpoint,
10101164 workspaceId,
10111165 sourceId,
@@ -1017,7 +1171,7 @@ describe("workspace-execution-environment", () => { · 5 unmodified lines
10171171 cause instanceof Error ? cause : new Error(String(cause)),
10181172 }).pipe(Effect.orDie);
10191173
1020− const resolveEnvironment = makeResolver(persistence);
1174+ const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace);
10211175 const environment = yield* resolveEnvironment({
10221176 workspaceId,
10231177 accountId: AccountIdSchema.make("acc_catalog"),
@@ -1050,13 +1204,14 @@ describe("workspace-execution-environment", () => { · 26 unmodified lines
10501204 const persistence = yield* makePersistence;
10511205 const workspaceId = WorkspaceIdSchema.make("ws_github_discovery");
10521206 const sourceId = SourceIdSchema.make("src_github_discovery");
1207+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
10531208 yield* persistConnectedGithubOpenApiSource({
1054− persistence,
1209+ runtimeLocalWorkspace,
10551210 workspaceId,
10561211 sourceId,
10571212 });
10581213
1059− const resolveEnvironment = makeResolver(persistence);
1214+ const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace);
10601215 const environment = yield* resolveEnvironment({
10611216 workspaceId,
10621217 accountId: AccountIdSchema.make("acc_github_discovery"),
@@ -1103,14 +1258,15 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines
11031258 const persistence = yield* makePersistence;
11041259 const workspaceId = WorkspaceIdSchema.make("ws_invoke");
11051260 const sourceId = SourceIdSchema.make("src_invoke");
1261+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
11061262 yield* persistConnectedEchoTool({
1107− persistence,
1263+ runtimeLocalWorkspace,
11081264 endpoint: server.endpoint,
11091265 workspaceId,
11101266 sourceId,
11111267 });
11121268
1113− const resolveEnvironment = makeResolver(persistence);
1269+ const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace);
11141270 const environment = yield* resolveEnvironment({
11151271 workspaceId,
11161272 accountId: AccountIdSchema.make("acc_invoke"),
@@ -1139,7 +1295,8 @@ describe("workspace-execution-environment", () => { · 22 unmodified lines
11391295 const specServer = yield* makeOpenApiSpecServer;
11401296 const persistence = yield* makePersistence;
11411297 const workspaceId = WorkspaceIdSchema.make("ws_add_openapi");
1142− const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence);
1298+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
1299+ const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace);
11431300 const now = Date.now();
11441301 const tokenSecretMaterialId = SecretMaterialIdSchema.make(
11451302 "sec_test_openapi_bearer",
@@ -1220,21 +1377,35 @@ describe("workspace-execution-environment", () => { · 74 unmodified lines
12201377 encodeURIComponent("exec_add_openapi:executor.sources.add:"),
12211378 );
12221379
1223− const storedSource =
1224− yield* persistence.rows.sources.getByWorkspaceAndId(
1225− workspaceId,
1226− added.id,
1227− );
1228− expect(Option.isSome(storedSource)).toBe(true);
1229− if (Option.isSome(storedSource)) {
1230− const documents =
1231− yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(
1232− storedSource.value.recipeRevisionId,
1233− );
1234− expect(documents[0]?.contentText).toContain(
1235− '"operationId":"repos.getRepo"',
1236− );
1237− }
1380+ const loadedConfig = yield* Effect.tryPromise({
1381+ try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context),
1382+ catch: (cause) =>
1383+ cause instanceof Error ? cause : new Error(String(cause)),
1384+ });
1385+ expect(loadedConfig.config?.sources?.[added.id]).toMatchObject({
1386+ kind: "openapi",
1387+ connection: {
1388+ endpoint: `${specServer.baseUrl}/`,
1389+ },
1390+ });
1391+ const artifact = yield* Effect.tryPromise({
1392+ try: () =>
1393+ readLocalSourceArtifact({
1394+ context: runtimeLocalWorkspace.context,
1395+ sourceId: added.id,
1396+ }),
1397+ catch: (cause) =>
1398+ cause instanceof Error ? cause : new Error(String(cause)),
1399+ });
1400+ expect(artifact?.documents[0]?.contentText).toContain(
1401+ '"operationId":"repos.getRepo"',
1402+ );
1403+ const workspaceState = yield* Effect.tryPromise({
1404+ try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context),
1405+ catch: (cause) =>
1406+ cause instanceof Error ? cause : new Error(String(cause)),
1407+ });
1408+ expect(workspaceState.sources[added.id]?.status).toBe("connected");
12381409
12391410 const freshEnvironment = yield* resolveEnvironment({
12401411 workspaceId,
@@ -1280,7 +1451,8 @@ describe("workspace-execution-environment", () => { · 39 unmodified lines
12801451 const graphqlServer = yield* makeGraphqlServer;
12811452 const persistence = yield* makePersistence;
12821453 const workspaceId = WorkspaceIdSchema.make("ws_add_graphql");
1283− const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence);
1454+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
1455+ const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace);
12841456 const now = Date.now();
12851457 const tokenSecretMaterialId = SecretMaterialIdSchema.make(
12861458 "sec_test_graphql_bearer",
@@ -1339,33 +1511,38 @@ describe("workspace-execution-environment", () => { · 52 unmodified lines
13391511 expect(added.auth.kind).toBe("bearer");
13401512 expect(added.auth.token?.providerId).toBe("postgres");
13411513
1342− const storedSource =
1343− yield* persistence.rows.sources.getByWorkspaceAndId(
1344− workspaceId,
1345− added.id,
1346− );
1347− expect(Option.isSome(storedSource)).toBe(true);
1348− if (Option.isSome(storedSource)) {
1349− const documents =
1350− yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(
1351− storedSource.value.recipeRevisionId,
1352− );
1353− expect(documents[0]?.contentText).toContain('"__schema"');
1354− expect(
1355− (yield* persistence.rows.sourceRecipeSchemaBundles.listByRevisionId(
1356− storedSource.value.recipeRevisionId,
1357− )).length,
1358− ).toBe(1);
1514+ const artifact = yield* Effect.tryPromise({
1515+ try: () =>
1516+ readLocalSourceArtifact({
1517+ context: runtimeLocalWorkspace.context,
1518+ sourceId: added.id,
1519+ }),
1520+ catch: (cause) =>
1521+ cause instanceof Error ? cause : new Error(String(cause)),
1522+ });
1523+ expect(artifact?.documents[0]?.contentText).toContain('"__schema"');
1524+ expect(artifact?.schemaBundles.length).toBe(1);
1525+ if (artifact === null) {
1526+ throw new Error(`Missing local source artifact for ${added.id}`);
13591527 }
13601528
1361− const connectedSourceRecord = Option.getOrThrow(storedSource);
1362− yield* persistence.rows.sourceRecipeRevisions.update(
1363− connectedSourceRecord.recipeRevisionId,
1364− {
1365− manifestJson: null,
1366− updatedAt: Date.now(),
1367− },
1368− );
1529+ yield* Effect.tryPromise({
1530+ try: () =>
1531+ writeLocalSourceArtifact({
1532+ context: runtimeLocalWorkspace.context,
1533+ sourceId: added.id,
1534+ artifact: {
1535+ ...artifact,
1536+ revision: {
1537+ ...artifact.revision,
1538+ manifestJson: null,
1539+ updatedAt: Date.now(),
1540+ },
1541+ },
1542+ }),
1543+ catch: (cause) =>
1544+ cause instanceof Error ? cause : new Error(String(cause)),
1545+ });
13691546
13701547 const freshEnvironment = yield* resolveEnvironment({
13711548 workspaceId,
@@ -1515,9 +1692,10 @@ describe("workspace-execution-environment", () => { · 143 unmodified lines
15151692 const persistence = yield* makePersistence;
15161693 const workspaceId = WorkspaceIdSchema.make("ws_params_auth");
15171694 const sourceId = SourceIdSchema.make("src_params_auth");
1695+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
15181696
15191697 yield* persistConnectedGithubOpenApiSource({
1520− persistence,
1698+ runtimeLocalWorkspace,
15211699 workspaceId,
15221700 sourceId,
15231701 endpoint: specServer.baseUrl,
@@ -1527,7 +1705,7 @@ describe("workspace-execution-environment", () => { · 3 unmodified lines
15271705 },
15281706 });
15291707
1530− const resolveEnvironment = makeResolver(persistence);
1708+ const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace);
15311709 const environment = yield* resolveEnvironment({
15321710 workspaceId,
15331711 accountId: AccountIdSchema.make("acc_params_auth"),
@@ -1567,14 +1745,15 @@ describe("workspace-execution-environment", () => { · 33 unmodified lines
15671745 "ws_openapi_describe_bundle",
15681746 );
15691747 const sourceId = SourceIdSchema.make("src_openapi_describe_bundle");
1748+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
15701749
15711750 yield* persistConnectedGithubOpenApiSource({
1572− persistence,
1751+ runtimeLocalWorkspace,
15731752 workspaceId,
15741753 sourceId,
15751754 });
15761755
1577− const resolveEnvironment = makeResolver(persistence);
1756+ const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace);
15781757 const environment = yield* resolveEnvironment({
15791758 workspaceId,
15801759 accountId: AccountIdSchema.make("acc_openapi_describe_bundle"),
@@ -1621,10 +1800,12 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines
16211800 () =>
16221801 Effect.gen(function* () {
16231802 const persistence = yield* makePersistence;
1624− const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence);
1803+ const workspaceId = WorkspaceIdSchema.make("ws_describe_add_source");
1804+ const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId);
1805+ const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace);
16251806
16261807 const environment = yield* resolveEnvironment({
1627− workspaceId: WorkspaceIdSchema.make("ws_describe_add_source"),
1808+ workspaceId,
16281809 accountId: AccountIdSchema.make("acc_describe_add_source"),
16291810 executionId: ExecutionIdSchema.make("exec_describe_add_source"),
16301811 });
packages/control-plane/src/runtime/workspace-execution-environment.ts+8−33
@@ -395,49 +395,24 @@ const authorizePersistedToolInvocation = (input: { · 394 unmodified lines
395395 >[0]["onElicitation"];
396396 }): Effect.Effect<void, Error, never> =>
397397 Effect.gen(function* () {
398− const workspace = yield* input.rows.workspaces
399− .getById(input.workspaceId)
400− .pipe(
401− Effect.mapError((cause) =>
402− cause instanceof Error ? cause : new Error(String(cause)),
403− ),
404− );
405− const storedPolicies = Option.isSome(workspace)
406− ? yield* input.rows.policies
407− .listForWorkspaceContext({
408− organizationId: workspace.value.organizationId,
409− workspaceId: input.workspaceId,
410− })
411− .pipe(
412− Effect.mapError((cause) =>
413− cause instanceof Error ? cause : new Error(String(cause)),
414− ),
415− )
416− : [];
417− const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies({
418− store: input.rows,
419− workspaceId: input.workspaceId,
420− }).pipe(
398+ const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
399+ const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
421400 Effect.mapError((cause) =>
422401 cause instanceof Error ? cause : new Error(String(cause)),
423402 ),
424403 );
425− const policies = localWorkspacePolicies === null
426− ? storedPolicies
427− : [
428− ...storedPolicies.filter((policy) => policy.scopeType !== "workspace"),
429− ...localWorkspacePolicies.policies,
430− ];
431404
432405 const decision = evaluateInvocationPolicy({
433406 descriptor: input.descriptor,
434407 args: input.args,
435− policies,
408+ policies: localWorkspacePolicies.policies,
436409 context: {
437410 workspaceId: input.workspaceId,
438− organizationId: Option.isSome(workspace)
439− ? workspace.value.organizationId
440− : ("org_unknown" as never),
411+ organizationId:
412+ runtimeLocalWorkspace !== null
413+ && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId
414+ ? runtimeLocalWorkspace.installation.organizationId
415+ : ("org_unknown" as never),
441416 accountId: input.accountId,
442417 clientId:
443418 typeof input.context?.clientId === "string" &&
packages/control-plane/src/schema/models/local-config.ts+1−0
@@ -6,6 +6,7 @@ export const LocalConfigSecretProviderSourceSchema = Schema.Literal( · 5 unmodified lines
66 "env",
77 "file",
88 "exec",
9+ "params",
910 );
1011
1112 export const LocalConfigEnvSecretProviderSchema = Schema.Struct({
packages/control-plane/src/schema/models/local-installation.ts+5−22
@@ -1,10 +1,5 @@
1−import {
2− createInsertSchema,
3− createSelectSchema,
4− createUpdateSchema,
5−} from "drizzle-orm/effect-schema";
1+import * as Schema from "effect/Schema";
62
7−import { localInstallationsTable } from "../../persistence/schema";
83 import { TimestampMsSchema } from "../common";
94 import {
105 AccountIdSchema,
@@ -13,28 +8,16 @@ import { · 2 unmodified lines
138 WorkspaceIdSchema,
149 } from "../ids";
1510
16−const localInstallationSchemaOverrides = {
11+export const LocalInstallationSchema = Schema.Struct({
1712 id: InstallationIdSchema,
1813 accountId: AccountIdSchema,
1914 organizationId: OrganizationIdSchema,
2015 workspaceId: WorkspaceIdSchema,
2116 createdAt: TimestampMsSchema,
2217 updatedAt: TimestampMsSchema,
23−} as const;
24−
25−export const LocalInstallationSchema = createSelectSchema(
26− localInstallationsTable,
27− localInstallationSchemaOverrides,
28−);
29−
30−export const LocalInstallationInsertSchema = createInsertSchema(
31− localInstallationsTable,
32− localInstallationSchemaOverrides,
33−);
18+});
3419
35−export const LocalInstallationUpdateSchema = createUpdateSchema(
36− localInstallationsTable,
37− localInstallationSchemaOverrides,
38−);
20+export const LocalInstallationInsertSchema = LocalInstallationSchema;
21+export const LocalInstallationUpdateSchema = Schema.partial(LocalInstallationSchema);
3922
4023 export type LocalInstallation = typeof LocalInstallationSchema.Type;
packages/react/src/index.test.tsx+4−4
@@ -5,10 +5,10 @@ import { NodeHttpServer } from "@effect/platform-node"; · 4 unmodified lines
55 import { describe, expect, it } from "@effect/vitest";
66 import {
77 createControlPlaneApiLayer,
8− createSqlControlPlaneRuntime,
8+ createControlPlaneRuntime,
99 type LocalInstallation,
1010 type Source,
11− type SqlControlPlaneRuntime,
11+ type ControlPlaneRuntime,
1212 } from "@executor/control-plane";
1313 import * as Context from "effect/Context";
1414 import * as Effect from "effect/Effect";
@@ -69,7 +69,7 @@ type RunningServer = { · 54 unmodified lines
6969 };
7070
7171 type ApiServer = RunningServer & {
72− runtime: SqlControlPlaneRuntime;
72+ runtime: ControlPlaneRuntime;
7373 };
7474
7575 type HookHarness<T> = {
@@ -95,7 +95,7 @@ const closeScope = (scope: Scope.CloseableScope) => · 19 unmodified lines
9595
9696 const startControlPlaneServer = async (): Promise<ApiServer> => {
9797 const runtime = await Effect.runPromise(
98− createSqlControlPlaneRuntime({
98+ createControlPlaneRuntime({
9999 localDataDir: ":memory:",
100100 workspaceRoot: mkdtempSync(join(tmpdir(), "executor-react-test-")),
101101 }),
packages/server/src/index.ts+7−7
@@ -11,10 +11,10 @@ import { Readable } from "node:stream"; · 10 unmodified lines
1111 import { HttpApiBuilder, HttpServer } from "@effect/platform";
1212 import {
1313 createControlPlaneApiLayer,
14− createSqlControlPlaneRuntime,
14+ createControlPlaneRuntime,
1515 type ResolveExecutionEnvironment,
1616 type ResolveSecretMaterial,
17− type SqlControlPlaneRuntime,
17+ type ControlPlaneRuntime,
1818 } from "@executor/control-plane";
1919 import { createExecutorMcpRequestHandler } from "@executor/executor-mcp";
2020 import * as Cause from "effect/Cause";
@@ -58,7 +58,7 @@ type StaticUiOptions = { · 37 unmodified lines
5858 };
5959
6060 export type LocalExecutorServer = {
61− readonly runtime: SqlControlPlaneRuntime;
61+ readonly runtime: ControlPlaneRuntime;
6262 readonly port: number;
6363 readonly host: string;
6464 readonly baseUrl: string;
@@ -77,7 +77,7 @@ export type StartLocalExecutorServerOptions = { · 12 unmodified lines
7777 };
7878
7979 export type LocalExecutorRequestHandler = {
80− readonly runtime: SqlControlPlaneRuntime;
80+ readonly runtime: ControlPlaneRuntime;
8181 readonly handleApiRequest: (request: Request) => Promise<Response>;
8282 readonly getBaseUrl: () => string | undefined;
8383 readonly setBaseUrl: (baseUrl: string) => void;
@@ -86,7 +86,7 @@ export type LocalExecutorRequestHandler = { · 2 unmodified lines
8686 type ControlPlaneWebHandler = ReturnType<typeof HttpApiBuilder.toWebHandler>;
8787 type ExecutorMcpHandler = ReturnType<typeof createExecutorMcpRequestHandler>;
8888
89−const disposeRuntime = (runtime: SqlControlPlaneRuntime) =>
89+const disposeRuntime = (runtime: ControlPlaneRuntime) =>
9090 Effect.tryPromise({
9191 try: () => runtime.close(),
9292 catch: (cause) =>
@@ -98,7 +98,7 @@ const createRuntime = ( · 5 unmodified lines
9898 getLocalServerBaseUrl: () => string | undefined,
9999 options: StartLocalExecutorServerOptions,
100100 ) =>
101− createSqlControlPlaneRuntime({
101+ createControlPlaneRuntime({
102102 localDataDir,
103103 migrationsFolder: options.migrationsFolder,
104104 workspaceRoot: options.workspaceRoot,
@@ -209,7 +209,7 @@ const createRuntimeWithLegacyMigration = ( · 104 unmodified lines
209209 );
210210 });
211211
212−const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) =>
212+const createControlPlaneWebHandler = (runtime: ControlPlaneRuntime) =>
213213 Effect.acquireRelease(
214214 Effect.sync(() =>
215215 HttpApiBuilder.toWebHandler(