History

Remove SQL control-plane persistence

Switch the control plane fully to local file-backed state and delete the old SQL persistence layer. This keeps runtime storage aligned with the local-first architecture and removes the legacy migration path.

Rhys Sullivan committed 8 months ago

Commit 08365d91 parent 06df963106 files · +2111 −18820Browse files

Unified · Split

106 files
  1. ARCHITECTURE.md +5 −5
  2. PLAN.md +2 −2
  3. README.md +11 −5
  4. apps/executor/src/cli/main.ts +1 −13
  5. apps/executor/src/cli/runtime-paths.ts +0 −18
  6. packages/control-plane/drizzle.config.ts +0 −13
  7. packages/control-plane/drizzle/20260306091536_abnormal_martin_li/migration.sql +0 −158
  8. packages/control-plane/drizzle/20260306091536_abnormal_martin_li/snapshot.json +0 −1937
  9. packages/control-plane/drizzle/20260306094249_large_giant_girl/migration.sql +0 −39
  10. packages/control-plane/drizzle/20260306094249_large_giant_girl/snapshot.json +0 −2425
  11. packages/control-plane/drizzle/20260307090915_blue_bullseye/migration.sql +0 −70
  12. packages/control-plane/drizzle/20260307090915_blue_bullseye/snapshot.json +0 −3122
  13. packages/control-plane/drizzle/20260307134000_openapi_tool_identity/migration.sql +0 −37
  14. packages/control-plane/drizzle/20260307152000_source_document_text/migration.sql +0 −1
  15. packages/control-plane/drizzle/20260308041000_credentials_model/migration.sql +0 −41
  16. packages/control-plane/drizzle/20260308120000_search_and_indexes/migration.sql +0 −16
  17. packages/control-plane/drizzle/20260308140000_secret_materials_name/migration.sql +0 −1
  18. packages/control-plane/drizzle/20260309103000_policy_scope_and_interaction_purpose/migration.sql +0 −17
  19. packages/control-plane/drizzle/20260310120000_source_recipes_and_actor_credentials/migration.sql +0 −491
  20. packages/control-plane/drizzle/20260310183000_source_recipe_schema_bundles/migration.sql +0 −14
  21. packages/control-plane/drizzle/20260310223000_source_adapter_registry_strings/migration.sql +0 −9
  22. packages/control-plane/drizzle/20260310233000_source_adapter_families_and_data_migrations/migration.sql +0 −23
  23. packages/control-plane/drizzle/20260311010000_source_binding_configs/migration.sql +0 −62
  24. packages/control-plane/drizzle/20260311133000_import_auth_slots_and_materialization_hash/migration.sql +0 −82
  25. packages/control-plane/drizzle/20260311170000_thin_source_recipe_operations/migration.sql +0 −73
  26. packages/control-plane/drizzle/20260311223000_drop_tool_artifacts/migration.sql +0 −7
  27. packages/control-plane/drizzle/20260312013000_adapter_keys_and_versioned_binding_configs/migration.sql +0 −55
  28. packages/control-plane/drizzle/20260312113000_auth_artifacts_and_leases/migration.sql +0 −132
  29. packages/control-plane/drizzle/20260312143000_auth_lease_template_placements/migration.sql +0 −40
  30. packages/control-plane/drizzle/20260312233000_execution_steps_and_private_interaction_responses/migration.sql +0 −25
  31. packages/control-plane/src/api/local/http.ts +5 −5
  32. packages/control-plane/src/index.ts +0 −1
  33. packages/control-plane/src/persistence/client.ts +0 −70
  34. packages/control-plane/src/persistence/code-migrations.ts +0 −234
  35. packages/control-plane/src/persistence/index.ts +0 −182
  36. packages/control-plane/src/persistence/persistence-errors.ts +0 −92
  37. packages/control-plane/src/persistence/post-migrations.test.ts +0 −915
  38. packages/control-plane/src/persistence/repos/accounts-repo.ts +0 −71
  39. packages/control-plane/src/persistence/repos/auth-artifacts-repo.ts +0 −226
  40. packages/control-plane/src/persistence/repos/auth-leases-repo.ts +0 −145
  41. packages/control-plane/src/persistence/repos/code-migrations-repo.ts +0 −41
  42. packages/control-plane/src/persistence/repos/execution-interactions-repo.ts +0 −96
  43. packages/control-plane/src/persistence/repos/execution-steps-repo.ts +0 −89
  44. packages/control-plane/src/persistence/repos/executions-repo.ts +0 −75
  45. packages/control-plane/src/persistence/repos/index.ts +0 −16
  46. packages/control-plane/src/persistence/repos/secret-materials-repo.ts +0 −144
  47. packages/control-plane/src/persistence/repos/shared.ts +0 −126
  48. packages/control-plane/src/persistence/repos/source-auth-sessions-repo.ts +0 −163
  49. packages/control-plane/src/persistence/repos/source-oauth-clients-repo.ts +0 −79
  50. packages/control-plane/src/persistence/repos/source-recipe-documents-repo.ts +0 −85
  51. packages/control-plane/src/persistence/repos/source-recipe-operations-repo.ts +0 −86
  52. packages/control-plane/src/persistence/repos/source-recipe-revisions-repo.ts +0 −157
  53. packages/control-plane/src/persistence/repos/source-recipe-schema-bundles-repo.ts +0 −106
  54. packages/control-plane/src/persistence/repos/source-recipes-repo.ts +0 −67
  55. packages/control-plane/src/persistence/repos/sources-repo.ts +0 −154
  56. packages/control-plane/src/persistence/schema.ts +0 −550
  57. packages/control-plane/src/persistence/sql-runtime.ts +0 −206
  58. packages/control-plane/src/runtime/auth-leases.ts +6 −6
  59. packages/control-plane/src/runtime/control-plane-runtime.test.ts +1 −1
  60. packages/control-plane/src/runtime/execution-replay.test.ts +0 −271
  61. packages/control-plane/src/runtime/executor-tools.ts +21 −13
  62. packages/control-plane/src/runtime/index.ts +90 −102
  63. packages/control-plane/src/runtime/live-execution.test.ts +1 −1
  64. packages/control-plane/src/runtime/live-execution.ts +3 −2
  65. packages/control-plane/src/runtime/local-control-plane-store.ts +1054 −0
  66. packages/control-plane/src/runtime/local-operations.ts +16 −15
  67. packages/control-plane/src/runtime/local-storage.ts +154 −0
  68. packages/control-plane/src/runtime/local-tools.ts +1 −1
  69. packages/control-plane/src/runtime/operation-errors.ts +10 −29
  70. packages/control-plane/src/runtime/operations-shared.ts +4 −17
  71. packages/control-plane/src/runtime/policies-operations.ts +35 −11
  72. packages/control-plane/src/runtime/secret-material-providers.ts +16 −14
  73. packages/control-plane/src/runtime/source-adapters/graphql.ts +0 −71
  74. packages/control-plane/src/runtime/source-adapters/mcp.ts +0 −19
  75. packages/control-plane/src/runtime/source-adapters/openapi.ts +0 −70
  76. packages/control-plane/src/runtime/source-adapters/types.ts +2 −11
  77. packages/control-plane/src/runtime/source-auth-material.test.ts +5 −5
  78. packages/control-plane/src/runtime/source-auth-material.ts +2 −2
  79. packages/control-plane/src/runtime/source-auth-service.test.ts +0 −608
  80. packages/control-plane/src/runtime/source-auth-service.ts +27 −25
  81. packages/control-plane/src/runtime/source-definitions.test.ts +11 −11
  82. packages/control-plane/src/runtime/source-inspection.test.ts +0 −344
  83. packages/control-plane/src/runtime/source-materialization.ts +26 −23
  84. packages/control-plane/src/runtime/source-recipe-support.ts +1 −348
  85. packages/control-plane/src/runtime/source-recipes-runtime.test.ts +0 −858
  86. packages/control-plane/src/runtime/source-recipes-runtime.ts +28 −15
  87. packages/control-plane/src/runtime/source-store.test.ts +0 −310
  88. packages/control-plane/src/runtime/source-store.ts +49 −29
  89. packages/control-plane/src/runtime/store.ts +199 −9
  90. packages/control-plane/src/runtime/workspace-execution-environment.test.ts +0 −1986
  91. packages/control-plane/src/runtime/workspace-execution-environment.ts +164 −266
  92. packages/control-plane/src/schema/models/auth-artifact.ts +25 −31
  93. packages/control-plane/src/schema/models/auth-lease.ts +2 −9
  94. packages/control-plane/src/schema/models/auth/account.ts +12 −17
  95. packages/control-plane/src/schema/models/code-migration.ts +5 −8
  96. packages/control-plane/src/schema/models/execution.ts +46 −55
  97. packages/control-plane/src/schema/models/secret-material.ts +3 −9
  98. packages/control-plane/src/schema/models/source-auth-session.ts +5 −9
  99. packages/control-plane/src/schema/models/source-oauth-client.ts +12 −12
  100. packages/control-plane/src/schema/models/source-recipe.ts +34 −45
  101. packages/control-plane/src/schema/models/source.ts +11 −10
  102. packages/control-plane/tsconfig.json +4 −0
  103. packages/kitchen-sink/src/source-runtime.test.ts +2 −2
  104. packages/react/src/index.test.tsx +0 −1
  105. packages/server/src/config.ts +0 −1
  106. packages/server/src/index.ts +0 −4
ARCHITECTURE.md+5−5
@@ -161,19 +161,19 @@ Because of that, these surfaces share: · 160 unmodified lines
161 161
162## Persistence model162## Persistence model
163 163
164The persistence layer is SQL-backed.164The persistence layer is local-file-backed.
165 165
166Default behavior:166Default behavior:
167 167
168- embedded PGlite for local use168- workspace config and state are stored in local files
169 169
170Optional behavior:170Optional behavior:
171 171
172- Postgres when a Postgres URL is supplied172- future cloud backends can plug in behind Effect service boundaries
173 173
174At a high level, the database stores these domains:174At a high level, the local control plane stores these domains:
175 175
176- installation identity: local installation, accounts, organizations, memberships, workspaces176- installation identity: local installation identity for the workspace
177- source state: sources, auth sessions, source credential bindings177- source state: sources, auth sessions, source credential bindings
178- tool model: tool artifacts and related metadata extracted from sources178- tool model: tool artifacts and related metadata extracted from sources
179- secret state: credentials and secret materials179- secret state: credentials and secret materials
PLAN.md+2−2
@@ -31,7 +31,7 @@ We are on v3 of this codebase which is a completely fresh start, the original co · 30 unmodified lines
31Architecture I like:31Architecture I like:
32 32
33Database:33Database:
34Postgres SQLIte for local, Postgres SQL for cloud hosted version34Local-file-backed control plane for local use, with room for future hosted backends
35 35
36Server:36Server:
37 37
@@ -69,7 +69,7 @@ Runtimes: · 31 unmodified lines
69Other:69Other:
70 70
71- Avoid hardcoding strings in the core libraries, prefer programming to interfaces, this is because it allows people to build custom adapters without having to modify the core71- Avoid hardcoding strings in the core libraries, prefer programming to interfaces, this is because it allows people to build custom adapters without having to modify the core
72- Aim for composability of adapters. For example, the cloud product may be able to leverage storing secrets both encrypted in postgres and also BYO 1password setup. This does not need to be implemented today but worth noting72- Aim for composability of adapters. For example, the cloud product may eventually store secrets in a hosted backend while still supporting BYO 1Password. This does not need to be implemented today but is worth noting.
73- This is a fresh start so we can make whatever changes we need.73- This is a fresh start so we can make whatever changes we need.
74 74
75Rough architecture is: Turborepo monorepo, effect-vitest for testing, bun for package manager / running apps (but we leverage Effect wherever possible rather than bun's apis i.e for the server), Next.js for web app75Rough architecture is: Turborepo monorepo, effect-vitest for testing, bun for package manager / running apps (but we leverage Effect wherever possible rather than bun's apis i.e for the server), Next.js for web app
README.md+11−5
@@ -280,18 +280,16 @@ The server also maintains local PID and log files in its runtime directory. · 279 unmodified lines
280 280
281## Persistence and data281## Persistence and data
282 282
283`executor` persists the local control plane to SQL.283`executor` persists the local control plane to local files.
284
285By default it uses embedded PGlite for a local install, which keeps the product self-contained. If you provide a Postgres URL, the persistence layer can target real Postgres instead.
286 284
287Persisted concepts include:285Persisted concepts include:
288 286
289- the local installation record287- local installation identity
290- organizations and workspaces provisioned for that install
291- connected sources288- connected sources
292- indexed tool artifacts and related metadata289- indexed tool artifacts and related metadata
293- credentials and secret material bindings290- credentials and secret material bindings
294- source auth sessions291- source auth sessions
292- execution and interaction state
295- executions and execution interactions293- executions and execution interactions
296- policies294- policies
297 295
@@ -324,6 +322,14 @@ If you are exploring the repo, these are the directories that matter most: · 26 unmodified lines
324- `packages/executor-mcp`: MCP bridge for `execute` and `resume`322- `packages/executor-mcp`: MCP bridge for `execute` and `resume`
325- `packages/codemode-*`: core tool abstractions plus MCP and OpenAPI adapters323- `packages/codemode-*`: core tool abstractions plus MCP and OpenAPI adapters
326 324
325## Releasing
326
327- `bun run --cwd apps/executor release:publish` is the only supported publish command.
328- Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`.
329- Beta releases use a prerelease semver like `1.3.0-beta.1` and publish to npm under `beta`.
330- Push a matching git tag such as `v1.2.3` or `v1.3.0-beta.1` to trigger `.github/workflows/publish-executor-package.yml`.
331- To build and pack the publish artifact locally without publishing, run `bun run --cwd apps/executor release:publish:dry-run`.
332
327## Project status333## Project status
328 334
329This repository is explicitly on its third major architecture iteration.335This repository is explicitly on its third major architecture iteration.
apps/executor/src/cli/main.ts+1−13
@@ -44,7 +44,6 @@ import { · 43 unmodified lines
44 seedGithubOpenApiSourceInWorkspace,44 seedGithubOpenApiSourceInWorkspace,
45} from "./dev";45} from "./dev";
46import {46import {
47 resolveRuntimeMigrationsDir,
48 resolveRuntimeWebAssetsDir,47 resolveRuntimeWebAssetsDir,
49 resolveSelfCommand,48 resolveSelfCommand,
50} from "./runtime-paths";49} from "./runtime-paths";
@@ -274,9 +273,7 @@ const buildRunWorkflowText = ( · 223 unmodified lines
274 273
275const loadRunWorkflowText = (): Effect.Effect<string, Error, never> =>274const loadRunWorkflowText = (): Effect.Effect<string, Error, never> =>
276 Effect.acquireUseRelease(275 Effect.acquireUseRelease(
277 createControlPlaneRuntime({276 createControlPlaneRuntime({}).pipe(Effect.mapError(toError)),
278 localDataDir: DEFAULT_LOCAL_DATA_DIR,
279 }).pipe(Effect.mapError(toError)),
280 (runtime) =>277 (runtime) =>
281 Effect.gen(function* () {278 Effect.gen(function* () {
282 const environment = yield* Effect.gen(function* () {279 const environment = yield* Effect.gen(function* () {
@@ -414,13 +411,11 @@ const isServerReachable = (baseUrl: string) => · 131 unmodified lines
414 411
415const getDefaultServerOptions = (port: number = DEFAULT_SERVER_PORT) => {412const getDefaultServerOptions = (port: number = DEFAULT_SERVER_PORT) => {
416 const assetsDir = resolveRuntimeWebAssetsDir();413 const assetsDir = resolveRuntimeWebAssetsDir();
417 const migrationsFolder = resolveRuntimeMigrationsDir();
418 414
419 return {415 return {
420 host: DEFAULT_SERVER_HOST,416 host: DEFAULT_SERVER_HOST,
421 port,417 port,
422 localDataDir: DEFAULT_LOCAL_DATA_DIR,418 localDataDir: DEFAULT_LOCAL_DATA_DIR,
423 migrationsFolder: migrationsFolder ?? undefined,
424 pidFile: DEFAULT_SERVER_PID_FILE,419 pidFile: DEFAULT_SERVER_PID_FILE,
425 ui: assetsDir ? { assetsDir } : undefined,420 ui: assetsDir ? { assetsDir } : undefined,
426 };421 };
@@ -533,7 +528,6 @@ type LocalServerStatus = { · 106 unmodified lines
533 logFile: string;528 logFile: string;
534 localDataDir: string;529 localDataDir: string;
535 webAssetsDir: string | null;530 webAssetsDir: string | null;
536 migrationsDir: string | null;
537 installation: {531 installation: {
538 accountId: string;532 accountId: string;
539 workspaceId: string;533 workspaceId: string;
@@ -571,7 +565,6 @@ const getServerStatus = (baseUrl: string): Effect.Effect<LocalServerStatus, Erro · 31 unmodified lines
571 logFile,565 logFile,
572 localDataDir: DEFAULT_LOCAL_DATA_DIR,566 localDataDir: DEFAULT_LOCAL_DATA_DIR,
573 webAssetsDir: resolveRuntimeWebAssetsDir(),567 webAssetsDir: resolveRuntimeWebAssetsDir(),
574 migrationsDir: resolveRuntimeMigrationsDir(),
575 installation,568 installation,
576 denoVersion,569 denoVersion,
577 } satisfies LocalServerStatus;570 } satisfies LocalServerStatus;
@@ -587,7 +580,6 @@ const renderStatus = (status: LocalServerStatus): string => · 9 unmodified lines
587 `logFile: ${status.logFile}`,580 `logFile: ${status.logFile}`,
588 `localDataDir: ${status.localDataDir}`,581 `localDataDir: ${status.localDataDir}`,
589 `webAssetsDir: ${status.webAssetsDir ?? "missing"}`,582 `webAssetsDir: ${status.webAssetsDir ?? "missing"}`,
590 `migrationsDir: ${status.migrationsDir ?? "missing"}`,
591 `workspaceId: ${status.installation?.workspaceId ?? "unavailable"}`,583 `workspaceId: ${status.installation?.workspaceId ?? "unavailable"}`,
592 `denoSandbox: ${renderDenoSandboxDetail(status.denoVersion)}`,584 `denoSandbox: ${renderDenoSandboxDetail(status.denoVersion)}`,
593 ].join("\n");585 ].join("\n");
@@ -616,10 +608,6 @@ const getDoctorReport = (baseUrl: string) => · 22 unmodified lines
616 ok: status.webAssetsDir !== null,608 ok: status.webAssetsDir !== null,
617 detail: status.webAssetsDir ?? "missing bundled web assets",609 detail: status.webAssetsDir ?? "missing bundled web assets",
618 },610 },
619 migrations: {
620 ok: status.migrationsDir !== null,
621 detail: status.migrationsDir ?? "missing migrations directory",
622 },
623 installation: {611 installation: {
624 ok: status.installation !== null,612 ok: status.installation !== null,
625 detail: status.installation613 detail: status.installation
apps/executor/src/cli/runtime-paths.ts+0−18
@@ -3,7 +3,6 @@ import { dirname, extname, resolve } from "node:path"; · 2 unmodified lines
3import { fileURLToPath } from "node:url";3import { fileURLToPath } from "node:url";
4 4
5import {5import {
6 EXECUTOR_MIGRATIONS_DIR_ENV,
7 EXECUTOR_WEB_ASSETS_DIR_ENV,6 EXECUTOR_WEB_ASSETS_DIR_ENV,
8} from "@executor/server";7} from "@executor/server";
9 8
@@ -93,20 +92,3 @@ export const resolveRuntimeWebAssetsDir = (): string | null => { · 83 unmodified lines
93 92
94 return resolveIfExists(resolve(sourceDir, "../../../web/dist"));93 return resolveIfExists(resolve(sourceDir, "../../../web/dist"));
95};94};
96
97export const resolveRuntimeMigrationsDir = (): string | null => {
98 const explicit = resolveIfExists(process.env[EXECUTOR_MIGRATIONS_DIR_ENV]);
99 if (explicit) {
100 return explicit;
101 }
102
103 const resourcesRoot = resolveRuntimeResourcesRoot();
104 const bundled = resourcesRoot
105 ? resolveIfExists(resolve(resourcesRoot, "migrations"))
106 : null;
107 if (bundled) {
108 return bundled;
109 }
110
111 return resolveIfExists(resolve(sourceDir, "../../../../packages/control-plane/drizzle"));
112};
packages/control-plane/drizzle.config.ts+0−13
@@ -1,13 +0,0 @@
1const databaseUrl =
2 process.env.DATABASE_URL ?? "postgres://localhost:5432/executor_v3";
3
4export default {
5 dialect: "postgresql",
6 schema: "./src/persistence/schema.ts",
7 out: "./drizzle",
8 dbCredentials: {
9 url: databaseUrl,
10 },
11 strict: true,
12 verbose: true,
13};
packages/control-plane/drizzle/20260306091536_abnormal_martin_li/migration.sql+0−158
@@ -1,158 +0,0 @@
1CREATE TABLE "accounts" (
2 "id" text PRIMARY KEY,
3 "provider" text NOT NULL,
4 "subject" text NOT NULL,
5 "email" text,
6 "display_name" text,
7 "created_at" bigint NOT NULL,
8 "updated_at" bigint NOT NULL,
9 CONSTRAINT "accounts_provider_check" CHECK ("provider" in ('local', 'workos', 'service'))
10);
11--> statement-breakpoint
12CREATE TABLE "execution_interactions" (
13 "id" text PRIMARY KEY,
14 "execution_id" text NOT NULL,
15 "status" text NOT NULL,
16 "kind" text NOT NULL,
17 "payload_json" text NOT NULL,
18 "response_json" text,
19 "created_at" bigint NOT NULL,
20 "updated_at" bigint NOT NULL,
21 CONSTRAINT "execution_interactions_status_check" CHECK ("status" in ('pending', 'resolved', 'cancelled'))
22);
23--> statement-breakpoint
24CREATE TABLE "executions" (
25 "id" text PRIMARY KEY,
26 "workspace_id" text NOT NULL,
27 "created_by_account_id" text NOT NULL,
28 "status" text NOT NULL,
29 "code" text NOT NULL,
30 "result_json" text,
31 "error_text" text,
32 "logs_json" text,
33 "started_at" bigint,
34 "completed_at" bigint,
35 "created_at" bigint NOT NULL,
36 "updated_at" bigint NOT NULL,
37 CONSTRAINT "executions_status_check" CHECK ("status" in ('pending', 'running', 'waiting_for_interaction', 'completed', 'failed', 'cancelled'))
38);
39--> statement-breakpoint
40CREATE TABLE "local_installations" (
41 "id" text PRIMARY KEY,
42 "account_id" text NOT NULL,
43 "organization_id" text NOT NULL,
44 "workspace_id" text NOT NULL,
45 "created_at" bigint NOT NULL,
46 "updated_at" bigint NOT NULL
47);
48--> statement-breakpoint
49CREATE TABLE "organization_memberships" (
50 "id" text PRIMARY KEY,
51 "organization_id" text NOT NULL,
52 "account_id" text NOT NULL,
53 "role" text NOT NULL,
54 "status" text NOT NULL,
55 "billable" boolean NOT NULL,
56 "invited_by_account_id" text,
57 "joined_at" bigint,
58 "created_at" bigint NOT NULL,
59 "updated_at" bigint NOT NULL,
60 CONSTRAINT "organization_memberships_role_check" CHECK ("role" in ('viewer', 'editor', 'admin', 'owner')),
61 CONSTRAINT "organization_memberships_status_check" CHECK ("status" in ('invited', 'active', 'suspended', 'removed'))
62);
63--> statement-breakpoint
64CREATE TABLE "organizations" (
65 "id" text PRIMARY KEY,
66 "slug" text NOT NULL,
67 "name" text NOT NULL,
68 "status" text NOT NULL,
69 "created_by_account_id" text,
70 "created_at" bigint NOT NULL,
71 "updated_at" bigint NOT NULL,
72 CONSTRAINT "organizations_status_check" CHECK ("status" in ('active', 'suspended', 'archived'))
73);
74--> statement-breakpoint
75CREATE TABLE "policies" (
76 "id" text PRIMARY KEY,
77 "workspace_id" text NOT NULL,
78 "target_account_id" text,
79 "client_id" text,
80 "resource_type" text NOT NULL,
81 "resource_pattern" text NOT NULL,
82 "match_type" text NOT NULL,
83 "effect" text NOT NULL,
84 "approval_mode" text NOT NULL,
85 "argument_conditions_json" text,
86 "priority" bigint NOT NULL,
87 "enabled" boolean NOT NULL,
88 "created_at" bigint NOT NULL,
89 "updated_at" bigint NOT NULL,
90 CONSTRAINT "policies_resource_type_check" CHECK ("resource_type" in ('all_tools', 'source', 'namespace', 'tool_path')),
91 CONSTRAINT "policies_match_type_check" CHECK ("match_type" in ('glob', 'exact')),
92 CONSTRAINT "policies_effect_check" CHECK ("effect" in ('allow', 'deny')),
93 CONSTRAINT "policies_approval_mode_check" CHECK ("approval_mode" in ('auto', 'required'))
94);
95--> statement-breakpoint
96CREATE TABLE "source_credential_bindings" (
97 "workspace_id" text,
98 "source_id" text,
99 "token_provider_id" text,
100 "token_handle" text,
101 "refresh_token_provider_id" text,
102 "refresh_token_handle" text,
103 "created_at" bigint NOT NULL,
104 "updated_at" bigint NOT NULL,
105 CONSTRAINT "source_credential_bindings_pkey" PRIMARY KEY("workspace_id","source_id")
106);
107--> statement-breakpoint
108CREATE TABLE "sources" (
109 "workspace_id" text,
110 "source_id" text,
111 "name" text NOT NULL,
112 "kind" text NOT NULL,
113 "endpoint" text NOT NULL,
114 "status" text NOT NULL,
115 "enabled" boolean NOT NULL,
116 "namespace" text,
117 "transport" text,
118 "query_params_json" text,
119 "headers_json" text,
120 "spec_url" text,
121 "default_headers_json" text,
122 "auth_kind" text NOT NULL,
123 "auth_header_name" text,
124 "auth_prefix" text,
125 "source_hash" text,
126 "last_error" text,
127 "created_at" bigint NOT NULL,
128 "updated_at" bigint NOT NULL,
129 CONSTRAINT "sources_pkey" PRIMARY KEY("workspace_id","source_id"),
130 CONSTRAINT "sources_kind_check" CHECK ("kind" in ('mcp', 'openapi', 'graphql', 'internal')),
131 CONSTRAINT "sources_status_check" CHECK ("status" in ('draft', 'probing', 'auth_required', 'connected', 'error')),
132 CONSTRAINT "sources_transport_check" CHECK ("transport" is null or "transport" in ('auto', 'streamable-http', 'sse')),
133 CONSTRAINT "sources_auth_kind_check" CHECK ("auth_kind" in ('none', 'bearer', 'oauth2'))
134);
135--> statement-breakpoint
136CREATE TABLE "workspaces" (
137 "id" text PRIMARY KEY,
138 "organization_id" text NOT NULL,
139 "name" text NOT NULL,
140 "created_by_account_id" text,
141 "created_at" bigint NOT NULL,
142 "updated_at" bigint NOT NULL
143);
144--> statement-breakpoint
145CREATE UNIQUE INDEX "accounts_provider_subject_idx" ON "accounts" ("provider","subject");--> statement-breakpoint
146CREATE INDEX "accounts_updated_idx" ON "accounts" ("updated_at","id");--> statement-breakpoint
147CREATE INDEX "execution_interactions_execution_idx" ON "execution_interactions" ("execution_id","updated_at","id");--> statement-breakpoint
148CREATE INDEX "executions_workspace_idx" ON "executions" ("workspace_id","updated_at","id");--> statement-breakpoint
149CREATE INDEX "organization_memberships_org_idx" ON "organization_memberships" ("organization_id");--> statement-breakpoint
150CREATE INDEX "organization_memberships_account_idx" ON "organization_memberships" ("account_id");--> statement-breakpoint
151CREATE UNIQUE INDEX "organization_memberships_org_account_idx" ON "organization_memberships" ("organization_id","account_id");--> statement-breakpoint
152CREATE UNIQUE INDEX "organizations_slug_idx" ON "organizations" ("slug");--> statement-breakpoint
153CREATE INDEX "organizations_updated_idx" ON "organizations" ("updated_at","id");--> statement-breakpoint
154CREATE INDEX "policies_workspace_idx" ON "policies" ("workspace_id","updated_at","id");--> statement-breakpoint
155CREATE INDEX "source_credential_bindings_workspace_idx" ON "source_credential_bindings" ("workspace_id","updated_at","source_id");--> statement-breakpoint
156CREATE UNIQUE INDEX "sources_workspace_name_idx" ON "sources" ("workspace_id","name");--> statement-breakpoint
157CREATE INDEX "workspaces_org_idx" ON "workspaces" ("organization_id");--> statement-breakpoint
158CREATE UNIQUE INDEX "workspaces_org_name_idx" ON "workspaces" ("organization_id","name");
packages/control-plane/drizzle/20260306091536_abnormal_martin_li/snapshot.json+0−1937
@@ -1,1937 +0,0 @@
1{
2 "version": "8",
3 "dialect": "postgres",
4 "id": "bac1651d-7a30-4187-a7e2-74bc2b099486",
5 "prevIds": [
6 "00000000-0000-0000-0000-000000000000"
7 ],
8 "ddl": [
9 {
10 "isRlsEnabled": false,
11 "name": "accounts",
12 "entityType": "tables",
13 "schema": "public"
14 },
15 {
16 "isRlsEnabled": false,
17 "name": "execution_interactions",
18 "entityType": "tables",
19 "schema": "public"
20 },
21 {
22 "isRlsEnabled": false,
23 "name": "executions",
24 "entityType": "tables",
25 "schema": "public"
26 },
27 {
28 "isRlsEnabled": false,
29 "name": "local_installations",
30 "entityType": "tables",
31 "schema": "public"
32 },
33 {
34 "isRlsEnabled": false,
35 "name": "organization_memberships",
36 "entityType": "tables",
37 "schema": "public"
38 },
39 {
40 "isRlsEnabled": false,
41 "name": "organizations",
42 "entityType": "tables",
43 "schema": "public"
44 },
45 {
46 "isRlsEnabled": false,
47 "name": "policies",
48 "entityType": "tables",
49 "schema": "public"
50 },
51 {
52 "isRlsEnabled": false,
53 "name": "source_credential_bindings",
54 "entityType": "tables",
55 "schema": "public"
56 },
57 {
58 "isRlsEnabled": false,
59 "name": "sources",
60 "entityType": "tables",
61 "schema": "public"
62 },
63 {
64 "isRlsEnabled": false,
65 "name": "workspaces",
66 "entityType": "tables",
67 "schema": "public"
68 },
69 {
70 "type": "text",
71 "typeSchema": null,
72 "notNull": true,
73 "dimensions": 0,
74 "default": null,
75 "generated": null,
76 "identity": null,
77 "name": "id",
78 "entityType": "columns",
79 "schema": "public",
80 "table": "accounts"
81 },
82 {
83 "type": "text",
84 "typeSchema": null,
85 "notNull": true,
86 "dimensions": 0,
87 "default": null,
88 "generated": null,
89 "identity": null,
90 "name": "provider",
91 "entityType": "columns",
92 "schema": "public",
93 "table": "accounts"
94 },
95 {
96 "type": "text",
97 "typeSchema": null,
98 "notNull": true,
99 "dimensions": 0,
100 "default": null,
101 "generated": null,
102 "identity": null,
103 "name": "subject",
104 "entityType": "columns",
105 "schema": "public",
106 "table": "accounts"
107 },
108 {
109 "type": "text",
110 "typeSchema": null,
111 "notNull": false,
112 "dimensions": 0,
113 "default": null,
114 "generated": null,
115 "identity": null,
116 "name": "email",
117 "entityType": "columns",
118 "schema": "public",
119 "table": "accounts"
120 },
121 {
122 "type": "text",
123 "typeSchema": null,
124 "notNull": false,
125 "dimensions": 0,
126 "default": null,
127 "generated": null,
128 "identity": null,
129 "name": "display_name",
130 "entityType": "columns",
131 "schema": "public",
132 "table": "accounts"
133 },
134 {
135 "type": "bigint",
136 "typeSchema": null,
137 "notNull": true,
138 "dimensions": 0,
139 "default": null,
140 "generated": null,
141 "identity": null,
142 "name": "created_at",
143 "entityType": "columns",
144 "schema": "public",
145 "table": "accounts"
146 },
147 {
148 "type": "bigint",
149 "typeSchema": null,
150 "notNull": true,
151 "dimensions": 0,
152 "default": null,
153 "generated": null,
154 "identity": null,
155 "name": "updated_at",
156 "entityType": "columns",
157 "schema": "public",
158 "table": "accounts"
159 },
160 {
161 "type": "text",
162 "typeSchema": null,
163 "notNull": true,
164 "dimensions": 0,
165 "default": null,
166 "generated": null,
167 "identity": null,
168 "name": "id",
169 "entityType": "columns",
170 "schema": "public",
171 "table": "execution_interactions"
172 },
173 {
174 "type": "text",
175 "typeSchema": null,
176 "notNull": true,
177 "dimensions": 0,
178 "default": null,
179 "generated": null,
180 "identity": null,
181 "name": "execution_id",
182 "entityType": "columns",
183 "schema": "public",
184 "table": "execution_interactions"
185 },
186 {
187 "type": "text",
188 "typeSchema": null,
189 "notNull": true,
190 "dimensions": 0,
191 "default": null,
192 "generated": null,
193 "identity": null,
194 "name": "status",
195 "entityType": "columns",
196 "schema": "public",
197 "table": "execution_interactions"
198 },
199 {
200 "type": "text",
201 "typeSchema": null,
202 "notNull": true,
203 "dimensions": 0,
204 "default": null,
205 "generated": null,
206 "identity": null,
207 "name": "kind",
208 "entityType": "columns",
209 "schema": "public",
210 "table": "execution_interactions"
211 },
212 {
213 "type": "text",
214 "typeSchema": null,
215 "notNull": true,
216 "dimensions": 0,
217 "default": null,
218 "generated": null,
219 "identity": null,
220 "name": "payload_json",
221 "entityType": "columns",
222 "schema": "public",
223 "table": "execution_interactions"
224 },
225 {
226 "type": "text",
227 "typeSchema": null,
228 "notNull": false,
229 "dimensions": 0,
230 "default": null,
231 "generated": null,
232 "identity": null,
233 "name": "response_json",
234 "entityType": "columns",
235 "schema": "public",
236 "table": "execution_interactions"
237 },
238 {
239 "type": "bigint",
240 "typeSchema": null,
241 "notNull": true,
242 "dimensions": 0,
243 "default": null,
244 "generated": null,
245 "identity": null,
246 "name": "created_at",
247 "entityType": "columns",
248 "schema": "public",
249 "table": "execution_interactions"
250 },
251 {
252 "type": "bigint",
253 "typeSchema": null,
254 "notNull": true,
255 "dimensions": 0,
256 "default": null,
257 "generated": null,
258 "identity": null,
259 "name": "updated_at",
260 "entityType": "columns",
261 "schema": "public",
262 "table": "execution_interactions"
263 },
264 {
265 "type": "text",
266 "typeSchema": null,
267 "notNull": true,
268 "dimensions": 0,
269 "default": null,
270 "generated": null,
271 "identity": null,
272 "name": "id",
273 "entityType": "columns",
274 "schema": "public",
275 "table": "executions"
276 },
277 {
278 "type": "text",
279 "typeSchema": null,
280 "notNull": true,
281 "dimensions": 0,
282 "default": null,
283 "generated": null,
284 "identity": null,
285 "name": "workspace_id",
286 "entityType": "columns",
287 "schema": "public",
288 "table": "executions"
289 },
290 {
291 "type": "text",
292 "typeSchema": null,
293 "notNull": true,
294 "dimensions": 0,
295 "default": null,
296 "generated": null,
297 "identity": null,
298 "name": "created_by_account_id",
299 "entityType": "columns",
300 "schema": "public",
301 "table": "executions"
302 },
303 {
304 "type": "text",
305 "typeSchema": null,
306 "notNull": true,
307 "dimensions": 0,
308 "default": null,
309 "generated": null,
310 "identity": null,
311 "name": "status",
312 "entityType": "columns",
313 "schema": "public",
314 "table": "executions"
315 },
316 {
317 "type": "text",
318 "typeSchema": null,
319 "notNull": true,
320 "dimensions": 0,
321 "default": null,
322 "generated": null,
323 "identity": null,
324 "name": "code",
325 "entityType": "columns",
326 "schema": "public",
327 "table": "executions"
328 },
329 {
330 "type": "text",
331 "typeSchema": null,
332 "notNull": false,
333 "dimensions": 0,
334 "default": null,
335 "generated": null,
336 "identity": null,
337 "name": "result_json",
338 "entityType": "columns",
339 "schema": "public",
340 "table": "executions"
341 },
342 {
343 "type": "text",
344 "typeSchema": null,
345 "notNull": false,
346 "dimensions": 0,
347 "default": null,
348 "generated": null,
349 "identity": null,
350 "name": "error_text",
351 "entityType": "columns",
352 "schema": "public",
353 "table": "executions"
354 },
355 {
356 "type": "text",
357 "typeSchema": null,
358 "notNull": false,
359 "dimensions": 0,
360 "default": null,
361 "generated": null,
362 "identity": null,
363 "name": "logs_json",
364 "entityType": "columns",
365 "schema": "public",
366 "table": "executions"
367 },
368 {
369 "type": "bigint",
370 "typeSchema": null,
371 "notNull": false,
372 "dimensions": 0,
373 "default": null,
374 "generated": null,
375 "identity": null,
376 "name": "started_at",
377 "entityType": "columns",
378 "schema": "public",
379 "table": "executions"
380 },
381 {
382 "type": "bigint",
383 "typeSchema": null,
384 "notNull": false,
385 "dimensions": 0,
386 "default": null,
387 "generated": null,
388 "identity": null,
389 "name": "completed_at",
390 "entityType": "columns",
391 "schema": "public",
392 "table": "executions"
393 },
394 {
395 "type": "bigint",
396 "typeSchema": null,
397 "notNull": true,
398 "dimensions": 0,
399 "default": null,
400 "generated": null,
401 "identity": null,
402 "name": "created_at",
403 "entityType": "columns",
404 "schema": "public",
405 "table": "executions"
406 },
407 {
408 "type": "bigint",
409 "typeSchema": null,
410 "notNull": true,
411 "dimensions": 0,
412 "default": null,
413 "generated": null,
414 "identity": null,
415 "name": "updated_at",
416 "entityType": "columns",
417 "schema": "public",
418 "table": "executions"
419 },
420 {
421 "type": "text",
422 "typeSchema": null,
423 "notNull": true,
424 "dimensions": 0,
425 "default": null,
426 "generated": null,
427 "identity": null,
428 "name": "id",
429 "entityType": "columns",
430 "schema": "public",
431 "table": "local_installations"
432 },
433 {
434 "type": "text",
435 "typeSchema": null,
436 "notNull": true,
437 "dimensions": 0,
438 "default": null,
439 "generated": null,
440 "identity": null,
441 "name": "account_id",
442 "entityType": "columns",
443 "schema": "public",
444 "table": "local_installations"
445 },
446 {
447 "type": "text",
448 "typeSchema": null,
449 "notNull": true,
450 "dimensions": 0,
451 "default": null,
452 "generated": null,
453 "identity": null,
454 "name": "organization_id",
455 "entityType": "columns",
456 "schema": "public",
457 "table": "local_installations"
458 },
459 {
460 "type": "text",
461 "typeSchema": null,
462 "notNull": true,
463 "dimensions": 0,
464 "default": null,
465 "generated": null,
466 "identity": null,
467 "name": "workspace_id",
468 "entityType": "columns",
469 "schema": "public",
470 "table": "local_installations"
471 },
472 {
473 "type": "bigint",
474 "typeSchema": null,
475 "notNull": true,
476 "dimensions": 0,
477 "default": null,
478 "generated": null,
479 "identity": null,
480 "name": "created_at",
481 "entityType": "columns",
482 "schema": "public",
483 "table": "local_installations"
484 },
485 {
486 "type": "bigint",
487 "typeSchema": null,
488 "notNull": true,
489 "dimensions": 0,
490 "default": null,
491 "generated": null,
492 "identity": null,
493 "name": "updated_at",
494 "entityType": "columns",
495 "schema": "public",
496 "table": "local_installations"
497 },
498 {
499 "type": "text",
500 "typeSchema": null,
501 "notNull": true,
502 "dimensions": 0,
503 "default": null,
504 "generated": null,
505 "identity": null,
506 "name": "id",
507 "entityType": "columns",
508 "schema": "public",
509 "table": "organization_memberships"
510 },
511 {
512 "type": "text",
513 "typeSchema": null,
514 "notNull": true,
515 "dimensions": 0,
516 "default": null,
517 "generated": null,
518 "identity": null,
519 "name": "organization_id",
520 "entityType": "columns",
521 "schema": "public",
522 "table": "organization_memberships"
523 },
524 {
525 "type": "text",
526 "typeSchema": null,
527 "notNull": true,
528 "dimensions": 0,
529 "default": null,
530 "generated": null,
531 "identity": null,
532 "name": "account_id",
533 "entityType": "columns",
534 "schema": "public",
535 "table": "organization_memberships"
536 },
537 {
538 "type": "text",
539 "typeSchema": null,
540 "notNull": true,
541 "dimensions": 0,
542 "default": null,
543 "generated": null,
544 "identity": null,
545 "name": "role",
546 "entityType": "columns",
547 "schema": "public",
548 "table": "organization_memberships"
549 },
550 {
551 "type": "text",
552 "typeSchema": null,
553 "notNull": true,
554 "dimensions": 0,
555 "default": null,
556 "generated": null,
557 "identity": null,
558 "name": "status",
559 "entityType": "columns",
560 "schema": "public",
561 "table": "organization_memberships"
562 },
563 {
564 "type": "boolean",
565 "typeSchema": null,
566 "notNull": true,
567 "dimensions": 0,
568 "default": null,
569 "generated": null,
570 "identity": null,
571 "name": "billable",
572 "entityType": "columns",
573 "schema": "public",
574 "table": "organization_memberships"
575 },
576 {
577 "type": "text",
578 "typeSchema": null,
579 "notNull": false,
580 "dimensions": 0,
581 "default": null,
582 "generated": null,
583 "identity": null,
584 "name": "invited_by_account_id",
585 "entityType": "columns",
586 "schema": "public",
587 "table": "organization_memberships"
588 },
589 {
590 "type": "bigint",
591 "typeSchema": null,
592 "notNull": false,
593 "dimensions": 0,
594 "default": null,
595 "generated": null,
596 "identity": null,
597 "name": "joined_at",
598 "entityType": "columns",
599 "schema": "public",
600 "table": "organization_memberships"
601 },
602 {
603 "type": "bigint",
604 "typeSchema": null,
605 "notNull": true,
606 "dimensions": 0,
607 "default": null,
608 "generated": null,
609 "identity": null,
610 "name": "created_at",
611 "entityType": "columns",
612 "schema": "public",
613 "table": "organization_memberships"
614 },
615 {
616 "type": "bigint",
617 "typeSchema": null,
618 "notNull": true,
619 "dimensions": 0,
620 "default": null,
621 "generated": null,
622 "identity": null,
623 "name": "updated_at",
624 "entityType": "columns",
625 "schema": "public",
626 "table": "organization_memberships"
627 },
628 {
629 "type": "text",
630 "typeSchema": null,
631 "notNull": true,
632 "dimensions": 0,
633 "default": null,
634 "generated": null,
635 "identity": null,
636 "name": "id",
637 "entityType": "columns",
638 "schema": "public",
639 "table": "organizations"
640 },
641 {
642 "type": "text",
643 "typeSchema": null,
644 "notNull": true,
645 "dimensions": 0,
646 "default": null,
647 "generated": null,
648 "identity": null,
649 "name": "slug",
650 "entityType": "columns",
651 "schema": "public",
652 "table": "organizations"
653 },
654 {
655 "type": "text",
656 "typeSchema": null,
657 "notNull": true,
658 "dimensions": 0,
659 "default": null,
660 "generated": null,
661 "identity": null,
662 "name": "name",
663 "entityType": "columns",
664 "schema": "public",
665 "table": "organizations"
666 },
667 {
668 "type": "text",
669 "typeSchema": null,
670 "notNull": true,
671 "dimensions": 0,
672 "default": null,
673 "generated": null,
674 "identity": null,
675 "name": "status",
676 "entityType": "columns",
677 "schema": "public",
678 "table": "organizations"
679 },
680 {
681 "type": "text",
682 "typeSchema": null,
683 "notNull": false,
684 "dimensions": 0,
685 "default": null,
686 "generated": null,
687 "identity": null,
688 "name": "created_by_account_id",
689 "entityType": "columns",
690 "schema": "public",
691 "table": "organizations"
692 },
693 {
694 "type": "bigint",
695 "typeSchema": null,
696 "notNull": true,
697 "dimensions": 0,
698 "default": null,
699 "generated": null,
700 "identity": null,
701 "name": "created_at",
702 "entityType": "columns",
703 "schema": "public",
704 "table": "organizations"
705 },
706 {
707 "type": "bigint",
708 "typeSchema": null,
709 "notNull": true,
710 "dimensions": 0,
711 "default": null,
712 "generated": null,
713 "identity": null,
714 "name": "updated_at",
715 "entityType": "columns",
716 "schema": "public",
717 "table": "organizations"
718 },
719 {
720 "type": "text",
721 "typeSchema": null,
722 "notNull": true,
723 "dimensions": 0,
724 "default": null,
725 "generated": null,
726 "identity": null,
727 "name": "id",
728 "entityType": "columns",
729 "schema": "public",
730 "table": "policies"
731 },
732 {
733 "type": "text",
734 "typeSchema": null,
735 "notNull": true,
736 "dimensions": 0,
737 "default": null,
738 "generated": null,
739 "identity": null,
740 "name": "workspace_id",
741 "entityType": "columns",
742 "schema": "public",
743 "table": "policies"
744 },
745 {
746 "type": "text",
747 "typeSchema": null,
748 "notNull": false,
749 "dimensions": 0,
750 "default": null,
751 "generated": null,
752 "identity": null,
753 "name": "target_account_id",
754 "entityType": "columns",
755 "schema": "public",
756 "table": "policies"
757 },
758 {
759 "type": "text",
760 "typeSchema": null,
761 "notNull": false,
762 "dimensions": 0,
763 "default": null,
764 "generated": null,
765 "identity": null,
766 "name": "client_id",
767 "entityType": "columns",
768 "schema": "public",
769 "table": "policies"
770 },
771 {
772 "type": "text",
773 "typeSchema": null,
774 "notNull": true,
775 "dimensions": 0,
776 "default": null,
777 "generated": null,
778 "identity": null,
779 "name": "resource_type",
780 "entityType": "columns",
781 "schema": "public",
782 "table": "policies"
783 },
784 {
785 "type": "text",
786 "typeSchema": null,
787 "notNull": true,
788 "dimensions": 0,
789 "default": null,
790 "generated": null,
791 "identity": null,
792 "name": "resource_pattern",
793 "entityType": "columns",
794 "schema": "public",
795 "table": "policies"
796 },
797 {
798 "type": "text",
799 "typeSchema": null,
800 "notNull": true,
801 "dimensions": 0,
802 "default": null,
803 "generated": null,
804 "identity": null,
805 "name": "match_type",
806 "entityType": "columns",
807 "schema": "public",
808 "table": "policies"
809 },
810 {
811 "type": "text",
812 "typeSchema": null,
813 "notNull": true,
814 "dimensions": 0,
815 "default": null,
816 "generated": null,
817 "identity": null,
818 "name": "effect",
819 "entityType": "columns",
820 "schema": "public",
821 "table": "policies"
822 },
823 {
824 "type": "text",
825 "typeSchema": null,
826 "notNull": true,
827 "dimensions": 0,
828 "default": null,
829 "generated": null,
830 "identity": null,
831 "name": "approval_mode",
832 "entityType": "columns",
833 "schema": "public",
834 "table": "policies"
835 },
836 {
837 "type": "text",
838 "typeSchema": null,
839 "notNull": false,
840 "dimensions": 0,
841 "default": null,
842 "generated": null,
843 "identity": null,
844 "name": "argument_conditions_json",
845 "entityType": "columns",
846 "schema": "public",
847 "table": "policies"
848 },
849 {
850 "type": "bigint",
851 "typeSchema": null,
852 "notNull": true,
853 "dimensions": 0,
854 "default": null,
855 "generated": null,
856 "identity": null,
857 "name": "priority",
858 "entityType": "columns",
859 "schema": "public",
860 "table": "policies"
861 },
862 {
863 "type": "boolean",
864 "typeSchema": null,
865 "notNull": true,
866 "dimensions": 0,
867 "default": null,
868 "generated": null,
869 "identity": null,
870 "name": "enabled",
871 "entityType": "columns",
872 "schema": "public",
873 "table": "policies"
874 },
875 {
876 "type": "bigint",
877 "typeSchema": null,
878 "notNull": true,
879 "dimensions": 0,
880 "default": null,
881 "generated": null,
882 "identity": null,
883 "name": "created_at",
884 "entityType": "columns",
885 "schema": "public",
886 "table": "policies"
887 },
888 {
889 "type": "bigint",
890 "typeSchema": null,
891 "notNull": true,
892 "dimensions": 0,
893 "default": null,
894 "generated": null,
895 "identity": null,
896 "name": "updated_at",
897 "entityType": "columns",
898 "schema": "public",
899 "table": "policies"
900 },
901 {
902 "type": "text",
903 "typeSchema": null,
904 "notNull": true,
905 "dimensions": 0,
906 "default": null,
907 "generated": null,
908 "identity": null,
909 "name": "workspace_id",
910 "entityType": "columns",
911 "schema": "public",
912 "table": "source_credential_bindings"
913 },
914 {
915 "type": "text",
916 "typeSchema": null,
917 "notNull": true,
918 "dimensions": 0,
919 "default": null,
920 "generated": null,
921 "identity": null,
922 "name": "source_id",
923 "entityType": "columns",
924 "schema": "public",
925 "table": "source_credential_bindings"
926 },
927 {
928 "type": "text",
929 "typeSchema": null,
930 "notNull": false,
931 "dimensions": 0,
932 "default": null,
933 "generated": null,
934 "identity": null,
935 "name": "token_provider_id",
936 "entityType": "columns",
937 "schema": "public",
938 "table": "source_credential_bindings"
939 },
940 {
941 "type": "text",
942 "typeSchema": null,
943 "notNull": false,
944 "dimensions": 0,
945 "default": null,
946 "generated": null,
947 "identity": null,
948 "name": "token_handle",
949 "entityType": "columns",
950 "schema": "public",
951 "table": "source_credential_bindings"
952 },
953 {
954 "type": "text",
955 "typeSchema": null,
956 "notNull": false,
957 "dimensions": 0,
958 "default": null,
959 "generated": null,
960 "identity": null,
961 "name": "refresh_token_provider_id",
962 "entityType": "columns",
963 "schema": "public",
964 "table": "source_credential_bindings"
965 },
966 {
967 "type": "text",
968 "typeSchema": null,
969 "notNull": false,
970 "dimensions": 0,
971 "default": null,
972 "generated": null,
973 "identity": null,
974 "name": "refresh_token_handle",
975 "entityType": "columns",
976 "schema": "public",
977 "table": "source_credential_bindings"
978 },
979 {
980 "type": "bigint",
981 "typeSchema": null,
982 "notNull": true,
983 "dimensions": 0,
984 "default": null,
985 "generated": null,
986 "identity": null,
987 "name": "created_at",
988 "entityType": "columns",
989 "schema": "public",
990 "table": "source_credential_bindings"
991 },
992 {
993 "type": "bigint",
994 "typeSchema": null,
995 "notNull": true,
996 "dimensions": 0,
997 "default": null,
998 "generated": null,
999 "identity": null,
1000 "name": "updated_at",
1001 "entityType": "columns",
1002 "schema": "public",
1003 "table": "source_credential_bindings"
1004 },
1005 {
1006 "type": "text",
1007 "typeSchema": null,
1008 "notNull": true,
1009 "dimensions": 0,
1010 "default": null,
1011 "generated": null,
1012 "identity": null,
1013 "name": "workspace_id",
1014 "entityType": "columns",
1015 "schema": "public",
1016 "table": "sources"
1017 },
1018 {
1019 "type": "text",
1020 "typeSchema": null,
1021 "notNull": true,
1022 "dimensions": 0,
1023 "default": null,
1024 "generated": null,
1025 "identity": null,
1026 "name": "source_id",
1027 "entityType": "columns",
1028 "schema": "public",
1029 "table": "sources"
1030 },
1031 {
1032 "type": "text",
1033 "typeSchema": null,
1034 "notNull": true,
1035 "dimensions": 0,
1036 "default": null,
1037 "generated": null,
1038 "identity": null,
1039 "name": "name",
1040 "entityType": "columns",
1041 "schema": "public",
1042 "table": "sources"
1043 },
1044 {
1045 "type": "text",
1046 "typeSchema": null,
1047 "notNull": true,
1048 "dimensions": 0,
1049 "default": null,
1050 "generated": null,
1051 "identity": null,
1052 "name": "kind",
1053 "entityType": "columns",
1054 "schema": "public",
1055 "table": "sources"
1056 },
1057 {
1058 "type": "text",
1059 "typeSchema": null,
1060 "notNull": true,
1061 "dimensions": 0,
1062 "default": null,
1063 "generated": null,
1064 "identity": null,
1065 "name": "endpoint",
1066 "entityType": "columns",
1067 "schema": "public",
1068 "table": "sources"
1069 },
1070 {
1071 "type": "text",
1072 "typeSchema": null,
1073 "notNull": true,
1074 "dimensions": 0,
1075 "default": null,
1076 "generated": null,
1077 "identity": null,
1078 "name": "status",
1079 "entityType": "columns",
1080 "schema": "public",
1081 "table": "sources"
1082 },
1083 {
1084 "type": "boolean",
1085 "typeSchema": null,
1086 "notNull": true,
1087 "dimensions": 0,
1088 "default": null,
1089 "generated": null,
1090 "identity": null,
1091 "name": "enabled",
1092 "entityType": "columns",
1093 "schema": "public",
1094 "table": "sources"
1095 },
1096 {
1097 "type": "text",
1098 "typeSchema": null,
1099 "notNull": false,
1100 "dimensions": 0,
1101 "default": null,
1102 "generated": null,
1103 "identity": null,
1104 "name": "namespace",
1105 "entityType": "columns",
1106 "schema": "public",
1107 "table": "sources"
1108 },
1109 {
1110 "type": "text",
1111 "typeSchema": null,
1112 "notNull": false,
1113 "dimensions": 0,
1114 "default": null,
1115 "generated": null,
1116 "identity": null,
1117 "name": "transport",
1118 "entityType": "columns",
1119 "schema": "public",
1120 "table": "sources"
1121 },
1122 {
1123 "type": "text",
1124 "typeSchema": null,
1125 "notNull": false,
1126 "dimensions": 0,
1127 "default": null,
1128 "generated": null,
1129 "identity": null,
1130 "name": "query_params_json",
1131 "entityType": "columns",
1132 "schema": "public",
1133 "table": "sources"
1134 },
1135 {
1136 "type": "text",
1137 "typeSchema": null,
1138 "notNull": false,
1139 "dimensions": 0,
1140 "default": null,
1141 "generated": null,
1142 "identity": null,
1143 "name": "headers_json",
1144 "entityType": "columns",
1145 "schema": "public",
1146 "table": "sources"
1147 },
1148 {
1149 "type": "text",
1150 "typeSchema": null,
1151 "notNull": false,
1152 "dimensions": 0,
1153 "default": null,
1154 "generated": null,
1155 "identity": null,
1156 "name": "spec_url",
1157 "entityType": "columns",
1158 "schema": "public",
1159 "table": "sources"
1160 },
1161 {
1162 "type": "text",
1163 "typeSchema": null,
1164 "notNull": false,
1165 "dimensions": 0,
1166 "default": null,
1167 "generated": null,
1168 "identity": null,
1169 "name": "default_headers_json",
1170 "entityType": "columns",
1171 "schema": "public",
1172 "table": "sources"
1173 },
1174 {
1175 "type": "text",
1176 "typeSchema": null,
1177 "notNull": true,
1178 "dimensions": 0,
1179 "default": null,
1180 "generated": null,
1181 "identity": null,
1182 "name": "auth_kind",
1183 "entityType": "columns",
1184 "schema": "public",
1185 "table": "sources"
1186 },
1187 {
1188 "type": "text",
1189 "typeSchema": null,
1190 "notNull": false,
1191 "dimensions": 0,
1192 "default": null,
1193 "generated": null,
1194 "identity": null,
1195 "name": "auth_header_name",
1196 "entityType": "columns",
1197 "schema": "public",
1198 "table": "sources"
1199 },
1200 {
1201 "type": "text",
1202 "typeSchema": null,
1203 "notNull": false,
1204 "dimensions": 0,
1205 "default": null,
1206 "generated": null,
1207 "identity": null,
1208 "name": "auth_prefix",
1209 "entityType": "columns",
1210 "schema": "public",
1211 "table": "sources"
1212 },
1213 {
1214 "type": "text",
1215 "typeSchema": null,
1216 "notNull": false,
1217 "dimensions": 0,
1218 "default": null,
1219 "generated": null,
1220 "identity": null,
1221 "name": "source_hash",
1222 "entityType": "columns",
1223 "schema": "public",
1224 "table": "sources"
1225 },
1226 {
1227 "type": "text",
1228 "typeSchema": null,
1229 "notNull": false,
1230 "dimensions": 0,
1231 "default": null,
1232 "generated": null,
1233 "identity": null,
1234 "name": "last_error",
1235 "entityType": "columns",
1236 "schema": "public",
1237 "table": "sources"
1238 },
1239 {
1240 "type": "bigint",
1241 "typeSchema": null,
1242 "notNull": true,
1243 "dimensions": 0,
1244 "default": null,
1245 "generated": null,
1246 "identity": null,
1247 "name": "created_at",
1248 "entityType": "columns",
1249 "schema": "public",
1250 "table": "sources"
1251 },
1252 {
1253 "type": "bigint",
1254 "typeSchema": null,
1255 "notNull": true,
1256 "dimensions": 0,
1257 "default": null,
1258 "generated": null,
1259 "identity": null,
1260 "name": "updated_at",
1261 "entityType": "columns",
1262 "schema": "public",
1263 "table": "sources"
1264 },
1265 {
1266 "type": "text",
1267 "typeSchema": null,
1268 "notNull": true,
1269 "dimensions": 0,
1270 "default": null,
1271 "generated": null,
1272 "identity": null,
1273 "name": "id",
1274 "entityType": "columns",
1275 "schema": "public",
1276 "table": "workspaces"
1277 },
1278 {
1279 "type": "text",
1280 "typeSchema": null,
1281 "notNull": true,
1282 "dimensions": 0,
1283 "default": null,
1284 "generated": null,
1285 "identity": null,
1286 "name": "organization_id",
1287 "entityType": "columns",
1288 "schema": "public",
1289 "table": "workspaces"
1290 },
1291 {
1292 "type": "text",
1293 "typeSchema": null,
1294 "notNull": true,
1295 "dimensions": 0,
1296 "default": null,
1297 "generated": null,
1298 "identity": null,
1299 "name": "name",
1300 "entityType": "columns",
1301 "schema": "public",
1302 "table": "workspaces"
1303 },
1304 {
1305 "type": "text",
1306 "typeSchema": null,
1307 "notNull": false,
1308 "dimensions": 0,
1309 "default": null,
1310 "generated": null,
1311 "identity": null,
1312 "name": "created_by_account_id",
1313 "entityType": "columns",
1314 "schema": "public",
1315 "table": "workspaces"
1316 },
1317 {
1318 "type": "bigint",
1319 "typeSchema": null,
1320 "notNull": true,
1321 "dimensions": 0,
1322 "default": null,
1323 "generated": null,
1324 "identity": null,
1325 "name": "created_at",
1326 "entityType": "columns",
1327 "schema": "public",
1328 "table": "workspaces"
1329 },
1330 {
1331 "type": "bigint",
1332 "typeSchema": null,
1333 "notNull": true,
1334 "dimensions": 0,
1335 "default": null,
1336 "generated": null,
1337 "identity": null,
1338 "name": "updated_at",
1339 "entityType": "columns",
1340 "schema": "public",
1341 "table": "workspaces"
1342 },
1343 {
1344 "nameExplicit": true,
1345 "columns": [
1346 {
1347 "value": "provider",
1348 "isExpression": false,
1349 "asc": true,
1350 "nullsFirst": false,
1351 "opclass": null
1352 },
1353 {
1354 "value": "subject",
1355 "isExpression": false,
1356 "asc": true,
1357 "nullsFirst": false,
1358 "opclass": null
1359 }
1360 ],
1361 "isUnique": true,
1362 "where": null,
1363 "with": "",
1364 "method": "btree",
1365 "concurrently": false,
1366 "name": "accounts_provider_subject_idx",
1367 "entityType": "indexes",
1368 "schema": "public",
1369 "table": "accounts"
1370 },
1371 {
1372 "nameExplicit": true,
1373 "columns": [
1374 {
1375 "value": "updated_at",
1376 "isExpression": false,
1377 "asc": true,
1378 "nullsFirst": false,
1379 "opclass": null
1380 },
1381 {
1382 "value": "id",
1383 "isExpression": false,
1384 "asc": true,
1385 "nullsFirst": false,
1386 "opclass": null
1387 }
1388 ],
1389 "isUnique": false,
1390 "where": null,
1391 "with": "",
1392 "method": "btree",
1393 "concurrently": false,
1394 "name": "accounts_updated_idx",
1395 "entityType": "indexes",
1396 "schema": "public",
1397 "table": "accounts"
1398 },
1399 {
1400 "nameExplicit": true,
1401 "columns": [
1402 {
1403 "value": "execution_id",
1404 "isExpression": false,
1405 "asc": true,
1406 "nullsFirst": false,
1407 "opclass": null
1408 },
1409 {
1410 "value": "updated_at",
1411 "isExpression": false,
1412 "asc": true,
1413 "nullsFirst": false,
1414 "opclass": null
1415 },
1416 {
1417 "value": "id",
1418 "isExpression": false,
1419 "asc": true,
1420 "nullsFirst": false,
1421 "opclass": null
1422 }
1423 ],
1424 "isUnique": false,
1425 "where": null,
1426 "with": "",
1427 "method": "btree",
1428 "concurrently": false,
1429 "name": "execution_interactions_execution_idx",
1430 "entityType": "indexes",
1431 "schema": "public",
1432 "table": "execution_interactions"
1433 },
1434 {
1435 "nameExplicit": true,
1436 "columns": [
1437 {
1438 "value": "workspace_id",
1439 "isExpression": false,
1440 "asc": true,
1441 "nullsFirst": false,
1442 "opclass": null
1443 },
1444 {
1445 "value": "updated_at",
1446 "isExpression": false,
1447 "asc": true,
1448 "nullsFirst": false,
1449 "opclass": null
1450 },
1451 {
1452 "value": "id",
1453 "isExpression": false,
1454 "asc": true,
1455 "nullsFirst": false,
1456 "opclass": null
1457 }
1458 ],
1459 "isUnique": false,
1460 "where": null,
1461 "with": "",
1462 "method": "btree",
1463 "concurrently": false,
1464 "name": "executions_workspace_idx",
1465 "entityType": "indexes",
1466 "schema": "public",
1467 "table": "executions"
1468 },
1469 {
1470 "nameExplicit": true,
1471 "columns": [
1472 {
1473 "value": "organization_id",
1474 "isExpression": false,
1475 "asc": true,
1476 "nullsFirst": false,
1477 "opclass": null
1478 }
1479 ],
1480 "isUnique": false,
1481 "where": null,
1482 "with": "",
1483 "method": "btree",
1484 "concurrently": false,
1485 "name": "organization_memberships_org_idx",
1486 "entityType": "indexes",
1487 "schema": "public",
1488 "table": "organization_memberships"
1489 },
1490 {
1491 "nameExplicit": true,
1492 "columns": [
1493 {
1494 "value": "account_id",
1495 "isExpression": false,
1496 "asc": true,
1497 "nullsFirst": false,
1498 "opclass": null
1499 }
1500 ],
1501 "isUnique": false,
1502 "where": null,
1503 "with": "",
1504 "method": "btree",
1505 "concurrently": false,
1506 "name": "organization_memberships_account_idx",
1507 "entityType": "indexes",
1508 "schema": "public",
1509 "table": "organization_memberships"
1510 },
1511 {
1512 "nameExplicit": true,
1513 "columns": [
1514 {
1515 "value": "organization_id",
1516 "isExpression": false,
1517 "asc": true,
1518 "nullsFirst": false,
1519 "opclass": null
1520 },
1521 {
1522 "value": "account_id",
1523 "isExpression": false,
1524 "asc": true,
1525 "nullsFirst": false,
1526 "opclass": null
1527 }
1528 ],
1529 "isUnique": true,
1530 "where": null,
1531 "with": "",
1532 "method": "btree",
1533 "concurrently": false,
1534 "name": "organization_memberships_org_account_idx",
1535 "entityType": "indexes",
1536 "schema": "public",
1537 "table": "organization_memberships"
1538 },
1539 {
1540 "nameExplicit": true,
1541 "columns": [
1542 {
1543 "value": "slug",
1544 "isExpression": false,
1545 "asc": true,
1546 "nullsFirst": false,
1547 "opclass": null
1548 }
1549 ],
1550 "isUnique": true,
1551 "where": null,
1552 "with": "",
1553 "method": "btree",
1554 "concurrently": false,
1555 "name": "organizations_slug_idx",
1556 "entityType": "indexes",
1557 "schema": "public",
1558 "table": "organizations"
1559 },
1560 {
1561 "nameExplicit": true,
1562 "columns": [
1563 {
1564 "value": "updated_at",
1565 "isExpression": false,
1566 "asc": true,
1567 "nullsFirst": false,
1568 "opclass": null
1569 },
1570 {
1571 "value": "id",
1572 "isExpression": false,
1573 "asc": true,
1574 "nullsFirst": false,
1575 "opclass": null
1576 }
1577 ],
1578 "isUnique": false,
1579 "where": null,
1580 "with": "",
1581 "method": "btree",
1582 "concurrently": false,
1583 "name": "organizations_updated_idx",
1584 "entityType": "indexes",
1585 "schema": "public",
1586 "table": "organizations"
1587 },
1588 {
1589 "nameExplicit": true,
1590 "columns": [
1591 {
1592 "value": "workspace_id",
1593 "isExpression": false,
1594 "asc": true,
1595 "nullsFirst": false,
1596 "opclass": null
1597 },
1598 {
1599 "value": "updated_at",
1600 "isExpression": false,
1601 "asc": true,
1602 "nullsFirst": false,
1603 "opclass": null
1604 },
1605 {
1606 "value": "id",
1607 "isExpression": false,
1608 "asc": true,
1609 "nullsFirst": false,
1610 "opclass": null
1611 }
1612 ],
1613 "isUnique": false,
1614 "where": null,
1615 "with": "",
1616 "method": "btree",
1617 "concurrently": false,
1618 "name": "policies_workspace_idx",
1619 "entityType": "indexes",
1620 "schema": "public",
1621 "table": "policies"
1622 },
1623 {
1624 "nameExplicit": true,
1625 "columns": [
1626 {
1627 "value": "workspace_id",
1628 "isExpression": false,
1629 "asc": true,
1630 "nullsFirst": false,
1631 "opclass": null
1632 },
1633 {
1634 "value": "updated_at",
1635 "isExpression": false,
1636 "asc": true,
1637 "nullsFirst": false,
1638 "opclass": null
1639 },
1640 {
1641 "value": "source_id",
1642 "isExpression": false,
1643 "asc": true,
1644 "nullsFirst": false,
1645 "opclass": null
1646 }
1647 ],
1648 "isUnique": false,
1649 "where": null,
1650 "with": "",
1651 "method": "btree",
1652 "concurrently": false,
1653 "name": "source_credential_bindings_workspace_idx",
1654 "entityType": "indexes",
1655 "schema": "public",
1656 "table": "source_credential_bindings"
1657 },
1658 {
1659 "nameExplicit": true,
1660 "columns": [
1661 {
1662 "value": "workspace_id",
1663 "isExpression": false,
1664 "asc": true,
1665 "nullsFirst": false,
1666 "opclass": null
1667 },
1668 {
1669 "value": "name",
1670 "isExpression": false,
1671 "asc": true,
1672 "nullsFirst": false,
1673 "opclass": null
1674 }
1675 ],
1676 "isUnique": true,
1677 "where": null,
1678 "with": "",
1679 "method": "btree",
1680 "concurrently": false,
1681 "name": "sources_workspace_name_idx",
1682 "entityType": "indexes",
1683 "schema": "public",
1684 "table": "sources"
1685 },
1686 {
1687 "nameExplicit": true,
1688 "columns": [
1689 {
1690 "value": "organization_id",
1691 "isExpression": false,
1692 "asc": true,
1693 "nullsFirst": false,
1694 "opclass": null
1695 }
1696 ],
1697 "isUnique": false,
1698 "where": null,
1699 "with": "",
1700 "method": "btree",
1701 "concurrently": false,
1702 "name": "workspaces_org_idx",
1703 "entityType": "indexes",
1704 "schema": "public",
1705 "table": "workspaces"
1706 },
1707 {
1708 "nameExplicit": true,
1709 "columns": [
1710 {
1711 "value": "organization_id",
1712 "isExpression": false,
1713 "asc": true,
1714 "nullsFirst": false,
1715 "opclass": null
1716 },
1717 {
1718 "value": "name",
1719 "isExpression": false,
1720 "asc": true,
1721 "nullsFirst": false,
1722 "opclass": null
1723 }
1724 ],
1725 "isUnique": true,
1726 "where": null,
1727 "with": "",
1728 "method": "btree",
1729 "concurrently": false,
1730 "name": "workspaces_org_name_idx",
1731 "entityType": "indexes",
1732 "schema": "public",
1733 "table": "workspaces"
1734 },
1735 {
1736 "columns": [
1737 "workspace_id",
1738 "source_id"
1739 ],
1740 "nameExplicit": false,
1741 "name": "source_credential_bindings_pkey",
1742 "entityType": "pks",
1743 "schema": "public",
1744 "table": "source_credential_bindings"
1745 },
1746 {
1747 "columns": [
1748 "workspace_id",
1749 "source_id"
1750 ],
1751 "nameExplicit": false,
1752 "name": "sources_pkey",
1753 "entityType": "pks",
1754 "schema": "public",
1755 "table": "sources"
1756 },
1757 {
1758 "columns": [
1759 "id"
1760 ],
1761 "nameExplicit": false,
1762 "name": "accounts_pkey",
1763 "schema": "public",
1764 "table": "accounts",
1765 "entityType": "pks"
1766 },
1767 {
1768 "columns": [
1769 "id"
1770 ],
1771 "nameExplicit": false,
1772 "name": "execution_interactions_pkey",
1773 "schema": "public",
1774 "table": "execution_interactions",
1775 "entityType": "pks"
1776 },
1777 {
1778 "columns": [
1779 "id"
1780 ],
1781 "nameExplicit": false,
1782 "name": "executions_pkey",
1783 "schema": "public",
1784 "table": "executions",
1785 "entityType": "pks"
1786 },
1787 {
1788 "columns": [
1789 "id"
1790 ],
1791 "nameExplicit": false,
1792 "name": "local_installations_pkey",
1793 "schema": "public",
1794 "table": "local_installations",
1795 "entityType": "pks"
1796 },
1797 {
1798 "columns": [
1799 "id"
1800 ],
1801 "nameExplicit": false,
1802 "name": "organization_memberships_pkey",
1803 "schema": "public",
1804 "table": "organization_memberships",
1805 "entityType": "pks"
1806 },
1807 {
1808 "columns": [
1809 "id"
1810 ],
1811 "nameExplicit": false,
1812 "name": "organizations_pkey",
1813 "schema": "public",
1814 "table": "organizations",
1815 "entityType": "pks"
1816 },
1817 {
1818 "columns": [
1819 "id"
1820 ],
1821 "nameExplicit": false,
1822 "name": "policies_pkey",
1823 "schema": "public",
1824 "table": "policies",
1825 "entityType": "pks"
1826 },
1827 {
1828 "columns": [
1829 "id"
1830 ],
1831 "nameExplicit": false,
1832 "name": "workspaces_pkey",
1833 "schema": "public",
1834 "table": "workspaces",
1835 "entityType": "pks"
1836 },
1837 {
1838 "value": "\"provider\" in ('local', 'workos', 'service')",
1839 "name": "accounts_provider_check",
1840 "entityType": "checks",
1841 "schema": "public",
1842 "table": "accounts"
1843 },
1844 {
1845 "value": "\"status\" in ('pending', 'resolved', 'cancelled')",
1846 "name": "execution_interactions_status_check",
1847 "entityType": "checks",
1848 "schema": "public",
1849 "table": "execution_interactions"
1850 },
1851 {
1852 "value": "\"status\" in ('pending', 'running', 'waiting_for_interaction', 'completed', 'failed', 'cancelled')",
1853 "name": "executions_status_check",
1854 "entityType": "checks",
1855 "schema": "public",
1856 "table": "executions"
1857 },
1858 {
1859 "value": "\"role\" in ('viewer', 'editor', 'admin', 'owner')",
1860 "name": "organization_memberships_role_check",
1861 "entityType": "checks",
1862 "schema": "public",
1863 "table": "organization_memberships"
1864 },
1865 {
1866 "value": "\"status\" in ('invited', 'active', 'suspended', 'removed')",
1867 "name": "organization_memberships_status_check",
1868 "entityType": "checks",
1869 "schema": "public",
1870 "table": "organization_memberships"
1871 },
1872 {
1873 "value": "\"status\" in ('active', 'suspended', 'archived')",
1874 "name": "organizations_status_check",
1875 "entityType": "checks",
1876 "schema": "public",
1877 "table": "organizations"
1878 },
1879 {
1880 "value": "\"resource_type\" in ('all_tools', 'source', 'namespace', 'tool_path')",
1881 "name": "policies_resource_type_check",
1882 "entityType": "checks",
1883 "schema": "public",
1884 "table": "policies"
1885 },
1886 {
1887 "value": "\"match_type\" in ('glob', 'exact')",
1888 "name": "policies_match_type_check",
1889 "entityType": "checks",
1890 "schema": "public",
1891 "table": "policies"
1892 },
1893 {
1894 "value": "\"effect\" in ('allow', 'deny')",
1895 "name": "policies_effect_check",
1896 "entityType": "checks",
1897 "schema": "public",
1898 "table": "policies"
1899 },
1900 {
1901 "value": "\"approval_mode\" in ('auto', 'required')",
1902 "name": "policies_approval_mode_check",
1903 "entityType": "checks",
1904 "schema": "public",
1905 "table": "policies"
1906 },
1907 {
1908 "value": "\"kind\" in ('mcp', 'openapi', 'graphql', 'internal')",
1909 "name": "sources_kind_check",
1910 "entityType": "checks",
1911 "schema": "public",
1912 "table": "sources"
1913 },
1914 {
1915 "value": "\"status\" in ('draft', 'probing', 'auth_required', 'connected', 'error')",
1916 "name": "sources_status_check",
1917 "entityType": "checks",
1918 "schema": "public",
1919 "table": "sources"
1920 },
1921 {
1922 "value": "\"transport\" is null or \"transport\" in ('auto', 'streamable-http', 'sse')",
1923 "name": "sources_transport_check",
1924 "entityType": "checks",
1925 "schema": "public",
1926 "table": "sources"
1927 },
1928 {
1929 "value": "\"auth_kind\" in ('none', 'bearer', 'oauth2')",
1930 "name": "sources_auth_kind_check",
1931 "entityType": "checks",
1932 "schema": "public",
1933 "table": "sources"
1934 }
1935 ],
1936 "renames": []
1937}
packages/control-plane/drizzle/20260306094249_large_giant_girl/migration.sql+0−39
@@ -1,39 +0,0 @@
1CREATE TABLE "secret_materials" (
2 "id" text PRIMARY KEY,
3 "purpose" text NOT NULL,
4 "value" text NOT NULL,
5 "created_at" bigint NOT NULL,
6 "updated_at" bigint NOT NULL,
7 CONSTRAINT "secret_materials_purpose_check" CHECK ("purpose" in ('auth_material', 'oauth_access_token', 'oauth_refresh_token', 'oauth_client_info'))
8);
9--> statement-breakpoint
10CREATE TABLE "source_auth_sessions" (
11 "id" text PRIMARY KEY,
12 "workspace_id" text NOT NULL,
13 "source_id" text NOT NULL,
14 "execution_id" text,
15 "interaction_id" text,
16 "strategy" text NOT NULL,
17 "status" text NOT NULL,
18 "endpoint" text NOT NULL,
19 "state" text NOT NULL,
20 "redirect_uri" text NOT NULL,
21 "scope" text,
22 "resource_metadata_url" text,
23 "authorization_server_url" text,
24 "resource_metadata_json" text,
25 "authorization_server_metadata_json" text,
26 "client_information_json" text,
27 "code_verifier" text,
28 "authorization_url" text,
29 "error_text" text,
30 "completed_at" bigint,
31 "created_at" bigint NOT NULL,
32 "updated_at" bigint NOT NULL,
33 CONSTRAINT "source_auth_sessions_strategy_check" CHECK ("strategy" in ('oauth2_authorization_code')),
34 CONSTRAINT "source_auth_sessions_status_check" CHECK ("status" in ('pending', 'completed', 'failed', 'cancelled'))
35);
36--> statement-breakpoint
37CREATE INDEX "secret_materials_updated_idx" ON "secret_materials" ("updated_at","id");--> statement-breakpoint
38CREATE INDEX "source_auth_sessions_workspace_idx" ON "source_auth_sessions" ("workspace_id","updated_at","id");--> statement-breakpoint
39CREATE UNIQUE INDEX "source_auth_sessions_state_idx" ON "source_auth_sessions" ("state");
packages/control-plane/drizzle/20260306094249_large_giant_girl/snapshot.json+0−2425
@@ -1,2425 +0,0 @@
1{
2 "version": "8",
3 "dialect": "postgres",
4 "id": "1141497b-c500-44ff-87db-6a440062becb",
5 "prevIds": [
6 "bac1651d-7a30-4187-a7e2-74bc2b099486"
7 ],
8 "ddl": [
9 {
10 "isRlsEnabled": false,
11 "name": "accounts",
12 "entityType": "tables",
13 "schema": "public"
14 },
15 {
16 "isRlsEnabled": false,
17 "name": "execution_interactions",
18 "entityType": "tables",
19 "schema": "public"
20 },
21 {
22 "isRlsEnabled": false,
23 "name": "executions",
24 "entityType": "tables",
25 "schema": "public"
26 },
27 {
28 "isRlsEnabled": false,
29 "name": "local_installations",
30 "entityType": "tables",
31 "schema": "public"
32 },
33 {
34 "isRlsEnabled": false,
35 "name": "organization_memberships",
36 "entityType": "tables",
37 "schema": "public"
38 },
39 {
40 "isRlsEnabled": false,
41 "name": "organizations",
42 "entityType": "tables",
43 "schema": "public"
44 },
45 {
46 "isRlsEnabled": false,
47 "name": "policies",
48 "entityType": "tables",
49 "schema": "public"
50 },
51 {
52 "isRlsEnabled": false,
53 "name": "secret_materials",
54 "entityType": "tables",
55 "schema": "public"
56 },
57 {
58 "isRlsEnabled": false,
59 "name": "source_auth_sessions",
60 "entityType": "tables",
61 "schema": "public"
62 },
63 {
64 "isRlsEnabled": false,
65 "name": "source_credential_bindings",
66 "entityType": "tables",
67 "schema": "public"
68 },
69 {
70 "isRlsEnabled": false,
71 "name": "sources",
72 "entityType": "tables",
73 "schema": "public"
74 },
75 {
76 "isRlsEnabled": false,
77 "name": "workspaces",
78 "entityType": "tables",
79 "schema": "public"
80 },
81 {
82 "type": "text",
83 "typeSchema": null,
84 "notNull": true,
85 "dimensions": 0,
86 "default": null,
87 "generated": null,
88 "identity": null,
89 "name": "id",
90 "entityType": "columns",
91 "schema": "public",
92 "table": "accounts"
93 },
94 {
95 "type": "text",
96 "typeSchema": null,
97 "notNull": true,
98 "dimensions": 0,
99 "default": null,
100 "generated": null,
101 "identity": null,
102 "name": "provider",
103 "entityType": "columns",
104 "schema": "public",
105 "table": "accounts"
106 },
107 {
108 "type": "text",
109 "typeSchema": null,
110 "notNull": true,
111 "dimensions": 0,
112 "default": null,
113 "generated": null,
114 "identity": null,
115 "name": "subject",
116 "entityType": "columns",
117 "schema": "public",
118 "table": "accounts"
119 },
120 {
121 "type": "text",
122 "typeSchema": null,
123 "notNull": false,
124 "dimensions": 0,
125 "default": null,
126 "generated": null,
127 "identity": null,
128 "name": "email",
129 "entityType": "columns",
130 "schema": "public",
131 "table": "accounts"
132 },
133 {
134 "type": "text",
135 "typeSchema": null,
136 "notNull": false,
137 "dimensions": 0,
138 "default": null,
139 "generated": null,
140 "identity": null,
141 "name": "display_name",
142 "entityType": "columns",
143 "schema": "public",
144 "table": "accounts"
145 },
146 {
147 "type": "bigint",
148 "typeSchema": null,
149 "notNull": true,
150 "dimensions": 0,
151 "default": null,
152 "generated": null,
153 "identity": null,
154 "name": "created_at",
155 "entityType": "columns",
156 "schema": "public",
157 "table": "accounts"
158 },
159 {
160 "type": "bigint",
161 "typeSchema": null,
162 "notNull": true,
163 "dimensions": 0,
164 "default": null,
165 "generated": null,
166 "identity": null,
167 "name": "updated_at",
168 "entityType": "columns",
169 "schema": "public",
170 "table": "accounts"
171 },
172 {
173 "type": "text",
174 "typeSchema": null,
175 "notNull": true,
176 "dimensions": 0,
177 "default": null,
178 "generated": null,
179 "identity": null,
180 "name": "id",
181 "entityType": "columns",
182 "schema": "public",
183 "table": "execution_interactions"
184 },
185 {
186 "type": "text",
187 "typeSchema": null,
188 "notNull": true,
189 "dimensions": 0,
190 "default": null,
191 "generated": null,
192 "identity": null,
193 "name": "execution_id",
194 "entityType": "columns",
195 "schema": "public",
196 "table": "execution_interactions"
197 },
198 {
199 "type": "text",
200 "typeSchema": null,
201 "notNull": true,
202 "dimensions": 0,
203 "default": null,
204 "generated": null,
205 "identity": null,
206 "name": "status",
207 "entityType": "columns",
208 "schema": "public",
209 "table": "execution_interactions"
210 },
211 {
212 "type": "text",
213 "typeSchema": null,
214 "notNull": true,
215 "dimensions": 0,
216 "default": null,
217 "generated": null,
218 "identity": null,
219 "name": "kind",
220 "entityType": "columns",
221 "schema": "public",
222 "table": "execution_interactions"
223 },
224 {
225 "type": "text",
226 "typeSchema": null,
227 "notNull": true,
228 "dimensions": 0,
229 "default": null,
230 "generated": null,
231 "identity": null,
232 "name": "payload_json",
233 "entityType": "columns",
234 "schema": "public",
235 "table": "execution_interactions"
236 },
237 {
238 "type": "text",
239 "typeSchema": null,
240 "notNull": false,
241 "dimensions": 0,
242 "default": null,
243 "generated": null,
244 "identity": null,
245 "name": "response_json",
246 "entityType": "columns",
247 "schema": "public",
248 "table": "execution_interactions"
249 },
250 {
251 "type": "bigint",
252 "typeSchema": null,
253 "notNull": true,
254 "dimensions": 0,
255 "default": null,
256 "generated": null,
257 "identity": null,
258 "name": "created_at",
259 "entityType": "columns",
260 "schema": "public",
261 "table": "execution_interactions"
262 },
263 {
264 "type": "bigint",
265 "typeSchema": null,
266 "notNull": true,
267 "dimensions": 0,
268 "default": null,
269 "generated": null,
270 "identity": null,
271 "name": "updated_at",
272 "entityType": "columns",
273 "schema": "public",
274 "table": "execution_interactions"
275 },
276 {
277 "type": "text",
278 "typeSchema": null,
279 "notNull": true,
280 "dimensions": 0,
281 "default": null,
282 "generated": null,
283 "identity": null,
284 "name": "id",
285 "entityType": "columns",
286 "schema": "public",
287 "table": "executions"
288 },
289 {
290 "type": "text",
291 "typeSchema": null,
292 "notNull": true,
293 "dimensions": 0,
294 "default": null,
295 "generated": null,
296 "identity": null,
297 "name": "workspace_id",
298 "entityType": "columns",
299 "schema": "public",
300 "table": "executions"
301 },
302 {
303 "type": "text",
304 "typeSchema": null,
305 "notNull": true,
306 "dimensions": 0,
307 "default": null,
308 "generated": null,
309 "identity": null,
310 "name": "created_by_account_id",
311 "entityType": "columns",
312 "schema": "public",
313 "table": "executions"
314 },
315 {
316 "type": "text",
317 "typeSchema": null,
318 "notNull": true,
319 "dimensions": 0,
320 "default": null,
321 "generated": null,
322 "identity": null,
323 "name": "status",
324 "entityType": "columns",
325 "schema": "public",
326 "table": "executions"
327 },
328 {
329 "type": "text",
330 "typeSchema": null,
331 "notNull": true,
332 "dimensions": 0,
333 "default": null,
334 "generated": null,
335 "identity": null,
336 "name": "code",
337 "entityType": "columns",
338 "schema": "public",
339 "table": "executions"
340 },
341 {
342 "type": "text",
343 "typeSchema": null,
344 "notNull": false,
345 "dimensions": 0,
346 "default": null,
347 "generated": null,
348 "identity": null,
349 "name": "result_json",
350 "entityType": "columns",
351 "schema": "public",
352 "table": "executions"
353 },
354 {
355 "type": "text",
356 "typeSchema": null,
357 "notNull": false,
358 "dimensions": 0,
359 "default": null,
360 "generated": null,
361 "identity": null,
362 "name": "error_text",
363 "entityType": "columns",
364 "schema": "public",
365 "table": "executions"
366 },
367 {
368 "type": "text",
369 "typeSchema": null,
370 "notNull": false,
371 "dimensions": 0,
372 "default": null,
373 "generated": null,
374 "identity": null,
375 "name": "logs_json",
376 "entityType": "columns",
377 "schema": "public",
378 "table": "executions"
379 },
380 {
381 "type": "bigint",
382 "typeSchema": null,
383 "notNull": false,
384 "dimensions": 0,
385 "default": null,
386 "generated": null,
387 "identity": null,
388 "name": "started_at",
389 "entityType": "columns",
390 "schema": "public",
391 "table": "executions"
392 },
393 {
394 "type": "bigint",
395 "typeSchema": null,
396 "notNull": false,
397 "dimensions": 0,
398 "default": null,
399 "generated": null,
400 "identity": null,
401 "name": "completed_at",
402 "entityType": "columns",
403 "schema": "public",
404 "table": "executions"
405 },
406 {
407 "type": "bigint",
408 "typeSchema": null,
409 "notNull": true,
410 "dimensions": 0,
411 "default": null,
412 "generated": null,
413 "identity": null,
414 "name": "created_at",
415 "entityType": "columns",
416 "schema": "public",
417 "table": "executions"
418 },
419 {
420 "type": "bigint",
421 "typeSchema": null,
422 "notNull": true,
423 "dimensions": 0,
424 "default": null,
425 "generated": null,
426 "identity": null,
427 "name": "updated_at",
428 "entityType": "columns",
429 "schema": "public",
430 "table": "executions"
431 },
432 {
433 "type": "text",
434 "typeSchema": null,
435 "notNull": true,
436 "dimensions": 0,
437 "default": null,
438 "generated": null,
439 "identity": null,
440 "name": "id",
441 "entityType": "columns",
442 "schema": "public",
443 "table": "local_installations"
444 },
445 {
446 "type": "text",
447 "typeSchema": null,
448 "notNull": true,
449 "dimensions": 0,
450 "default": null,
451 "generated": null,
452 "identity": null,
453 "name": "account_id",
454 "entityType": "columns",
455 "schema": "public",
456 "table": "local_installations"
457 },
458 {
459 "type": "text",
460 "typeSchema": null,
461 "notNull": true,
462 "dimensions": 0,
463 "default": null,
464 "generated": null,
465 "identity": null,
466 "name": "organization_id",
467 "entityType": "columns",
468 "schema": "public",
469 "table": "local_installations"
470 },
471 {
472 "type": "text",
473 "typeSchema": null,
474 "notNull": true,
475 "dimensions": 0,
476 "default": null,
477 "generated": null,
478 "identity": null,
479 "name": "workspace_id",
480 "entityType": "columns",
481 "schema": "public",
482 "table": "local_installations"
483 },
484 {
485 "type": "bigint",
486 "typeSchema": null,
487 "notNull": true,
488 "dimensions": 0,
489 "default": null,
490 "generated": null,
491 "identity": null,
492 "name": "created_at",
493 "entityType": "columns",
494 "schema": "public",
495 "table": "local_installations"
496 },
497 {
498 "type": "bigint",
499 "typeSchema": null,
500 "notNull": true,
501 "dimensions": 0,
502 "default": null,
503 "generated": null,
504 "identity": null,
505 "name": "updated_at",
506 "entityType": "columns",
507 "schema": "public",
508 "table": "local_installations"
509 },
510 {
511 "type": "text",
512 "typeSchema": null,
513 "notNull": true,
514 "dimensions": 0,
515 "default": null,
516 "generated": null,
517 "identity": null,
518 "name": "id",
519 "entityType": "columns",
520 "schema": "public",
521 "table": "organization_memberships"
522 },
523 {
524 "type": "text",
525 "typeSchema": null,
526 "notNull": true,
527 "dimensions": 0,
528 "default": null,
529 "generated": null,
530 "identity": null,
531 "name": "organization_id",
532 "entityType": "columns",
533 "schema": "public",
534 "table": "organization_memberships"
535 },
536 {
537 "type": "text",
538 "typeSchema": null,
539 "notNull": true,
540 "dimensions": 0,
541 "default": null,
542 "generated": null,
543 "identity": null,
544 "name": "account_id",
545 "entityType": "columns",
546 "schema": "public",
547 "table": "organization_memberships"
548 },
549 {
550 "type": "text",
551 "typeSchema": null,
552 "notNull": true,
553 "dimensions": 0,
554 "default": null,
555 "generated": null,
556 "identity": null,
557 "name": "role",
558 "entityType": "columns",
559 "schema": "public",
560 "table": "organization_memberships"
561 },
562 {
563 "type": "text",
564 "typeSchema": null,
565 "notNull": true,
566 "dimensions": 0,
567 "default": null,
568 "generated": null,
569 "identity": null,
570 "name": "status",
571 "entityType": "columns",
572 "schema": "public",
573 "table": "organization_memberships"
574 },
575 {
576 "type": "boolean",
577 "typeSchema": null,
578 "notNull": true,
579 "dimensions": 0,
580 "default": null,
581 "generated": null,
582 "identity": null,
583 "name": "billable",
584 "entityType": "columns",
585 "schema": "public",
586 "table": "organization_memberships"
587 },
588 {
589 "type": "text",
590 "typeSchema": null,
591 "notNull": false,
592 "dimensions": 0,
593 "default": null,
594 "generated": null,
595 "identity": null,
596 "name": "invited_by_account_id",
597 "entityType": "columns",
598 "schema": "public",
599 "table": "organization_memberships"
600 },
601 {
602 "type": "bigint",
603 "typeSchema": null,
604 "notNull": false,
605 "dimensions": 0,
606 "default": null,
607 "generated": null,
608 "identity": null,
609 "name": "joined_at",
610 "entityType": "columns",
611 "schema": "public",
612 "table": "organization_memberships"
613 },
614 {
615 "type": "bigint",
616 "typeSchema": null,
617 "notNull": true,
618 "dimensions": 0,
619 "default": null,
620 "generated": null,
621 "identity": null,
622 "name": "created_at",
623 "entityType": "columns",
624 "schema": "public",
625 "table": "organization_memberships"
626 },
627 {
628 "type": "bigint",
629 "typeSchema": null,
630 "notNull": true,
631 "dimensions": 0,
632 "default": null,
633 "generated": null,
634 "identity": null,
635 "name": "updated_at",
636 "entityType": "columns",
637 "schema": "public",
638 "table": "organization_memberships"
639 },
640 {
641 "type": "text",
642 "typeSchema": null,
643 "notNull": true,
644 "dimensions": 0,
645 "default": null,
646 "generated": null,
647 "identity": null,
648 "name": "id",
649 "entityType": "columns",
650 "schema": "public",
651 "table": "organizations"
652 },
653 {
654 "type": "text",
655 "typeSchema": null,
656 "notNull": true,
657 "dimensions": 0,
658 "default": null,
659 "generated": null,
660 "identity": null,
661 "name": "slug",
662 "entityType": "columns",
663 "schema": "public",
664 "table": "organizations"
665 },
666 {
667 "type": "text",
668 "typeSchema": null,
669 "notNull": true,
670 "dimensions": 0,
671 "default": null,
672 "generated": null,
673 "identity": null,
674 "name": "name",
675 "entityType": "columns",
676 "schema": "public",
677 "table": "organizations"
678 },
679 {
680 "type": "text",
681 "typeSchema": null,
682 "notNull": true,
683 "dimensions": 0,
684 "default": null,
685 "generated": null,
686 "identity": null,
687 "name": "status",
688 "entityType": "columns",
689 "schema": "public",
690 "table": "organizations"
691 },
692 {
693 "type": "text",
694 "typeSchema": null,
695 "notNull": false,
696 "dimensions": 0,
697 "default": null,
698 "generated": null,
699 "identity": null,
700 "name": "created_by_account_id",
701 "entityType": "columns",
702 "schema": "public",
703 "table": "organizations"
704 },
705 {
706 "type": "bigint",
707 "typeSchema": null,
708 "notNull": true,
709 "dimensions": 0,
710 "default": null,
711 "generated": null,
712 "identity": null,
713 "name": "created_at",
714 "entityType": "columns",
715 "schema": "public",
716 "table": "organizations"
717 },
718 {
719 "type": "bigint",
720 "typeSchema": null,
721 "notNull": true,
722 "dimensions": 0,
723 "default": null,
724 "generated": null,
725 "identity": null,
726 "name": "updated_at",
727 "entityType": "columns",
728 "schema": "public",
729 "table": "organizations"
730 },
731 {
732 "type": "text",
733 "typeSchema": null,
734 "notNull": true,
735 "dimensions": 0,
736 "default": null,
737 "generated": null,
738 "identity": null,
739 "name": "id",
740 "entityType": "columns",
741 "schema": "public",
742 "table": "policies"
743 },
744 {
745 "type": "text",
746 "typeSchema": null,
747 "notNull": true,
748 "dimensions": 0,
749 "default": null,
750 "generated": null,
751 "identity": null,
752 "name": "workspace_id",
753 "entityType": "columns",
754 "schema": "public",
755 "table": "policies"
756 },
757 {
758 "type": "text",
759 "typeSchema": null,
760 "notNull": false,
761 "dimensions": 0,
762 "default": null,
763 "generated": null,
764 "identity": null,
765 "name": "target_account_id",
766 "entityType": "columns",
767 "schema": "public",
768 "table": "policies"
769 },
770 {
771 "type": "text",
772 "typeSchema": null,
773 "notNull": false,
774 "dimensions": 0,
775 "default": null,
776 "generated": null,
777 "identity": null,
778 "name": "client_id",
779 "entityType": "columns",
780 "schema": "public",
781 "table": "policies"
782 },
783 {
784 "type": "text",
785 "typeSchema": null,
786 "notNull": true,
787 "dimensions": 0,
788 "default": null,
789 "generated": null,
790 "identity": null,
791 "name": "resource_type",
792 "entityType": "columns",
793 "schema": "public",
794 "table": "policies"
795 },
796 {
797 "type": "text",
798 "typeSchema": null,
799 "notNull": true,
800 "dimensions": 0,
801 "default": null,
802 "generated": null,
803 "identity": null,
804 "name": "resource_pattern",
805 "entityType": "columns",
806 "schema": "public",
807 "table": "policies"
808 },
809 {
810 "type": "text",
811 "typeSchema": null,
812 "notNull": true,
813 "dimensions": 0,
814 "default": null,
815 "generated": null,
816 "identity": null,
817 "name": "match_type",
818 "entityType": "columns",
819 "schema": "public",
820 "table": "policies"
821 },
822 {
823 "type": "text",
824 "typeSchema": null,
825 "notNull": true,
826 "dimensions": 0,
827 "default": null,
828 "generated": null,
829 "identity": null,
830 "name": "effect",
831 "entityType": "columns",
832 "schema": "public",
833 "table": "policies"
834 },
835 {
836 "type": "text",
837 "typeSchema": null,
838 "notNull": true,
839 "dimensions": 0,
840 "default": null,
841 "generated": null,
842 "identity": null,
843 "name": "approval_mode",
844 "entityType": "columns",
845 "schema": "public",
846 "table": "policies"
847 },
848 {
849 "type": "text",
850 "typeSchema": null,
851 "notNull": false,
852 "dimensions": 0,
853 "default": null,
854 "generated": null,
855 "identity": null,
856 "name": "argument_conditions_json",
857 "entityType": "columns",
858 "schema": "public",
859 "table": "policies"
860 },
861 {
862 "type": "bigint",
863 "typeSchema": null,
864 "notNull": true,
865 "dimensions": 0,
866 "default": null,
867 "generated": null,
868 "identity": null,
869 "name": "priority",
870 "entityType": "columns",
871 "schema": "public",
872 "table": "policies"
873 },
874 {
875 "type": "boolean",
876 "typeSchema": null,
877 "notNull": true,
878 "dimensions": 0,
879 "default": null,
880 "generated": null,
881 "identity": null,
882 "name": "enabled",
883 "entityType": "columns",
884 "schema": "public",
885 "table": "policies"
886 },
887 {
888 "type": "bigint",
889 "typeSchema": null,
890 "notNull": true,
891 "dimensions": 0,
892 "default": null,
893 "generated": null,
894 "identity": null,
895 "name": "created_at",
896 "entityType": "columns",
897 "schema": "public",
898 "table": "policies"
899 },
900 {
901 "type": "bigint",
902 "typeSchema": null,
903 "notNull": true,
904 "dimensions": 0,
905 "default": null,
906 "generated": null,
907 "identity": null,
908 "name": "updated_at",
909 "entityType": "columns",
910 "schema": "public",
911 "table": "policies"
912 },
913 {
914 "type": "text",
915 "typeSchema": null,
916 "notNull": true,
917 "dimensions": 0,
918 "default": null,
919 "generated": null,
920 "identity": null,
921 "name": "id",
922 "entityType": "columns",
923 "schema": "public",
924 "table": "secret_materials"
925 },
926 {
927 "type": "text",
928 "typeSchema": null,
929 "notNull": true,
930 "dimensions": 0,
931 "default": null,
932 "generated": null,
933 "identity": null,
934 "name": "purpose",
935 "entityType": "columns",
936 "schema": "public",
937 "table": "secret_materials"
938 },
939 {
940 "type": "text",
941 "typeSchema": null,
942 "notNull": true,
943 "dimensions": 0,
944 "default": null,
945 "generated": null,
946 "identity": null,
947 "name": "value",
948 "entityType": "columns",
949 "schema": "public",
950 "table": "secret_materials"
951 },
952 {
953 "type": "bigint",
954 "typeSchema": null,
955 "notNull": true,
956 "dimensions": 0,
957 "default": null,
958 "generated": null,
959 "identity": null,
960 "name": "created_at",
961 "entityType": "columns",
962 "schema": "public",
963 "table": "secret_materials"
964 },
965 {
966 "type": "bigint",
967 "typeSchema": null,
968 "notNull": true,
969 "dimensions": 0,
970 "default": null,
971 "generated": null,
972 "identity": null,
973 "name": "updated_at",
974 "entityType": "columns",
975 "schema": "public",
976 "table": "secret_materials"
977 },
978 {
979 "type": "text",
980 "typeSchema": null,
981 "notNull": true,
982 "dimensions": 0,
983 "default": null,
984 "generated": null,
985 "identity": null,
986 "name": "id",
987 "entityType": "columns",
988 "schema": "public",
989 "table": "source_auth_sessions"
990 },
991 {
992 "type": "text",
993 "typeSchema": null,
994 "notNull": true,
995 "dimensions": 0,
996 "default": null,
997 "generated": null,
998 "identity": null,
999 "name": "workspace_id",
1000 "entityType": "columns",
1001 "schema": "public",
1002 "table": "source_auth_sessions"
1003 },
1004 {
1005 "type": "text",
1006 "typeSchema": null,
1007 "notNull": true,
1008 "dimensions": 0,
1009 "default": null,
1010 "generated": null,
1011 "identity": null,
1012 "name": "source_id",
1013 "entityType": "columns",
1014 "schema": "public",
1015 "table": "source_auth_sessions"
1016 },
1017 {
1018 "type": "text",
1019 "typeSchema": null,
1020 "notNull": false,
1021 "dimensions": 0,
1022 "default": null,
1023 "generated": null,
1024 "identity": null,
1025 "name": "execution_id",
1026 "entityType": "columns",
1027 "schema": "public",
1028 "table": "source_auth_sessions"
1029 },
1030 {
1031 "type": "text",
1032 "typeSchema": null,
1033 "notNull": false,
1034 "dimensions": 0,
1035 "default": null,
1036 "generated": null,
1037 "identity": null,
1038 "name": "interaction_id",
1039 "entityType": "columns",
1040 "schema": "public",
1041 "table": "source_auth_sessions"
1042 },
1043 {
1044 "type": "text",
1045 "typeSchema": null,
1046 "notNull": true,
1047 "dimensions": 0,
1048 "default": null,
1049 "generated": null,
1050 "identity": null,
1051 "name": "strategy",
1052 "entityType": "columns",
1053 "schema": "public",
1054 "table": "source_auth_sessions"
1055 },
1056 {
1057 "type": "text",
1058 "typeSchema": null,
1059 "notNull": true,
1060 "dimensions": 0,
1061 "default": null,
1062 "generated": null,
1063 "identity": null,
1064 "name": "status",
1065 "entityType": "columns",
1066 "schema": "public",
1067 "table": "source_auth_sessions"
1068 },
1069 {
1070 "type": "text",
1071 "typeSchema": null,
1072 "notNull": true,
1073 "dimensions": 0,
1074 "default": null,
1075 "generated": null,
1076 "identity": null,
1077 "name": "endpoint",
1078 "entityType": "columns",
1079 "schema": "public",
1080 "table": "source_auth_sessions"
1081 },
1082 {
1083 "type": "text",
1084 "typeSchema": null,
1085 "notNull": true,
1086 "dimensions": 0,
1087 "default": null,
1088 "generated": null,
1089 "identity": null,
1090 "name": "state",
1091 "entityType": "columns",
1092 "schema": "public",
1093 "table": "source_auth_sessions"
1094 },
1095 {
1096 "type": "text",
1097 "typeSchema": null,
1098 "notNull": true,
1099 "dimensions": 0,
1100 "default": null,
1101 "generated": null,
1102 "identity": null,
1103 "name": "redirect_uri",
1104 "entityType": "columns",
1105 "schema": "public",
1106 "table": "source_auth_sessions"
1107 },
1108 {
1109 "type": "text",
1110 "typeSchema": null,
1111 "notNull": false,
1112 "dimensions": 0,
1113 "default": null,
1114 "generated": null,
1115 "identity": null,
1116 "name": "scope",
1117 "entityType": "columns",
1118 "schema": "public",
1119 "table": "source_auth_sessions"
1120 },
1121 {
1122 "type": "text",
1123 "typeSchema": null,
1124 "notNull": false,
1125 "dimensions": 0,
1126 "default": null,
1127 "generated": null,
1128 "identity": null,
1129 "name": "resource_metadata_url",
1130 "entityType": "columns",
1131 "schema": "public",
1132 "table": "source_auth_sessions"
1133 },
1134 {
1135 "type": "text",
1136 "typeSchema": null,
1137 "notNull": false,
1138 "dimensions": 0,
1139 "default": null,
1140 "generated": null,
1141 "identity": null,
1142 "name": "authorization_server_url",
1143 "entityType": "columns",
1144 "schema": "public",
1145 "table": "source_auth_sessions"
1146 },
1147 {
1148 "type": "text",
1149 "typeSchema": null,
1150 "notNull": false,
1151 "dimensions": 0,
1152 "default": null,
1153 "generated": null,
1154 "identity": null,
1155 "name": "resource_metadata_json",
1156 "entityType": "columns",
1157 "schema": "public",
1158 "table": "source_auth_sessions"
1159 },
1160 {
1161 "type": "text",
1162 "typeSchema": null,
1163 "notNull": false,
1164 "dimensions": 0,
1165 "default": null,
1166 "generated": null,
1167 "identity": null,
1168 "name": "authorization_server_metadata_json",
1169 "entityType": "columns",
1170 "schema": "public",
1171 "table": "source_auth_sessions"
1172 },
1173 {
1174 "type": "text",
1175 "typeSchema": null,
1176 "notNull": false,
1177 "dimensions": 0,
1178 "default": null,
1179 "generated": null,
1180 "identity": null,
1181 "name": "client_information_json",
1182 "entityType": "columns",
1183 "schema": "public",
1184 "table": "source_auth_sessions"
1185 },
1186 {
1187 "type": "text",
1188 "typeSchema": null,
1189 "notNull": false,
1190 "dimensions": 0,
1191 "default": null,
1192 "generated": null,
1193 "identity": null,
1194 "name": "code_verifier",
1195 "entityType": "columns",
1196 "schema": "public",
1197 "table": "source_auth_sessions"
1198 },
1199 {
1200 "type": "text",
1201 "typeSchema": null,
1202 "notNull": false,
1203 "dimensions": 0,
1204 "default": null,
1205 "generated": null,
1206 "identity": null,
1207 "name": "authorization_url",
1208 "entityType": "columns",
1209 "schema": "public",
1210 "table": "source_auth_sessions"
1211 },
1212 {
1213 "type": "text",
1214 "typeSchema": null,
1215 "notNull": false,
1216 "dimensions": 0,
1217 "default": null,
1218 "generated": null,
1219 "identity": null,
1220 "name": "error_text",
1221 "entityType": "columns",
1222 "schema": "public",
1223 "table": "source_auth_sessions"
1224 },
1225 {
1226 "type": "bigint",
1227 "typeSchema": null,
1228 "notNull": false,
1229 "dimensions": 0,
1230 "default": null,
1231 "generated": null,
1232 "identity": null,
1233 "name": "completed_at",
1234 "entityType": "columns",
1235 "schema": "public",
1236 "table": "source_auth_sessions"
1237 },
1238 {
1239 "type": "bigint",
1240 "typeSchema": null,
1241 "notNull": true,
1242 "dimensions": 0,
1243 "default": null,
1244 "generated": null,
1245 "identity": null,
1246 "name": "created_at",
1247 "entityType": "columns",
1248 "schema": "public",
1249 "table": "source_auth_sessions"
1250 },
1251 {
1252 "type": "bigint",
1253 "typeSchema": null,
1254 "notNull": true,
1255 "dimensions": 0,
1256 "default": null,
1257 "generated": null,
1258 "identity": null,
1259 "name": "updated_at",
1260 "entityType": "columns",
1261 "schema": "public",
1262 "table": "source_auth_sessions"
1263 },
1264 {
1265 "type": "text",
1266 "typeSchema": null,
1267 "notNull": true,
1268 "dimensions": 0,
1269 "default": null,
1270 "generated": null,
1271 "identity": null,
1272 "name": "workspace_id",
1273 "entityType": "columns",
1274 "schema": "public",
1275 "table": "source_credential_bindings"
1276 },
1277 {
1278 "type": "text",
1279 "typeSchema": null,
1280 "notNull": true,
1281 "dimensions": 0,
1282 "default": null,
1283 "generated": null,
1284 "identity": null,
1285 "name": "source_id",
1286 "entityType": "columns",
1287 "schema": "public",
1288 "table": "source_credential_bindings"
1289 },
1290 {
1291 "type": "text",
1292 "typeSchema": null,
1293 "notNull": false,
1294 "dimensions": 0,
1295 "default": null,
1296 "generated": null,
1297 "identity": null,
1298 "name": "token_provider_id",
1299 "entityType": "columns",
1300 "schema": "public",
1301 "table": "source_credential_bindings"
1302 },
1303 {
1304 "type": "text",
1305 "typeSchema": null,
1306 "notNull": false,
1307 "dimensions": 0,
1308 "default": null,
1309 "generated": null,
1310 "identity": null,
1311 "name": "token_handle",
1312 "entityType": "columns",
1313 "schema": "public",
1314 "table": "source_credential_bindings"
1315 },
1316 {
1317 "type": "text",
1318 "typeSchema": null,
1319 "notNull": false,
1320 "dimensions": 0,
1321 "default": null,
1322 "generated": null,
1323 "identity": null,
1324 "name": "refresh_token_provider_id",
1325 "entityType": "columns",
1326 "schema": "public",
1327 "table": "source_credential_bindings"
1328 },
1329 {
1330 "type": "text",
1331 "typeSchema": null,
1332 "notNull": false,
1333 "dimensions": 0,
1334 "default": null,
1335 "generated": null,
1336 "identity": null,
1337 "name": "refresh_token_handle",
1338 "entityType": "columns",
1339 "schema": "public",
1340 "table": "source_credential_bindings"
1341 },
1342 {
1343 "type": "bigint",
1344 "typeSchema": null,
1345 "notNull": true,
1346 "dimensions": 0,
1347 "default": null,
1348 "generated": null,
1349 "identity": null,
1350 "name": "created_at",
1351 "entityType": "columns",
1352 "schema": "public",
1353 "table": "source_credential_bindings"
1354 },
1355 {
1356 "type": "bigint",
1357 "typeSchema": null,
1358 "notNull": true,
1359 "dimensions": 0,
1360 "default": null,
1361 "generated": null,
1362 "identity": null,
1363 "name": "updated_at",
1364 "entityType": "columns",
1365 "schema": "public",
1366 "table": "source_credential_bindings"
1367 },
1368 {
1369 "type": "text",
1370 "typeSchema": null,
1371 "notNull": true,
1372 "dimensions": 0,
1373 "default": null,
1374 "generated": null,
1375 "identity": null,
1376 "name": "workspace_id",
1377 "entityType": "columns",
1378 "schema": "public",
1379 "table": "sources"
1380 },
1381 {
1382 "type": "text",
1383 "typeSchema": null,
1384 "notNull": true,
1385 "dimensions": 0,
1386 "default": null,
1387 "generated": null,
1388 "identity": null,
1389 "name": "source_id",
1390 "entityType": "columns",
1391 "schema": "public",
1392 "table": "sources"
1393 },
1394 {
1395 "type": "text",
1396 "typeSchema": null,
1397 "notNull": true,
1398 "dimensions": 0,
1399 "default": null,
1400 "generated": null,
1401 "identity": null,
1402 "name": "name",
1403 "entityType": "columns",
1404 "schema": "public",
1405 "table": "sources"
1406 },
1407 {
1408 "type": "text",
1409 "typeSchema": null,
1410 "notNull": true,
1411 "dimensions": 0,
1412 "default": null,
1413 "generated": null,
1414 "identity": null,
1415 "name": "kind",
1416 "entityType": "columns",
1417 "schema": "public",
1418 "table": "sources"
1419 },
1420 {
1421 "type": "text",
1422 "typeSchema": null,
1423 "notNull": true,
1424 "dimensions": 0,
1425 "default": null,
1426 "generated": null,
1427 "identity": null,
1428 "name": "endpoint",
1429 "entityType": "columns",
1430 "schema": "public",
1431 "table": "sources"
1432 },
1433 {
1434 "type": "text",
1435 "typeSchema": null,
1436 "notNull": true,
1437 "dimensions": 0,
1438 "default": null,
1439 "generated": null,
1440 "identity": null,
1441 "name": "status",
1442 "entityType": "columns",
1443 "schema": "public",
1444 "table": "sources"
1445 },
1446 {
1447 "type": "boolean",
1448 "typeSchema": null,
1449 "notNull": true,
1450 "dimensions": 0,
1451 "default": null,
1452 "generated": null,
1453 "identity": null,
1454 "name": "enabled",
1455 "entityType": "columns",
1456 "schema": "public",
1457 "table": "sources"
1458 },
1459 {
1460 "type": "text",
1461 "typeSchema": null,
1462 "notNull": false,
1463 "dimensions": 0,
1464 "default": null,
1465 "generated": null,
1466 "identity": null,
1467 "name": "namespace",
1468 "entityType": "columns",
1469 "schema": "public",
1470 "table": "sources"
1471 },
1472 {
1473 "type": "text",
1474 "typeSchema": null,
1475 "notNull": false,
1476 "dimensions": 0,
1477 "default": null,
1478 "generated": null,
1479 "identity": null,
1480 "name": "transport",
1481 "entityType": "columns",
1482 "schema": "public",
1483 "table": "sources"
1484 },
1485 {
1486 "type": "text",
1487 "typeSchema": null,
1488 "notNull": false,
1489 "dimensions": 0,
1490 "default": null,
1491 "generated": null,
1492 "identity": null,
1493 "name": "query_params_json",
1494 "entityType": "columns",
1495 "schema": "public",
1496 "table": "sources"
1497 },
1498 {
1499 "type": "text",
1500 "typeSchema": null,
1501 "notNull": false,
1502 "dimensions": 0,
1503 "default": null,
1504 "generated": null,
1505 "identity": null,
1506 "name": "headers_json",
1507 "entityType": "columns",
1508 "schema": "public",
1509 "table": "sources"
1510 },
1511 {
1512 "type": "text",
1513 "typeSchema": null,
1514 "notNull": false,
1515 "dimensions": 0,
1516 "default": null,
1517 "generated": null,
1518 "identity": null,
1519 "name": "spec_url",
1520 "entityType": "columns",
1521 "schema": "public",
1522 "table": "sources"
1523 },
1524 {
1525 "type": "text",
1526 "typeSchema": null,
1527 "notNull": false,
1528 "dimensions": 0,
1529 "default": null,
1530 "generated": null,
1531 "identity": null,
1532 "name": "default_headers_json",
1533 "entityType": "columns",
1534 "schema": "public",
1535 "table": "sources"
1536 },
1537 {
1538 "type": "text",
1539 "typeSchema": null,
1540 "notNull": true,
1541 "dimensions": 0,
1542 "default": null,
1543 "generated": null,
1544 "identity": null,
1545 "name": "auth_kind",
1546 "entityType": "columns",
1547 "schema": "public",
1548 "table": "sources"
1549 },
1550 {
1551 "type": "text",
1552 "typeSchema": null,
1553 "notNull": false,
1554 "dimensions": 0,
1555 "default": null,
1556 "generated": null,
1557 "identity": null,
1558 "name": "auth_header_name",
1559 "entityType": "columns",
1560 "schema": "public",
1561 "table": "sources"
1562 },
1563 {
1564 "type": "text",
1565 "typeSchema": null,
1566 "notNull": false,
1567 "dimensions": 0,
1568 "default": null,
1569 "generated": null,
1570 "identity": null,
1571 "name": "auth_prefix",
1572 "entityType": "columns",
1573 "schema": "public",
1574 "table": "sources"
1575 },
1576 {
1577 "type": "text",
1578 "typeSchema": null,
1579 "notNull": false,
1580 "dimensions": 0,
1581 "default": null,
1582 "generated": null,
1583 "identity": null,
1584 "name": "source_hash",
1585 "entityType": "columns",
1586 "schema": "public",
1587 "table": "sources"
1588 },
1589 {
1590 "type": "text",
1591 "typeSchema": null,
1592 "notNull": false,
1593 "dimensions": 0,
1594 "default": null,
1595 "generated": null,
1596 "identity": null,
1597 "name": "last_error",
1598 "entityType": "columns",
1599 "schema": "public",
1600 "table": "sources"
1601 },
1602 {
1603 "type": "bigint",
1604 "typeSchema": null,
1605 "notNull": true,
1606 "dimensions": 0,
1607 "default": null,
1608 "generated": null,
1609 "identity": null,
1610 "name": "created_at",
1611 "entityType": "columns",
1612 "schema": "public",
1613 "table": "sources"
1614 },
1615 {
1616 "type": "bigint",
1617 "typeSchema": null,
1618 "notNull": true,
1619 "dimensions": 0,
1620 "default": null,
1621 "generated": null,
1622 "identity": null,
1623 "name": "updated_at",
1624 "entityType": "columns",
1625 "schema": "public",
1626 "table": "sources"
1627 },
1628 {
1629 "type": "text",
1630 "typeSchema": null,
1631 "notNull": true,
1632 "dimensions": 0,
1633 "default": null,
1634 "generated": null,
1635 "identity": null,
1636 "name": "id",
1637 "entityType": "columns",
1638 "schema": "public",
1639 "table": "workspaces"
1640 },
1641 {
1642 "type": "text",
1643 "typeSchema": null,
1644 "notNull": true,
1645 "dimensions": 0,
1646 "default": null,
1647 "generated": null,
1648 "identity": null,
1649 "name": "organization_id",
1650 "entityType": "columns",
1651 "schema": "public",
1652 "table": "workspaces"
1653 },
1654 {
1655 "type": "text",
1656 "typeSchema": null,
1657 "notNull": true,
1658 "dimensions": 0,
1659 "default": null,
1660 "generated": null,
1661 "identity": null,
1662 "name": "name",
1663 "entityType": "columns",
1664 "schema": "public",
1665 "table": "workspaces"
1666 },
1667 {
1668 "type": "text",
1669 "typeSchema": null,
1670 "notNull": false,
1671 "dimensions": 0,
1672 "default": null,
1673 "generated": null,
1674 "identity": null,
1675 "name": "created_by_account_id",
1676 "entityType": "columns",
1677 "schema": "public",
1678 "table": "workspaces"
1679 },
1680 {
1681 "type": "bigint",
1682 "typeSchema": null,
1683 "notNull": true,
1684 "dimensions": 0,
1685 "default": null,
1686 "generated": null,
1687 "identity": null,
1688 "name": "created_at",
1689 "entityType": "columns",
1690 "schema": "public",
1691 "table": "workspaces"
1692 },
1693 {
1694 "type": "bigint",
1695 "typeSchema": null,
1696 "notNull": true,
1697 "dimensions": 0,
1698 "default": null,
1699 "generated": null,
1700 "identity": null,
1701 "name": "updated_at",
1702 "entityType": "columns",
1703 "schema": "public",
1704 "table": "workspaces"
1705 },
1706 {
1707 "nameExplicit": true,
1708 "columns": [
1709 {
1710 "value": "provider",
1711 "isExpression": false,
1712 "asc": true,
1713 "nullsFirst": false,
1714 "opclass": null
1715 },
1716 {
1717 "value": "subject",
1718 "isExpression": false,
1719 "asc": true,
1720 "nullsFirst": false,
1721 "opclass": null
1722 }
1723 ],
1724 "isUnique": true,
1725 "where": null,
1726 "with": "",
1727 "method": "btree",
1728 "concurrently": false,
1729 "name": "accounts_provider_subject_idx",
1730 "entityType": "indexes",
1731 "schema": "public",
1732 "table": "accounts"
1733 },
1734 {
1735 "nameExplicit": true,
1736 "columns": [
1737 {
1738 "value": "updated_at",
1739 "isExpression": false,
1740 "asc": true,
1741 "nullsFirst": false,
1742 "opclass": null
1743 },
1744 {
1745 "value": "id",
1746 "isExpression": false,
1747 "asc": true,
1748 "nullsFirst": false,
1749 "opclass": null
1750 }
1751 ],
1752 "isUnique": false,
1753 "where": null,
1754 "with": "",
1755 "method": "btree",
1756 "concurrently": false,
1757 "name": "accounts_updated_idx",
1758 "entityType": "indexes",
1759 "schema": "public",
1760 "table": "accounts"
1761 },
1762 {
1763 "nameExplicit": true,
1764 "columns": [
1765 {
1766 "value": "execution_id",
1767 "isExpression": false,
1768 "asc": true,
1769 "nullsFirst": false,
1770 "opclass": null
1771 },
1772 {
1773 "value": "updated_at",
1774 "isExpression": false,
1775 "asc": true,
1776 "nullsFirst": false,
1777 "opclass": null
1778 },
1779 {
1780 "value": "id",
1781 "isExpression": false,
1782 "asc": true,
1783 "nullsFirst": false,
1784 "opclass": null
1785 }
1786 ],
1787 "isUnique": false,
1788 "where": null,
1789 "with": "",
1790 "method": "btree",
1791 "concurrently": false,
1792 "name": "execution_interactions_execution_idx",
1793 "entityType": "indexes",
1794 "schema": "public",
1795 "table": "execution_interactions"
1796 },
1797 {
1798 "nameExplicit": true,
1799 "columns": [
1800 {
1801 "value": "workspace_id",
1802 "isExpression": false,
1803 "asc": true,
1804 "nullsFirst": false,
1805 "opclass": null
1806 },
1807 {
1808 "value": "updated_at",
1809 "isExpression": false,
1810 "asc": true,
1811 "nullsFirst": false,
1812 "opclass": null
1813 },
1814 {
1815 "value": "id",
1816 "isExpression": false,
1817 "asc": true,
1818 "nullsFirst": false,
1819 "opclass": null
1820 }
1821 ],
1822 "isUnique": false,
1823 "where": null,
1824 "with": "",
1825 "method": "btree",
1826 "concurrently": false,
1827 "name": "executions_workspace_idx",
1828 "entityType": "indexes",
1829 "schema": "public",
1830 "table": "executions"
1831 },
1832 {
1833 "nameExplicit": true,
1834 "columns": [
1835 {
1836 "value": "organization_id",
1837 "isExpression": false,
1838 "asc": true,
1839 "nullsFirst": false,
1840 "opclass": null
1841 }
1842 ],
1843 "isUnique": false,
1844 "where": null,
1845 "with": "",
1846 "method": "btree",
1847 "concurrently": false,
1848 "name": "organization_memberships_org_idx",
1849 "entityType": "indexes",
1850 "schema": "public",
1851 "table": "organization_memberships"
1852 },
1853 {
1854 "nameExplicit": true,
1855 "columns": [
1856 {
1857 "value": "account_id",
1858 "isExpression": false,
1859 "asc": true,
1860 "nullsFirst": false,
1861 "opclass": null
1862 }
1863 ],
1864 "isUnique": false,
1865 "where": null,
1866 "with": "",
1867 "method": "btree",
1868 "concurrently": false,
1869 "name": "organization_memberships_account_idx",
1870 "entityType": "indexes",
1871 "schema": "public",
1872 "table": "organization_memberships"
1873 },
1874 {
1875 "nameExplicit": true,
1876 "columns": [
1877 {
1878 "value": "organization_id",
1879 "isExpression": false,
1880 "asc": true,
1881 "nullsFirst": false,
1882 "opclass": null
1883 },
1884 {
1885 "value": "account_id",
1886 "isExpression": false,
1887 "asc": true,
1888 "nullsFirst": false,
1889 "opclass": null
1890 }
1891 ],
1892 "isUnique": true,
1893 "where": null,
1894 "with": "",
1895 "method": "btree",
1896 "concurrently": false,
1897 "name": "organization_memberships_org_account_idx",
1898 "entityType": "indexes",
1899 "schema": "public",
1900 "table": "organization_memberships"
1901 },
1902 {
1903 "nameExplicit": true,
1904 "columns": [
1905 {
1906 "value": "slug",
1907 "isExpression": false,
1908 "asc": true,
1909 "nullsFirst": false,
1910 "opclass": null
1911 }
1912 ],
1913 "isUnique": true,
1914 "where": null,
1915 "with": "",
1916 "method": "btree",
1917 "concurrently": false,
1918 "name": "organizations_slug_idx",
1919 "entityType": "indexes",
1920 "schema": "public",
1921 "table": "organizations"
1922 },
1923 {
1924 "nameExplicit": true,
1925 "columns": [
1926 {
1927 "value": "updated_at",
1928 "isExpression": false,
1929 "asc": true,
1930 "nullsFirst": false,
1931 "opclass": null
1932 },
1933 {
1934 "value": "id",
1935 "isExpression": false,
1936 "asc": true,
1937 "nullsFirst": false,
1938 "opclass": null
1939 }
1940 ],
1941 "isUnique": false,
1942 "where": null,
1943 "with": "",
1944 "method": "btree",
1945 "concurrently": false,
1946 "name": "organizations_updated_idx",
1947 "entityType": "indexes",
1948 "schema": "public",
1949 "table": "organizations"
1950 },
1951 {
1952 "nameExplicit": true,
1953 "columns": [
1954 {
1955 "value": "workspace_id",
1956 "isExpression": false,
1957 "asc": true,
1958 "nullsFirst": false,
1959 "opclass": null
1960 },
1961 {
1962 "value": "updated_at",
1963 "isExpression": false,
1964 "asc": true,
1965 "nullsFirst": false,
1966 "opclass": null
1967 },
1968 {
1969 "value": "id",
1970 "isExpression": false,
1971 "asc": true,
1972 "nullsFirst": false,
1973 "opclass": null
1974 }
1975 ],
1976 "isUnique": false,
1977 "where": null,
1978 "with": "",
1979 "method": "btree",
1980 "concurrently": false,
1981 "name": "policies_workspace_idx",
1982 "entityType": "indexes",
1983 "schema": "public",
1984 "table": "policies"
1985 },
1986 {
1987 "nameExplicit": true,
1988 "columns": [
1989 {
1990 "value": "updated_at",
1991 "isExpression": false,
1992 "asc": true,
1993 "nullsFirst": false,
1994 "opclass": null
1995 },
1996 {
1997 "value": "id",
1998 "isExpression": false,
1999 "asc": true,
2000 "nullsFirst": false,
2001 "opclass": null
2002 }
2003 ],
2004 "isUnique": false,
2005 "where": null,
2006 "with": "",
2007 "method": "btree",
2008 "concurrently": false,
2009 "name": "secret_materials_updated_idx",
2010 "entityType": "indexes",
2011 "schema": "public",
2012 "table": "secret_materials"
2013 },
2014 {
2015 "nameExplicit": true,
2016 "columns": [
2017 {
2018 "value": "workspace_id",
2019 "isExpression": false,
2020 "asc": true,
2021 "nullsFirst": false,
2022 "opclass": null
2023 },
2024 {
2025 "value": "updated_at",
2026 "isExpression": false,
2027 "asc": true,
2028 "nullsFirst": false,
2029 "opclass": null
2030 },
2031 {
2032 "value": "id",
2033 "isExpression": false,
2034 "asc": true,
2035 "nullsFirst": false,
2036 "opclass": null
2037 }
2038 ],
2039 "isUnique": false,
2040 "where": null,
2041 "with": "",
2042 "method": "btree",
2043 "concurrently": false,
2044 "name": "source_auth_sessions_workspace_idx",
2045 "entityType": "indexes",
2046 "schema": "public",
2047 "table": "source_auth_sessions"
2048 },
2049 {
2050 "nameExplicit": true,
2051 "columns": [
2052 {
2053 "value": "state",
2054 "isExpression": false,
2055 "asc": true,
2056 "nullsFirst": false,
2057 "opclass": null
2058 }
2059 ],
2060 "isUnique": true,
2061 "where": null,
2062 "with": "",
2063 "method": "btree",
2064 "concurrently": false,
2065 "name": "source_auth_sessions_state_idx",
2066 "entityType": "indexes",
2067 "schema": "public",
2068 "table": "source_auth_sessions"
2069 },
2070 {
2071 "nameExplicit": true,
2072 "columns": [
2073 {
2074 "value": "workspace_id",
2075 "isExpression": false,
2076 "asc": true,
2077 "nullsFirst": false,
2078 "opclass": null
2079 },
2080 {
2081 "value": "updated_at",
2082 "isExpression": false,
2083 "asc": true,
2084 "nullsFirst": false,
2085 "opclass": null
2086 },
2087 {
2088 "value": "source_id",
2089 "isExpression": false,
2090 "asc": true,
2091 "nullsFirst": false,
2092 "opclass": null
2093 }
2094 ],
2095 "isUnique": false,
2096 "where": null,
2097 "with": "",
2098 "method": "btree",
2099 "concurrently": false,
2100 "name": "source_credential_bindings_workspace_idx",
2101 "entityType": "indexes",
2102 "schema": "public",
2103 "table": "source_credential_bindings"
2104 },
2105 {
2106 "nameExplicit": true,
2107 "columns": [
2108 {
2109 "value": "workspace_id",
2110 "isExpression": false,
2111 "asc": true,
2112 "nullsFirst": false,
2113 "opclass": null
2114 },
2115 {
2116 "value": "name",
2117 "isExpression": false,
2118 "asc": true,
2119 "nullsFirst": false,
2120 "opclass": null
2121 }
2122 ],
2123 "isUnique": true,
2124 "where": null,
2125 "with": "",
2126 "method": "btree",
2127 "concurrently": false,
2128 "name": "sources_workspace_name_idx",
2129 "entityType": "indexes",
2130 "schema": "public",
2131 "table": "sources"
2132 },
2133 {
2134 "nameExplicit": true,
2135 "columns": [
2136 {
2137 "value": "organization_id",
2138 "isExpression": false,
2139 "asc": true,
2140 "nullsFirst": false,
2141 "opclass": null
2142 }
2143 ],
2144 "isUnique": false,
2145 "where": null,
2146 "with": "",
2147 "method": "btree",
2148 "concurrently": false,
2149 "name": "workspaces_org_idx",
2150 "entityType": "indexes",
2151 "schema": "public",
2152 "table": "workspaces"
2153 },
2154 {
2155 "nameExplicit": true,
2156 "columns": [
2157 {
2158 "value": "organization_id",
2159 "isExpression": false,
2160 "asc": true,
2161 "nullsFirst": false,
2162 "opclass": null
2163 },
2164 {
2165 "value": "name",
2166 "isExpression": false,
2167 "asc": true,
2168 "nullsFirst": false,
2169 "opclass": null
2170 }
2171 ],
2172 "isUnique": true,
2173 "where": null,
2174 "with": "",
2175 "method": "btree",
2176 "concurrently": false,
2177 "name": "workspaces_org_name_idx",
2178 "entityType": "indexes",
2179 "schema": "public",
2180 "table": "workspaces"
2181 },
2182 {
2183 "columns": [
2184 "workspace_id",
2185 "source_id"
2186 ],
2187 "nameExplicit": false,
2188 "name": "source_credential_bindings_pkey",
2189 "entityType": "pks",
2190 "schema": "public",
2191 "table": "source_credential_bindings"
2192 },
2193 {
2194 "columns": [
2195 "workspace_id",
2196 "source_id"
2197 ],
2198 "nameExplicit": false,
2199 "name": "sources_pkey",
2200 "entityType": "pks",
2201 "schema": "public",
2202 "table": "sources"
2203 },
2204 {
2205 "columns": [
2206 "id"
2207 ],
2208 "nameExplicit": false,
2209 "name": "accounts_pkey",
2210 "schema": "public",
2211 "table": "accounts",
2212 "entityType": "pks"
2213 },
2214 {
2215 "columns": [
2216 "id"
2217 ],
2218 "nameExplicit": false,
2219 "name": "execution_interactions_pkey",
2220 "schema": "public",
2221 "table": "execution_interactions",
2222 "entityType": "pks"
2223 },
2224 {
2225 "columns": [
2226 "id"
2227 ],
2228 "nameExplicit": false,
2229 "name": "executions_pkey",
2230 "schema": "public",
2231 "table": "executions",
2232 "entityType": "pks"
2233 },
2234 {
2235 "columns": [
2236 "id"
2237 ],
2238 "nameExplicit": false,
2239 "name": "local_installations_pkey",
2240 "schema": "public",
2241 "table": "local_installations",
2242 "entityType": "pks"
2243 },
2244 {
2245 "columns": [
2246 "id"
2247 ],
2248 "nameExplicit": false,
2249 "name": "organization_memberships_pkey",
2250 "schema": "public",
2251 "table": "organization_memberships",
2252 "entityType": "pks"
2253 },
2254 {
2255 "columns": [
2256 "id"
2257 ],
2258 "nameExplicit": false,
2259 "name": "organizations_pkey",
2260 "schema": "public",
2261 "table": "organizations",
2262 "entityType": "pks"
2263 },
2264 {
2265 "columns": [
2266 "id"
2267 ],
2268 "nameExplicit": false,
2269 "name": "policies_pkey",
2270 "schema": "public",
2271 "table": "policies",
2272 "entityType": "pks"
2273 },
2274 {
2275 "columns": [
2276 "id"
2277 ],
2278 "nameExplicit": false,
2279 "name": "secret_materials_pkey",
2280 "schema": "public",
2281 "table": "secret_materials",
2282 "entityType": "pks"
2283 },
2284 {
2285 "columns": [
2286 "id"
2287 ],
2288 "nameExplicit": false,
2289 "name": "source_auth_sessions_pkey",
2290 "schema": "public",
2291 "table": "source_auth_sessions",
2292 "entityType": "pks"
2293 },
2294 {
2295 "columns": [
2296 "id"
2297 ],
2298 "nameExplicit": false,
2299 "name": "workspaces_pkey",
2300 "schema": "public",
2301 "table": "workspaces",
2302 "entityType": "pks"
2303 },
2304 {
2305 "value": "\"provider\" in ('local', 'workos', 'service')",
2306 "name": "accounts_provider_check",
2307 "entityType": "checks",
2308 "schema": "public",
2309 "table": "accounts"
2310 },
2311 {
2312 "value": "\"status\" in ('pending', 'resolved', 'cancelled')",
2313 "name": "execution_interactions_status_check",
2314 "entityType": "checks",
2315 "schema": "public",
2316 "table": "execution_interactions"
2317 },
2318 {
2319 "value": "\"status\" in ('pending', 'running', 'waiting_for_interaction', 'completed', 'failed', 'cancelled')",
2320 "name": "executions_status_check",
2321 "entityType": "checks",
2322 "schema": "public",
2323 "table": "executions"
2324 },
2325 {
2326 "value": "\"role\" in ('viewer', 'editor', 'admin', 'owner')",
2327 "name": "organization_memberships_role_check",
2328 "entityType": "checks",
2329 "schema": "public",
2330 "table": "organization_memberships"
2331 },
2332 {
2333 "value": "\"status\" in ('invited', 'active', 'suspended', 'removed')",
2334 "name": "organization_memberships_status_check",
2335 "entityType": "checks",
2336 "schema": "public",
2337 "table": "organization_memberships"
2338 },
2339 {
2340 "value": "\"status\" in ('active', 'suspended', 'archived')",
2341 "name": "organizations_status_check",
2342 "entityType": "checks",
2343 "schema": "public",
2344 "table": "organizations"
2345 },
2346 {
2347 "value": "\"resource_type\" in ('all_tools', 'source', 'namespace', 'tool_path')",
2348 "name": "policies_resource_type_check",
2349 "entityType": "checks",
2350 "schema": "public",
2351 "table": "policies"
2352 },
2353 {
2354 "value": "\"match_type\" in ('glob', 'exact')",
2355 "name": "policies_match_type_check",
2356 "entityType": "checks",
2357 "schema": "public",
2358 "table": "policies"
2359 },
2360 {
2361 "value": "\"effect\" in ('allow', 'deny')",
2362 "name": "policies_effect_check",
2363 "entityType": "checks",
2364 "schema": "public",
2365 "table": "policies"
2366 },
2367 {
2368 "value": "\"approval_mode\" in ('auto', 'required')",
2369 "name": "policies_approval_mode_check",
2370 "entityType": "checks",
2371 "schema": "public",
2372 "table": "policies"
2373 },
2374 {
2375 "value": "\"purpose\" in ('auth_material', 'oauth_access_token', 'oauth_refresh_token', 'oauth_client_info')",
2376 "name": "secret_materials_purpose_check",
2377 "entityType": "checks",
2378 "schema": "public",
2379 "table": "secret_materials"
2380 },
2381 {
2382 "value": "\"strategy\" in ('oauth2_authorization_code')",
2383 "name": "source_auth_sessions_strategy_check",
2384 "entityType": "checks",
2385 "schema": "public",
2386 "table": "source_auth_sessions"
2387 },
2388 {
2389 "value": "\"status\" in ('pending', 'completed', 'failed', 'cancelled')",
2390 "name": "source_auth_sessions_status_check",
2391 "entityType": "checks",
2392 "schema": "public",
2393 "table": "source_auth_sessions"
2394 },
2395 {
2396 "value": "\"kind\" in ('mcp', 'openapi', 'graphql', 'internal')",
2397 "name": "sources_kind_check",
2398 "entityType": "checks",
2399 "schema": "public",
2400 "table": "sources"
2401 },
2402 {
2403 "value": "\"status\" in ('draft', 'probing', 'auth_required', 'connected', 'error')",
2404 "name": "sources_status_check",
2405 "entityType": "checks",
2406 "schema": "public",
2407 "table": "sources"
2408 },
2409 {
2410 "value": "\"transport\" is null or \"transport\" in ('auto', 'streamable-http', 'sse')",
2411 "name": "sources_transport_check",
2412 "entityType": "checks",
2413 "schema": "public",
2414 "table": "sources"
2415 },
2416 {
2417 "value": "\"auth_kind\" in ('none', 'bearer', 'oauth2')",
2418 "name": "sources_auth_kind_check",
2419 "entityType": "checks",
2420 "schema": "public",
2421 "table": "sources"
2422 }
2423 ],
2424 "renames": []
2425}
packages/control-plane/drizzle/20260307090915_blue_bullseye/migration.sql+0−70
@@ -1,70 +0,0 @@
1CREATE TABLE "tool_artifact_parameters" (
2 "workspace_id" text,
3 "path" text,
4 "position" bigint,
5 "name" text NOT NULL,
6 "location" text NOT NULL,
7 "required" boolean NOT NULL,
8 CONSTRAINT "tool_artifact_parameters_pkey" PRIMARY KEY("workspace_id","path","position"),
9 CONSTRAINT "tool_artifact_parameters_location_check" CHECK ("location" in ('path', 'query', 'header', 'cookie'))
10);
11--> statement-breakpoint
12CREATE TABLE "tool_artifact_ref_hint_keys" (
13 "workspace_id" text,
14 "path" text,
15 "position" bigint,
16 "ref_hint_key" text NOT NULL,
17 CONSTRAINT "tool_artifact_ref_hint_keys_pkey" PRIMARY KEY("workspace_id","path","position")
18);
19--> statement-breakpoint
20CREATE TABLE "tool_artifact_request_body_content_types" (
21 "workspace_id" text,
22 "path" text,
23 "position" bigint,
24 "content_type" text NOT NULL,
25 CONSTRAINT "tool_artifact_request_body_content_types_pkey" PRIMARY KEY("workspace_id","path","position")
26);
27--> statement-breakpoint
28CREATE TABLE "tool_artifacts" (
29 "workspace_id" text,
30 "path" text,
31 "tool_id" text NOT NULL,
32 "source_id" text NOT NULL,
33 "title" text,
34 "description" text,
35 "search_namespace" text NOT NULL,
36 "search_text" text NOT NULL,
37 "input_schema_json" text,
38 "output_schema_json" text,
39 "provider_kind" text NOT NULL,
40 "mcp_tool_name" text,
41 "openapi_method" text,
42 "openapi_path_template" text,
43 "openapi_operation_hash" text,
44 "openapi_request_body_required" boolean,
45 "created_at" bigint NOT NULL,
46 "updated_at" bigint NOT NULL,
47 CONSTRAINT "tool_artifacts_pkey" PRIMARY KEY("workspace_id","path"),
48 CONSTRAINT "tool_artifacts_provider_kind_check" CHECK ("provider_kind" in ('mcp', 'openapi')),
49 CONSTRAINT "tool_artifacts_mcp_shape_check" CHECK ("provider_kind" <> 'mcp'
50 or (
51 "mcp_tool_name" is not null
52 and "openapi_method" is null
53 and "openapi_path_template" is null
54 and "openapi_operation_hash" is null
55 and "openapi_request_body_required" is null
56 )),
57 CONSTRAINT "tool_artifacts_openapi_shape_check" CHECK ("provider_kind" <> 'openapi'
58 or (
59 "mcp_tool_name" is null
60 and "openapi_method" in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace')
61 and "openapi_path_template" is not null
62 and "openapi_operation_hash" is not null
63 ))
64);
65--> statement-breakpoint
66CREATE INDEX "tool_artifact_parameters_lookup_idx" ON "tool_artifact_parameters" ("workspace_id","path","position");--> statement-breakpoint
67CREATE INDEX "tool_artifact_ref_hint_keys_lookup_idx" ON "tool_artifact_ref_hint_keys" ("workspace_id","path","position");--> statement-breakpoint
68CREATE INDEX "tool_artifact_request_body_content_types_lookup_idx" ON "tool_artifact_request_body_content_types" ("workspace_id","path","position");--> statement-breakpoint
69CREATE INDEX "tool_artifacts_workspace_source_idx" ON "tool_artifacts" ("workspace_id","source_id","updated_at","path");--> statement-breakpoint
70CREATE INDEX "tool_artifacts_workspace_namespace_idx" ON "tool_artifacts" ("workspace_id","search_namespace","path");
packages/control-plane/drizzle/20260307090915_blue_bullseye/snapshot.json+0−3122
@@ -1,3122 +0,0 @@
1{
2 "version": "8",
3 "dialect": "postgres",
4 "id": "3e9aa0ec-194c-4364-9e57-4539db8fb2ad",
5 "prevIds": [
6 "1141497b-c500-44ff-87db-6a440062becb"
7 ],
8 "ddl": [
9 {
10 "isRlsEnabled": false,
11 "name": "accounts",
12 "entityType": "tables",
13 "schema": "public"
14 },
15 {
16 "isRlsEnabled": false,
17 "name": "execution_interactions",
18 "entityType": "tables",
19 "schema": "public"
20 },
21 {
22 "isRlsEnabled": false,
23 "name": "executions",
24 "entityType": "tables",
25 "schema": "public"
26 },
27 {
28 "isRlsEnabled": false,
29 "name": "local_installations",
30 "entityType": "tables",
31 "schema": "public"
32 },
33 {
34 "isRlsEnabled": false,
35 "name": "organization_memberships",
36 "entityType": "tables",
37 "schema": "public"
38 },
39 {
40 "isRlsEnabled": false,
41 "name": "organizations",
42 "entityType": "tables",
43 "schema": "public"
44 },
45 {
46 "isRlsEnabled": false,
47 "name": "policies",
48 "entityType": "tables",
49 "schema": "public"
50 },
51 {
52 "isRlsEnabled": false,
53 "name": "secret_materials",
54 "entityType": "tables",
55 "schema": "public"
56 },
57 {
58 "isRlsEnabled": false,
59 "name": "source_auth_sessions",
60 "entityType": "tables",
61 "schema": "public"
62 },
63 {
64 "isRlsEnabled": false,
65 "name": "source_credential_bindings",
66 "entityType": "tables",
67 "schema": "public"
68 },
69 {
70 "isRlsEnabled": false,
71 "name": "sources",
72 "entityType": "tables",
73 "schema": "public"
74 },
75 {
76 "isRlsEnabled": false,
77 "name": "tool_artifact_parameters",
78 "entityType": "tables",
79 "schema": "public"
80 },
81 {
82 "isRlsEnabled": false,
83 "name": "tool_artifact_ref_hint_keys",
84 "entityType": "tables",
85 "schema": "public"
86 },
87 {
88 "isRlsEnabled": false,
89 "name": "tool_artifact_request_body_content_types",
90 "entityType": "tables",
91 "schema": "public"
92 },
93 {
94 "isRlsEnabled": false,
95 "name": "tool_artifacts",
96 "entityType": "tables",
97 "schema": "public"
98 },
99 {
100 "isRlsEnabled": false,
101 "name": "workspaces",
102 "entityType": "tables",
103 "schema": "public"
104 },
105 {
106 "type": "text",
107 "typeSchema": null,
108 "notNull": true,
109 "dimensions": 0,
110 "default": null,
111 "generated": null,
112 "identity": null,
113 "name": "id",
114 "entityType": "columns",
115 "schema": "public",
116 "table": "accounts"
117 },
118 {
119 "type": "text",
120 "typeSchema": null,
121 "notNull": true,
122 "dimensions": 0,
123 "default": null,
124 "generated": null,
125 "identity": null,
126 "name": "provider",
127 "entityType": "columns",
128 "schema": "public",
129 "table": "accounts"
130 },
131 {
132 "type": "text",
133 "typeSchema": null,
134 "notNull": true,
135 "dimensions": 0,
136 "default": null,
137 "generated": null,
138 "identity": null,
139 "name": "subject",
140 "entityType": "columns",
141 "schema": "public",
142 "table": "accounts"
143 },
144 {
145 "type": "text",
146 "typeSchema": null,
147 "notNull": false,
148 "dimensions": 0,
149 "default": null,
150 "generated": null,
151 "identity": null,
152 "name": "email",
153 "entityType": "columns",
154 "schema": "public",
155 "table": "accounts"
156 },
157 {
158 "type": "text",
159 "typeSchema": null,
160 "notNull": false,
161 "dimensions": 0,
162 "default": null,
163 "generated": null,
164 "identity": null,
165 "name": "display_name",
166 "entityType": "columns",
167 "schema": "public",
168 "table": "accounts"
169 },
170 {
171 "type": "bigint",
172 "typeSchema": null,
173 "notNull": true,
174 "dimensions": 0,
175 "default": null,
176 "generated": null,
177 "identity": null,
178 "name": "created_at",
179 "entityType": "columns",
180 "schema": "public",
181 "table": "accounts"
182 },
183 {
184 "type": "bigint",
185 "typeSchema": null,
186 "notNull": true,
187 "dimensions": 0,
188 "default": null,
189 "generated": null,
190 "identity": null,
191 "name": "updated_at",
192 "entityType": "columns",
193 "schema": "public",
194 "table": "accounts"
195 },
196 {
197 "type": "text",
198 "typeSchema": null,
199 "notNull": true,
200 "dimensions": 0,
201 "default": null,
202 "generated": null,
203 "identity": null,
204 "name": "id",
205 "entityType": "columns",
206 "schema": "public",
207 "table": "execution_interactions"
208 },
209 {
210 "type": "text",
211 "typeSchema": null,
212 "notNull": true,
213 "dimensions": 0,
214 "default": null,
215 "generated": null,
216 "identity": null,
217 "name": "execution_id",
218 "entityType": "columns",
219 "schema": "public",
220 "table": "execution_interactions"
221 },
222 {
223 "type": "text",
224 "typeSchema": null,
225 "notNull": true,
226 "dimensions": 0,
227 "default": null,
228 "generated": null,
229 "identity": null,
230 "name": "status",
231 "entityType": "columns",
232 "schema": "public",
233 "table": "execution_interactions"
234 },
235 {
236 "type": "text",
237 "typeSchema": null,
238 "notNull": true,
239 "dimensions": 0,
240 "default": null,
241 "generated": null,
242 "identity": null,
243 "name": "kind",
244 "entityType": "columns",
245 "schema": "public",
246 "table": "execution_interactions"
247 },
248 {
249 "type": "text",
250 "typeSchema": null,
251 "notNull": true,
252 "dimensions": 0,
253 "default": null,
254 "generated": null,
255 "identity": null,
256 "name": "payload_json",
257 "entityType": "columns",
258 "schema": "public",
259 "table": "execution_interactions"
260 },
261 {
262 "type": "text",
263 "typeSchema": null,
264 "notNull": false,
265 "dimensions": 0,
266 "default": null,
267 "generated": null,
268 "identity": null,
269 "name": "response_json",
270 "entityType": "columns",
271 "schema": "public",
272 "table": "execution_interactions"
273 },
274 {
275 "type": "bigint",
276 "typeSchema": null,
277 "notNull": true,
278 "dimensions": 0,
279 "default": null,
280 "generated": null,
281 "identity": null,
282 "name": "created_at",
283 "entityType": "columns",
284 "schema": "public",
285 "table": "execution_interactions"
286 },
287 {
288 "type": "bigint",
289 "typeSchema": null,
290 "notNull": true,
291 "dimensions": 0,
292 "default": null,
293 "generated": null,
294 "identity": null,
295 "name": "updated_at",
296 "entityType": "columns",
297 "schema": "public",
298 "table": "execution_interactions"
299 },
300 {
301 "type": "text",
302 "typeSchema": null,
303 "notNull": true,
304 "dimensions": 0,
305 "default": null,
306 "generated": null,
307 "identity": null,
308 "name": "id",
309 "entityType": "columns",
310 "schema": "public",
311 "table": "executions"
312 },
313 {
314 "type": "text",
315 "typeSchema": null,
316 "notNull": true,
317 "dimensions": 0,
318 "default": null,
319 "generated": null,
320 "identity": null,
321 "name": "workspace_id",
322 "entityType": "columns",
323 "schema": "public",
324 "table": "executions"
325 },
326 {
327 "type": "text",
328 "typeSchema": null,
329 "notNull": true,
330 "dimensions": 0,
331 "default": null,
332 "generated": null,
333 "identity": null,
334 "name": "created_by_account_id",
335 "entityType": "columns",
336 "schema": "public",
337 "table": "executions"
338 },
339 {
340 "type": "text",
341 "typeSchema": null,
342 "notNull": true,
343 "dimensions": 0,
344 "default": null,
345 "generated": null,
346 "identity": null,
347 "name": "status",
348 "entityType": "columns",
349 "schema": "public",
350 "table": "executions"
351 },
352 {
353 "type": "text",
354 "typeSchema": null,
355 "notNull": true,
356 "dimensions": 0,
357 "default": null,
358 "generated": null,
359 "identity": null,
360 "name": "code",
361 "entityType": "columns",
362 "schema": "public",
363 "table": "executions"
364 },
365 {
366 "type": "text",
367 "typeSchema": null,
368 "notNull": false,
369 "dimensions": 0,
370 "default": null,
371 "generated": null,
372 "identity": null,
373 "name": "result_json",
374 "entityType": "columns",
375 "schema": "public",
376 "table": "executions"
377 },
378 {
379 "type": "text",
380 "typeSchema": null,
381 "notNull": false,
382 "dimensions": 0,
383 "default": null,
384 "generated": null,
385 "identity": null,
386 "name": "error_text",
387 "entityType": "columns",
388 "schema": "public",
389 "table": "executions"
390 },
391 {
392 "type": "text",
393 "typeSchema": null,
394 "notNull": false,
395 "dimensions": 0,
396 "default": null,
397 "generated": null,
398 "identity": null,
399 "name": "logs_json",
400 "entityType": "columns",
401 "schema": "public",
402 "table": "executions"
403 },
404 {
405 "type": "bigint",
406 "typeSchema": null,
407 "notNull": false,
408 "dimensions": 0,
409 "default": null,
410 "generated": null,
411 "identity": null,
412 "name": "started_at",
413 "entityType": "columns",
414 "schema": "public",
415 "table": "executions"
416 },
417 {
418 "type": "bigint",
419 "typeSchema": null,
420 "notNull": false,
421 "dimensions": 0,
422 "default": null,
423 "generated": null,
424 "identity": null,
425 "name": "completed_at",
426 "entityType": "columns",
427 "schema": "public",
428 "table": "executions"
429 },
430 {
431 "type": "bigint",
432 "typeSchema": null,
433 "notNull": true,
434 "dimensions": 0,
435 "default": null,
436 "generated": null,
437 "identity": null,
438 "name": "created_at",
439 "entityType": "columns",
440 "schema": "public",
441 "table": "executions"
442 },
443 {
444 "type": "bigint",
445 "typeSchema": null,
446 "notNull": true,
447 "dimensions": 0,
448 "default": null,
449 "generated": null,
450 "identity": null,
451 "name": "updated_at",
452 "entityType": "columns",
453 "schema": "public",
454 "table": "executions"
455 },
456 {
457 "type": "text",
458 "typeSchema": null,
459 "notNull": true,
460 "dimensions": 0,
461 "default": null,
462 "generated": null,
463 "identity": null,
464 "name": "id",
465 "entityType": "columns",
466 "schema": "public",
467 "table": "local_installations"
468 },
469 {
470 "type": "text",
471 "typeSchema": null,
472 "notNull": true,
473 "dimensions": 0,
474 "default": null,
475 "generated": null,
476 "identity": null,
477 "name": "account_id",
478 "entityType": "columns",
479 "schema": "public",
480 "table": "local_installations"
481 },
482 {
483 "type": "text",
484 "typeSchema": null,
485 "notNull": true,
486 "dimensions": 0,
487 "default": null,
488 "generated": null,
489 "identity": null,
490 "name": "organization_id",
491 "entityType": "columns",
492 "schema": "public",
493 "table": "local_installations"
494 },
495 {
496 "type": "text",
497 "typeSchema": null,
498 "notNull": true,
499 "dimensions": 0,
500 "default": null,
501 "generated": null,
502 "identity": null,
503 "name": "workspace_id",
504 "entityType": "columns",
505 "schema": "public",
506 "table": "local_installations"
507 },
508 {
509 "type": "bigint",
510 "typeSchema": null,
511 "notNull": true,
512 "dimensions": 0,
513 "default": null,
514 "generated": null,
515 "identity": null,
516 "name": "created_at",
517 "entityType": "columns",
518 "schema": "public",
519 "table": "local_installations"
520 },
521 {
522 "type": "bigint",
523 "typeSchema": null,
524 "notNull": true,
525 "dimensions": 0,
526 "default": null,
527 "generated": null,
528 "identity": null,
529 "name": "updated_at",
530 "entityType": "columns",
531 "schema": "public",
532 "table": "local_installations"
533 },
534 {
535 "type": "text",
536 "typeSchema": null,
537 "notNull": true,
538 "dimensions": 0,
539 "default": null,
540 "generated": null,
541 "identity": null,
542 "name": "id",
543 "entityType": "columns",
544 "schema": "public",
545 "table": "organization_memberships"
546 },
547 {
548 "type": "text",
549 "typeSchema": null,
550 "notNull": true,
551 "dimensions": 0,
552 "default": null,
553 "generated": null,
554 "identity": null,
555 "name": "organization_id",
556 "entityType": "columns",
557 "schema": "public",
558 "table": "organization_memberships"
559 },
560 {
561 "type": "text",
562 "typeSchema": null,
563 "notNull": true,
564 "dimensions": 0,
565 "default": null,
566 "generated": null,
567 "identity": null,
568 "name": "account_id",
569 "entityType": "columns",
570 "schema": "public",
571 "table": "organization_memberships"
572 },
573 {
574 "type": "text",
575 "typeSchema": null,
576 "notNull": true,
577 "dimensions": 0,
578 "default": null,
579 "generated": null,
580 "identity": null,
581 "name": "role",
582 "entityType": "columns",
583 "schema": "public",
584 "table": "organization_memberships"
585 },
586 {
587 "type": "text",
588 "typeSchema": null,
589 "notNull": true,
590 "dimensions": 0,
591 "default": null,
592 "generated": null,
593 "identity": null,
594 "name": "status",
595 "entityType": "columns",
596 "schema": "public",
597 "table": "organization_memberships"
598 },
599 {
600 "type": "boolean",
601 "typeSchema": null,
602 "notNull": true,
603 "dimensions": 0,
604 "default": null,
605 "generated": null,
606 "identity": null,
607 "name": "billable",
608 "entityType": "columns",
609 "schema": "public",
610 "table": "organization_memberships"
611 },
612 {
613 "type": "text",
614 "typeSchema": null,
615 "notNull": false,
616 "dimensions": 0,
617 "default": null,
618 "generated": null,
619 "identity": null,
620 "name": "invited_by_account_id",
621 "entityType": "columns",
622 "schema": "public",
623 "table": "organization_memberships"
624 },
625 {
626 "type": "bigint",
627 "typeSchema": null,
628 "notNull": false,
629 "dimensions": 0,
630 "default": null,
631 "generated": null,
632 "identity": null,
633 "name": "joined_at",
634 "entityType": "columns",
635 "schema": "public",
636 "table": "organization_memberships"
637 },
638 {
639 "type": "bigint",
640 "typeSchema": null,
641 "notNull": true,
642 "dimensions": 0,
643 "default": null,
644 "generated": null,
645 "identity": null,
646 "name": "created_at",
647 "entityType": "columns",
648 "schema": "public",
649 "table": "organization_memberships"
650 },
651 {
652 "type": "bigint",
653 "typeSchema": null,
654 "notNull": true,
655 "dimensions": 0,
656 "default": null,
657 "generated": null,
658 "identity": null,
659 "name": "updated_at",
660 "entityType": "columns",
661 "schema": "public",
662 "table": "organization_memberships"
663 },
664 {
665 "type": "text",
666 "typeSchema": null,
667 "notNull": true,
668 "dimensions": 0,
669 "default": null,
670 "generated": null,
671 "identity": null,
672 "name": "id",
673 "entityType": "columns",
674 "schema": "public",
675 "table": "organizations"
676 },
677 {
678 "type": "text",
679 "typeSchema": null,
680 "notNull": true,
681 "dimensions": 0,
682 "default": null,
683 "generated": null,
684 "identity": null,
685 "name": "slug",
686 "entityType": "columns",
687 "schema": "public",
688 "table": "organizations"
689 },
690 {
691 "type": "text",
692 "typeSchema": null,
693 "notNull": true,
694 "dimensions": 0,
695 "default": null,
696 "generated": null,
697 "identity": null,
698 "name": "name",
699 "entityType": "columns",
700 "schema": "public",
701 "table": "organizations"
702 },
703 {
704 "type": "text",
705 "typeSchema": null,
706 "notNull": true,
707 "dimensions": 0,
708 "default": null,
709 "generated": null,
710 "identity": null,
711 "name": "status",
712 "entityType": "columns",
713 "schema": "public",
714 "table": "organizations"
715 },
716 {
717 "type": "text",
718 "typeSchema": null,
719 "notNull": false,
720 "dimensions": 0,
721 "default": null,
722 "generated": null,
723 "identity": null,
724 "name": "created_by_account_id",
725 "entityType": "columns",
726 "schema": "public",
727 "table": "organizations"
728 },
729 {
730 "type": "bigint",
731 "typeSchema": null,
732 "notNull": true,
733 "dimensions": 0,
734 "default": null,
735 "generated": null,
736 "identity": null,
737 "name": "created_at",
738 "entityType": "columns",
739 "schema": "public",
740 "table": "organizations"
741 },
742 {
743 "type": "bigint",
744 "typeSchema": null,
745 "notNull": true,
746 "dimensions": 0,
747 "default": null,
748 "generated": null,
749 "identity": null,
750 "name": "updated_at",
751 "entityType": "columns",
752 "schema": "public",
753 "table": "organizations"
754 },
755 {
756 "type": "text",
757 "typeSchema": null,
758 "notNull": true,
759 "dimensions": 0,
760 "default": null,
761 "generated": null,
762 "identity": null,
763 "name": "id",
764 "entityType": "columns",
765 "schema": "public",
766 "table": "policies"
767 },
768 {
769 "type": "text",
770 "typeSchema": null,
771 "notNull": true,
772 "dimensions": 0,
773 "default": null,
774 "generated": null,
775 "identity": null,
776 "name": "workspace_id",
777 "entityType": "columns",
778 "schema": "public",
779 "table": "policies"
780 },
781 {
782 "type": "text",
783 "typeSchema": null,
784 "notNull": false,
785 "dimensions": 0,
786 "default": null,
787 "generated": null,
788 "identity": null,
789 "name": "target_account_id",
790 "entityType": "columns",
791 "schema": "public",
792 "table": "policies"
793 },
794 {
795 "type": "text",
796 "typeSchema": null,
797 "notNull": false,
798 "dimensions": 0,
799 "default": null,
800 "generated": null,
801 "identity": null,
802 "name": "client_id",
803 "entityType": "columns",
804 "schema": "public",
805 "table": "policies"
806 },
807 {
808 "type": "text",
809 "typeSchema": null,
810 "notNull": true,
811 "dimensions": 0,
812 "default": null,
813 "generated": null,
814 "identity": null,
815 "name": "resource_type",
816 "entityType": "columns",
817 "schema": "public",
818 "table": "policies"
819 },
820 {
821 "type": "text",
822 "typeSchema": null,
823 "notNull": true,
824 "dimensions": 0,
825 "default": null,
826 "generated": null,
827 "identity": null,
828 "name": "resource_pattern",
829 "entityType": "columns",
830 "schema": "public",
831 "table": "policies"
832 },
833 {
834 "type": "text",
835 "typeSchema": null,
836 "notNull": true,
837 "dimensions": 0,
838 "default": null,
839 "generated": null,
840 "identity": null,
841 "name": "match_type",
842 "entityType": "columns",
843 "schema": "public",
844 "table": "policies"
845 },
846 {
847 "type": "text",
848 "typeSchema": null,
849 "notNull": true,
850 "dimensions": 0,
851 "default": null,
852 "generated": null,
853 "identity": null,
854 "name": "effect",
855 "entityType": "columns",
856 "schema": "public",
857 "table": "policies"
858 },
859 {
860 "type": "text",
861 "typeSchema": null,
862 "notNull": true,
863 "dimensions": 0,
864 "default": null,
865 "generated": null,
866 "identity": null,
867 "name": "approval_mode",
868 "entityType": "columns",
869 "schema": "public",
870 "table": "policies"
871 },
872 {
873 "type": "text",
874 "typeSchema": null,
875 "notNull": false,
876 "dimensions": 0,
877 "default": null,
878 "generated": null,
879 "identity": null,
880 "name": "argument_conditions_json",
881 "entityType": "columns",
882 "schema": "public",
883 "table": "policies"
884 },
885 {
886 "type": "bigint",
887 "typeSchema": null,
888 "notNull": true,
889 "dimensions": 0,
890 "default": null,
891 "generated": null,
892 "identity": null,
893 "name": "priority",
894 "entityType": "columns",
895 "schema": "public",
896 "table": "policies"
897 },
898 {
899 "type": "boolean",
900 "typeSchema": null,
901 "notNull": true,
902 "dimensions": 0,
903 "default": null,
904 "generated": null,
905 "identity": null,
906 "name": "enabled",
907 "entityType": "columns",
908 "schema": "public",
909 "table": "policies"
910 },
911 {
912 "type": "bigint",
913 "typeSchema": null,
914 "notNull": true,
915 "dimensions": 0,
916 "default": null,
917 "generated": null,
918 "identity": null,
919 "name": "created_at",
920 "entityType": "columns",
921 "schema": "public",
922 "table": "policies"
923 },
924 {
925 "type": "bigint",
926 "typeSchema": null,
927 "notNull": true,
928 "dimensions": 0,
929 "default": null,
930 "generated": null,
931 "identity": null,
932 "name": "updated_at",
933 "entityType": "columns",
934 "schema": "public",
935 "table": "policies"
936 },
937 {
938 "type": "text",
939 "typeSchema": null,
940 "notNull": true,
941 "dimensions": 0,
942 "default": null,
943 "generated": null,
944 "identity": null,
945 "name": "id",
946 "entityType": "columns",
947 "schema": "public",
948 "table": "secret_materials"
949 },
950 {
951 "type": "text",
952 "typeSchema": null,
953 "notNull": true,
954 "dimensions": 0,
955 "default": null,
956 "generated": null,
957 "identity": null,
958 "name": "purpose",
959 "entityType": "columns",
960 "schema": "public",
961 "table": "secret_materials"
962 },
963 {
964 "type": "text",
965 "typeSchema": null,
966 "notNull": true,
967 "dimensions": 0,
968 "default": null,
969 "generated": null,
970 "identity": null,
971 "name": "value",
972 "entityType": "columns",
973 "schema": "public",
974 "table": "secret_materials"
975 },
976 {
977 "type": "bigint",
978 "typeSchema": null,
979 "notNull": true,
980 "dimensions": 0,
981 "default": null,
982 "generated": null,
983 "identity": null,
984 "name": "created_at",
985 "entityType": "columns",
986 "schema": "public",
987 "table": "secret_materials"
988 },
989 {
990 "type": "bigint",
991 "typeSchema": null,
992 "notNull": true,
993 "dimensions": 0,
994 "default": null,
995 "generated": null,
996 "identity": null,
997 "name": "updated_at",
998 "entityType": "columns",
999 "schema": "public",
1000 "table": "secret_materials"
1001 },
1002 {
1003 "type": "text",
1004 "typeSchema": null,
1005 "notNull": true,
1006 "dimensions": 0,
1007 "default": null,
1008 "generated": null,
1009 "identity": null,
1010 "name": "id",
1011 "entityType": "columns",
1012 "schema": "public",
1013 "table": "source_auth_sessions"
1014 },
1015 {
1016 "type": "text",
1017 "typeSchema": null,
1018 "notNull": true,
1019 "dimensions": 0,
1020 "default": null,
1021 "generated": null,
1022 "identity": null,
1023 "name": "workspace_id",
1024 "entityType": "columns",
1025 "schema": "public",
1026 "table": "source_auth_sessions"
1027 },
1028 {
1029 "type": "text",
1030 "typeSchema": null,
1031 "notNull": true,
1032 "dimensions": 0,
1033 "default": null,
1034 "generated": null,
1035 "identity": null,
1036 "name": "source_id",
1037 "entityType": "columns",
1038 "schema": "public",
1039 "table": "source_auth_sessions"
1040 },
1041 {
1042 "type": "text",
1043 "typeSchema": null,
1044 "notNull": false,
1045 "dimensions": 0,
1046 "default": null,
1047 "generated": null,
1048 "identity": null,
1049 "name": "execution_id",
1050 "entityType": "columns",
1051 "schema": "public",
1052 "table": "source_auth_sessions"
1053 },
1054 {
1055 "type": "text",
1056 "typeSchema": null,
1057 "notNull": false,
1058 "dimensions": 0,
1059 "default": null,
1060 "generated": null,
1061 "identity": null,
1062 "name": "interaction_id",
1063 "entityType": "columns",
1064 "schema": "public",
1065 "table": "source_auth_sessions"
1066 },
1067 {
1068 "type": "text",
1069 "typeSchema": null,
1070 "notNull": true,
1071 "dimensions": 0,
1072 "default": null,
1073 "generated": null,
1074 "identity": null,
1075 "name": "strategy",
1076 "entityType": "columns",
1077 "schema": "public",
1078 "table": "source_auth_sessions"
1079 },
1080 {
1081 "type": "text",
1082 "typeSchema": null,
1083 "notNull": true,
1084 "dimensions": 0,
1085 "default": null,
1086 "generated": null,
1087 "identity": null,
1088 "name": "status",
1089 "entityType": "columns",
1090 "schema": "public",
1091 "table": "source_auth_sessions"
1092 },
1093 {
1094 "type": "text",
1095 "typeSchema": null,
1096 "notNull": true,
1097 "dimensions": 0,
1098 "default": null,
1099 "generated": null,
1100 "identity": null,
1101 "name": "endpoint",
1102 "entityType": "columns",
1103 "schema": "public",
1104 "table": "source_auth_sessions"
1105 },
1106 {
1107 "type": "text",
1108 "typeSchema": null,
1109 "notNull": true,
1110 "dimensions": 0,
1111 "default": null,
1112 "generated": null,
1113 "identity": null,
1114 "name": "state",
1115 "entityType": "columns",
1116 "schema": "public",
1117 "table": "source_auth_sessions"
1118 },
1119 {
1120 "type": "text",
1121 "typeSchema": null,
1122 "notNull": true,
1123 "dimensions": 0,
1124 "default": null,
1125 "generated": null,
1126 "identity": null,
1127 "name": "redirect_uri",
1128 "entityType": "columns",
1129 "schema": "public",
1130 "table": "source_auth_sessions"
1131 },
1132 {
1133 "type": "text",
1134 "typeSchema": null,
1135 "notNull": false,
1136 "dimensions": 0,
1137 "default": null,
1138 "generated": null,
1139 "identity": null,
1140 "name": "scope",
1141 "entityType": "columns",
1142 "schema": "public",
1143 "table": "source_auth_sessions"
1144 },
1145 {
1146 "type": "text",
1147 "typeSchema": null,
1148 "notNull": false,
1149 "dimensions": 0,
1150 "default": null,
1151 "generated": null,
1152 "identity": null,
1153 "name": "resource_metadata_url",
1154 "entityType": "columns",
1155 "schema": "public",
1156 "table": "source_auth_sessions"
1157 },
1158 {
1159 "type": "text",
1160 "typeSchema": null,
1161 "notNull": false,
1162 "dimensions": 0,
1163 "default": null,
1164 "generated": null,
1165 "identity": null,
1166 "name": "authorization_server_url",
1167 "entityType": "columns",
1168 "schema": "public",
1169 "table": "source_auth_sessions"
1170 },
1171 {
1172 "type": "text",
1173 "typeSchema": null,
1174 "notNull": false,
1175 "dimensions": 0,
1176 "default": null,
1177 "generated": null,
1178 "identity": null,
1179 "name": "resource_metadata_json",
1180 "entityType": "columns",
1181 "schema": "public",
1182 "table": "source_auth_sessions"
1183 },
1184 {
1185 "type": "text",
1186 "typeSchema": null,
1187 "notNull": false,
1188 "dimensions": 0,
1189 "default": null,
1190 "generated": null,
1191 "identity": null,
1192 "name": "authorization_server_metadata_json",
1193 "entityType": "columns",
1194 "schema": "public",
1195 "table": "source_auth_sessions"
1196 },
1197 {
1198 "type": "text",
1199 "typeSchema": null,
1200 "notNull": false,
1201 "dimensions": 0,
1202 "default": null,
1203 "generated": null,
1204 "identity": null,
1205 "name": "client_information_json",
1206 "entityType": "columns",
1207 "schema": "public",
1208 "table": "source_auth_sessions"
1209 },
1210 {
1211 "type": "text",
1212 "typeSchema": null,
1213 "notNull": false,
1214 "dimensions": 0,
1215 "default": null,
1216 "generated": null,
1217 "identity": null,
1218 "name": "code_verifier",
1219 "entityType": "columns",
1220 "schema": "public",
1221 "table": "source_auth_sessions"
1222 },
1223 {
1224 "type": "text",
1225 "typeSchema": null,
1226 "notNull": false,
1227 "dimensions": 0,
1228 "default": null,
1229 "generated": null,
1230 "identity": null,
1231 "name": "authorization_url",
1232 "entityType": "columns",
1233 "schema": "public",
1234 "table": "source_auth_sessions"
1235 },
1236 {
1237 "type": "text",
1238 "typeSchema": null,
1239 "notNull": false,
1240 "dimensions": 0,
1241 "default": null,
1242 "generated": null,
1243 "identity": null,
1244 "name": "error_text",
1245 "entityType": "columns",
1246 "schema": "public",
1247 "table": "source_auth_sessions"
1248 },
1249 {
1250 "type": "bigint",
1251 "typeSchema": null,
1252 "notNull": false,
1253 "dimensions": 0,
1254 "default": null,
1255 "generated": null,
1256 "identity": null,
1257 "name": "completed_at",
1258 "entityType": "columns",
1259 "schema": "public",
1260 "table": "source_auth_sessions"
1261 },
1262 {
1263 "type": "bigint",
1264 "typeSchema": null,
1265 "notNull": true,
1266 "dimensions": 0,
1267 "default": null,
1268 "generated": null,
1269 "identity": null,
1270 "name": "created_at",
1271 "entityType": "columns",
1272 "schema": "public",
1273 "table": "source_auth_sessions"
1274 },
1275 {
1276 "type": "bigint",
1277 "typeSchema": null,
1278 "notNull": true,
1279 "dimensions": 0,
1280 "default": null,
1281 "generated": null,
1282 "identity": null,
1283 "name": "updated_at",
1284 "entityType": "columns",
1285 "schema": "public",
1286 "table": "source_auth_sessions"
1287 },
1288 {
1289 "type": "text",
1290 "typeSchema": null,
1291 "notNull": true,
1292 "dimensions": 0,
1293 "default": null,
1294 "generated": null,
1295 "identity": null,
1296 "name": "workspace_id",
1297 "entityType": "columns",
1298 "schema": "public",
1299 "table": "source_credential_bindings"
1300 },
1301 {
1302 "type": "text",
1303 "typeSchema": null,
1304 "notNull": true,
1305 "dimensions": 0,
1306 "default": null,
1307 "generated": null,
1308 "identity": null,
1309 "name": "source_id",
1310 "entityType": "columns",
1311 "schema": "public",
1312 "table": "source_credential_bindings"
1313 },
1314 {
1315 "type": "text",
1316 "typeSchema": null,
1317 "notNull": false,
1318 "dimensions": 0,
1319 "default": null,
1320 "generated": null,
1321 "identity": null,
1322 "name": "token_provider_id",
1323 "entityType": "columns",
1324 "schema": "public",
1325 "table": "source_credential_bindings"
1326 },
1327 {
1328 "type": "text",
1329 "typeSchema": null,
1330 "notNull": false,
1331 "dimensions": 0,
1332 "default": null,
1333 "generated": null,
1334 "identity": null,
1335 "name": "token_handle",
1336 "entityType": "columns",
1337 "schema": "public",
1338 "table": "source_credential_bindings"
1339 },
1340 {
1341 "type": "text",
1342 "typeSchema": null,
1343 "notNull": false,
1344 "dimensions": 0,
1345 "default": null,
1346 "generated": null,
1347 "identity": null,
1348 "name": "refresh_token_provider_id",
1349 "entityType": "columns",
1350 "schema": "public",
1351 "table": "source_credential_bindings"
1352 },
1353 {
1354 "type": "text",
1355 "typeSchema": null,
1356 "notNull": false,
1357 "dimensions": 0,
1358 "default": null,
1359 "generated": null,
1360 "identity": null,
1361 "name": "refresh_token_handle",
1362 "entityType": "columns",
1363 "schema": "public",
1364 "table": "source_credential_bindings"
1365 },
1366 {
1367 "type": "bigint",
1368 "typeSchema": null,
1369 "notNull": true,
1370 "dimensions": 0,
1371 "default": null,
1372 "generated": null,
1373 "identity": null,
1374 "name": "created_at",
1375 "entityType": "columns",
1376 "schema": "public",
1377 "table": "source_credential_bindings"
1378 },
1379 {
1380 "type": "bigint",
1381 "typeSchema": null,
1382 "notNull": true,
1383 "dimensions": 0,
1384 "default": null,
1385 "generated": null,
1386 "identity": null,
1387 "name": "updated_at",
1388 "entityType": "columns",
1389 "schema": "public",
1390 "table": "source_credential_bindings"
1391 },
1392 {
1393 "type": "text",
1394 "typeSchema": null,
1395 "notNull": true,
1396 "dimensions": 0,
1397 "default": null,
1398 "generated": null,
1399 "identity": null,
1400 "name": "workspace_id",
1401 "entityType": "columns",
1402 "schema": "public",
1403 "table": "sources"
1404 },
1405 {
1406 "type": "text",
1407 "typeSchema": null,
1408 "notNull": true,
1409 "dimensions": 0,
1410 "default": null,
1411 "generated": null,
1412 "identity": null,
1413 "name": "source_id",
1414 "entityType": "columns",
1415 "schema": "public",
1416 "table": "sources"
1417 },
1418 {
1419 "type": "text",
1420 "typeSchema": null,
1421 "notNull": true,
1422 "dimensions": 0,
1423 "default": null,
1424 "generated": null,
1425 "identity": null,
1426 "name": "name",
1427 "entityType": "columns",
1428 "schema": "public",
1429 "table": "sources"
1430 },
1431 {
1432 "type": "text",
1433 "typeSchema": null,
1434 "notNull": true,
1435 "dimensions": 0,
1436 "default": null,
1437 "generated": null,
1438 "identity": null,
1439 "name": "kind",
1440 "entityType": "columns",
1441 "schema": "public",
1442 "table": "sources"
1443 },
1444 {
1445 "type": "text",
1446 "typeSchema": null,
1447 "notNull": true,
1448 "dimensions": 0,
1449 "default": null,
1450 "generated": null,
1451 "identity": null,
1452 "name": "endpoint",
1453 "entityType": "columns",
1454 "schema": "public",
1455 "table": "sources"
1456 },
1457 {
1458 "type": "text",
1459 "typeSchema": null,
1460 "notNull": true,
1461 "dimensions": 0,
1462 "default": null,
1463 "generated": null,
1464 "identity": null,
1465 "name": "status",
1466 "entityType": "columns",
1467 "schema": "public",
1468 "table": "sources"
1469 },
1470 {
1471 "type": "boolean",
1472 "typeSchema": null,
1473 "notNull": true,
1474 "dimensions": 0,
1475 "default": null,
1476 "generated": null,
1477 "identity": null,
1478 "name": "enabled",
1479 "entityType": "columns",
1480 "schema": "public",
1481 "table": "sources"
1482 },
1483 {
1484 "type": "text",
1485 "typeSchema": null,
1486 "notNull": false,
1487 "dimensions": 0,
1488 "default": null,
1489 "generated": null,
1490 "identity": null,
1491 "name": "namespace",
1492 "entityType": "columns",
1493 "schema": "public",
1494 "table": "sources"
1495 },
1496 {
1497 "type": "text",
1498 "typeSchema": null,
1499 "notNull": false,
1500 "dimensions": 0,
1501 "default": null,
1502 "generated": null,
1503 "identity": null,
1504 "name": "transport",
1505 "entityType": "columns",
1506 "schema": "public",
1507 "table": "sources"
1508 },
1509 {
1510 "type": "text",
1511 "typeSchema": null,
1512 "notNull": false,
1513 "dimensions": 0,
1514 "default": null,
1515 "generated": null,
1516 "identity": null,
1517 "name": "query_params_json",
1518 "entityType": "columns",
1519 "schema": "public",
1520 "table": "sources"
1521 },
1522 {
1523 "type": "text",
1524 "typeSchema": null,
1525 "notNull": false,
1526 "dimensions": 0,
1527 "default": null,
1528 "generated": null,
1529 "identity": null,
1530 "name": "headers_json",
1531 "entityType": "columns",
1532 "schema": "public",
1533 "table": "sources"
1534 },
1535 {
1536 "type": "text",
1537 "typeSchema": null,
1538 "notNull": false,
1539 "dimensions": 0,
1540 "default": null,
1541 "generated": null,
1542 "identity": null,
1543 "name": "spec_url",
1544 "entityType": "columns",
1545 "schema": "public",
1546 "table": "sources"
1547 },
1548 {
1549 "type": "text",
1550 "typeSchema": null,
1551 "notNull": false,
1552 "dimensions": 0,
1553 "default": null,
1554 "generated": null,
1555 "identity": null,
1556 "name": "default_headers_json",
1557 "entityType": "columns",
1558 "schema": "public",
1559 "table": "sources"
1560 },
1561 {
1562 "type": "text",
1563 "typeSchema": null,
1564 "notNull": true,
1565 "dimensions": 0,
1566 "default": null,
1567 "generated": null,
1568 "identity": null,
1569 "name": "auth_kind",
1570 "entityType": "columns",
1571 "schema": "public",
1572 "table": "sources"
1573 },
1574 {
1575 "type": "text",
1576 "typeSchema": null,
1577 "notNull": false,
1578 "dimensions": 0,
1579 "default": null,
1580 "generated": null,
1581 "identity": null,
1582 "name": "auth_header_name",
1583 "entityType": "columns",
1584 "schema": "public",
1585 "table": "sources"
1586 },
1587 {
1588 "type": "text",
1589 "typeSchema": null,
1590 "notNull": false,
1591 "dimensions": 0,
1592 "default": null,
1593 "generated": null,
1594 "identity": null,
1595 "name": "auth_prefix",
1596 "entityType": "columns",
1597 "schema": "public",
1598 "table": "sources"
1599 },
1600 {
1601 "type": "text",
1602 "typeSchema": null,
1603 "notNull": false,
1604 "dimensions": 0,
1605 "default": null,
1606 "generated": null,
1607 "identity": null,
1608 "name": "source_hash",
1609 "entityType": "columns",
1610 "schema": "public",
1611 "table": "sources"
1612 },
1613 {
1614 "type": "text",
1615 "typeSchema": null,
1616 "notNull": false,
1617 "dimensions": 0,
1618 "default": null,
1619 "generated": null,
1620 "identity": null,
1621 "name": "last_error",
1622 "entityType": "columns",
1623 "schema": "public",
1624 "table": "sources"
1625 },
1626 {
1627 "type": "bigint",
1628 "typeSchema": null,
1629 "notNull": true,
1630 "dimensions": 0,
1631 "default": null,
1632 "generated": null,
1633 "identity": null,
1634 "name": "created_at",
1635 "entityType": "columns",
1636 "schema": "public",
1637 "table": "sources"
1638 },
1639 {
1640 "type": "bigint",
1641 "typeSchema": null,
1642 "notNull": true,
1643 "dimensions": 0,
1644 "default": null,
1645 "generated": null,
1646 "identity": null,
1647 "name": "updated_at",
1648 "entityType": "columns",
1649 "schema": "public",
1650 "table": "sources"
1651 },
1652 {
1653 "type": "text",
1654 "typeSchema": null,
1655 "notNull": true,
1656 "dimensions": 0,
1657 "default": null,
1658 "generated": null,
1659 "identity": null,
1660 "name": "workspace_id",
1661 "entityType": "columns",
1662 "schema": "public",
1663 "table": "tool_artifact_parameters"
1664 },
1665 {
1666 "type": "text",
1667 "typeSchema": null,
1668 "notNull": true,
1669 "dimensions": 0,
1670 "default": null,
1671 "generated": null,
1672 "identity": null,
1673 "name": "path",
1674 "entityType": "columns",
1675 "schema": "public",
1676 "table": "tool_artifact_parameters"
1677 },
1678 {
1679 "type": "bigint",
1680 "typeSchema": null,
1681 "notNull": true,
1682 "dimensions": 0,
1683 "default": null,
1684 "generated": null,
1685 "identity": null,
1686 "name": "position",
1687 "entityType": "columns",
1688 "schema": "public",
1689 "table": "tool_artifact_parameters"
1690 },
1691 {
1692 "type": "text",
1693 "typeSchema": null,
1694 "notNull": true,
1695 "dimensions": 0,
1696 "default": null,
1697 "generated": null,
1698 "identity": null,
1699 "name": "name",
1700 "entityType": "columns",
1701 "schema": "public",
1702 "table": "tool_artifact_parameters"
1703 },
1704 {
1705 "type": "text",
1706 "typeSchema": null,
1707 "notNull": true,
1708 "dimensions": 0,
1709 "default": null,
1710 "generated": null,
1711 "identity": null,
1712 "name": "location",
1713 "entityType": "columns",
1714 "schema": "public",
1715 "table": "tool_artifact_parameters"
1716 },
1717 {
1718 "type": "boolean",
1719 "typeSchema": null,
1720 "notNull": true,
1721 "dimensions": 0,
1722 "default": null,
1723 "generated": null,
1724 "identity": null,
1725 "name": "required",
1726 "entityType": "columns",
1727 "schema": "public",
1728 "table": "tool_artifact_parameters"
1729 },
1730 {
1731 "type": "text",
1732 "typeSchema": null,
1733 "notNull": true,
1734 "dimensions": 0,
1735 "default": null,
1736 "generated": null,
1737 "identity": null,
1738 "name": "workspace_id",
1739 "entityType": "columns",
1740 "schema": "public",
1741 "table": "tool_artifact_ref_hint_keys"
1742 },
1743 {
1744 "type": "text",
1745 "typeSchema": null,
1746 "notNull": true,
1747 "dimensions": 0,
1748 "default": null,
1749 "generated": null,
1750 "identity": null,
1751 "name": "path",
1752 "entityType": "columns",
1753 "schema": "public",
1754 "table": "tool_artifact_ref_hint_keys"
1755 },
1756 {
1757 "type": "bigint",
1758 "typeSchema": null,
1759 "notNull": true,
1760 "dimensions": 0,
1761 "default": null,
1762 "generated": null,
1763 "identity": null,
1764 "name": "position",
1765 "entityType": "columns",
1766 "schema": "public",
1767 "table": "tool_artifact_ref_hint_keys"
1768 },
1769 {
1770 "type": "text",
1771 "typeSchema": null,
1772 "notNull": true,
1773 "dimensions": 0,
1774 "default": null,
1775 "generated": null,
1776 "identity": null,
1777 "name": "ref_hint_key",
1778 "entityType": "columns",
1779 "schema": "public",
1780 "table": "tool_artifact_ref_hint_keys"
1781 },
1782 {
1783 "type": "text",
1784 "typeSchema": null,
1785 "notNull": true,
1786 "dimensions": 0,
1787 "default": null,
1788 "generated": null,
1789 "identity": null,
1790 "name": "workspace_id",
1791 "entityType": "columns",
1792 "schema": "public",
1793 "table": "tool_artifact_request_body_content_types"
1794 },
1795 {
1796 "type": "text",
1797 "typeSchema": null,
1798 "notNull": true,
1799 "dimensions": 0,
1800 "default": null,
1801 "generated": null,
1802 "identity": null,
1803 "name": "path",
1804 "entityType": "columns",
1805 "schema": "public",
1806 "table": "tool_artifact_request_body_content_types"
1807 },
1808 {
1809 "type": "bigint",
1810 "typeSchema": null,
1811 "notNull": true,
1812 "dimensions": 0,
1813 "default": null,
1814 "generated": null,
1815 "identity": null,
1816 "name": "position",
1817 "entityType": "columns",
1818 "schema": "public",
1819 "table": "tool_artifact_request_body_content_types"
1820 },
1821 {
1822 "type": "text",
1823 "typeSchema": null,
1824 "notNull": true,
1825 "dimensions": 0,
1826 "default": null,
1827 "generated": null,
1828 "identity": null,
1829 "name": "content_type",
1830 "entityType": "columns",
1831 "schema": "public",
1832 "table": "tool_artifact_request_body_content_types"
1833 },
1834 {
1835 "type": "text",
1836 "typeSchema": null,
1837 "notNull": true,
1838 "dimensions": 0,
1839 "default": null,
1840 "generated": null,
1841 "identity": null,
1842 "name": "workspace_id",
1843 "entityType": "columns",
1844 "schema": "public",
1845 "table": "tool_artifacts"
1846 },
1847 {
1848 "type": "text",
1849 "typeSchema": null,
1850 "notNull": true,
1851 "dimensions": 0,
1852 "default": null,
1853 "generated": null,
1854 "identity": null,
1855 "name": "path",
1856 "entityType": "columns",
1857 "schema": "public",
1858 "table": "tool_artifacts"
1859 },
1860 {
1861 "type": "text",
1862 "typeSchema": null,
1863 "notNull": true,
1864 "dimensions": 0,
1865 "default": null,
1866 "generated": null,
1867 "identity": null,
1868 "name": "tool_id",
1869 "entityType": "columns",
1870 "schema": "public",
1871 "table": "tool_artifacts"
1872 },
1873 {
1874 "type": "text",
1875 "typeSchema": null,
1876 "notNull": true,
1877 "dimensions": 0,
1878 "default": null,
1879 "generated": null,
1880 "identity": null,
1881 "name": "source_id",
1882 "entityType": "columns",
1883 "schema": "public",
1884 "table": "tool_artifacts"
1885 },
1886 {
1887 "type": "text",
1888 "typeSchema": null,
1889 "notNull": false,
1890 "dimensions": 0,
1891 "default": null,
1892 "generated": null,
1893 "identity": null,
1894 "name": "title",
1895 "entityType": "columns",
1896 "schema": "public",
1897 "table": "tool_artifacts"
1898 },
1899 {
1900 "type": "text",
1901 "typeSchema": null,
1902 "notNull": false,
1903 "dimensions": 0,
1904 "default": null,
1905 "generated": null,
1906 "identity": null,
1907 "name": "description",
1908 "entityType": "columns",
1909 "schema": "public",
1910 "table": "tool_artifacts"
1911 },
1912 {
1913 "type": "text",
1914 "typeSchema": null,
1915 "notNull": true,
1916 "dimensions": 0,
1917 "default": null,
1918 "generated": null,
1919 "identity": null,
1920 "name": "search_namespace",
1921 "entityType": "columns",
1922 "schema": "public",
1923 "table": "tool_artifacts"
1924 },
1925 {
1926 "type": "text",
1927 "typeSchema": null,
1928 "notNull": true,
1929 "dimensions": 0,
1930 "default": null,
1931 "generated": null,
1932 "identity": null,
1933 "name": "search_text",
1934 "entityType": "columns",
1935 "schema": "public",
1936 "table": "tool_artifacts"
1937 },
1938 {
1939 "type": "text",
1940 "typeSchema": null,
1941 "notNull": false,
1942 "dimensions": 0,
1943 "default": null,
1944 "generated": null,
1945 "identity": null,
1946 "name": "input_schema_json",
1947 "entityType": "columns",
1948 "schema": "public",
1949 "table": "tool_artifacts"
1950 },
1951 {
1952 "type": "text",
1953 "typeSchema": null,
1954 "notNull": false,
1955 "dimensions": 0,
1956 "default": null,
1957 "generated": null,
1958 "identity": null,
1959 "name": "output_schema_json",
1960 "entityType": "columns",
1961 "schema": "public",
1962 "table": "tool_artifacts"
1963 },
1964 {
1965 "type": "text",
1966 "typeSchema": null,
1967 "notNull": true,
1968 "dimensions": 0,
1969 "default": null,
1970 "generated": null,
1971 "identity": null,
1972 "name": "provider_kind",
1973 "entityType": "columns",
1974 "schema": "public",
1975 "table": "tool_artifacts"
1976 },
1977 {
1978 "type": "text",
1979 "typeSchema": null,
1980 "notNull": false,
1981 "dimensions": 0,
1982 "default": null,
1983 "generated": null,
1984 "identity": null,
1985 "name": "mcp_tool_name",
1986 "entityType": "columns",
1987 "schema": "public",
1988 "table": "tool_artifacts"
1989 },
1990 {
1991 "type": "text",
1992 "typeSchema": null,
1993 "notNull": false,
1994 "dimensions": 0,
1995 "default": null,
1996 "generated": null,
1997 "identity": null,
1998 "name": "openapi_method",
1999 "entityType": "columns",
2000 "schema": "public",
2001 "table": "tool_artifacts"
2002 },
2003 {
2004 "type": "text",
2005 "typeSchema": null,
2006 "notNull": false,
2007 "dimensions": 0,
2008 "default": null,
2009 "generated": null,
2010 "identity": null,
2011 "name": "openapi_path_template",
2012 "entityType": "columns",
2013 "schema": "public",
2014 "table": "tool_artifacts"
2015 },
2016 {
2017 "type": "text",
2018 "typeSchema": null,
2019 "notNull": false,
2020 "dimensions": 0,
2021 "default": null,
2022 "generated": null,
2023 "identity": null,
2024 "name": "openapi_operation_hash",
2025 "entityType": "columns",
2026 "schema": "public",
2027 "table": "tool_artifacts"
2028 },
2029 {
2030 "type": "boolean",
2031 "typeSchema": null,
2032 "notNull": false,
2033 "dimensions": 0,
2034 "default": null,
2035 "generated": null,
2036 "identity": null,
2037 "name": "openapi_request_body_required",
2038 "entityType": "columns",
2039 "schema": "public",
2040 "table": "tool_artifacts"
2041 },
2042 {
2043 "type": "bigint",
2044 "typeSchema": null,
2045 "notNull": true,
2046 "dimensions": 0,
2047 "default": null,
2048 "generated": null,
2049 "identity": null,
2050 "name": "created_at",
2051 "entityType": "columns",
2052 "schema": "public",
2053 "table": "tool_artifacts"
2054 },
2055 {
2056 "type": "bigint",
2057 "typeSchema": null,
2058 "notNull": true,
2059 "dimensions": 0,
2060 "default": null,
2061 "generated": null,
2062 "identity": null,
2063 "name": "updated_at",
2064 "entityType": "columns",
2065 "schema": "public",
2066 "table": "tool_artifacts"
2067 },
2068 {
2069 "type": "text",
2070 "typeSchema": null,
2071 "notNull": true,
2072 "dimensions": 0,
2073 "default": null,
2074 "generated": null,
2075 "identity": null,
2076 "name": "id",
2077 "entityType": "columns",
2078 "schema": "public",
2079 "table": "workspaces"
2080 },
2081 {
2082 "type": "text",
2083 "typeSchema": null,
2084 "notNull": true,
2085 "dimensions": 0,
2086 "default": null,
2087 "generated": null,
2088 "identity": null,
2089 "name": "organization_id",
2090 "entityType": "columns",
2091 "schema": "public",
2092 "table": "workspaces"
2093 },
2094 {
2095 "type": "text",
2096 "typeSchema": null,
2097 "notNull": true,
2098 "dimensions": 0,
2099 "default": null,
2100 "generated": null,
2101 "identity": null,
2102 "name": "name",
2103 "entityType": "columns",
2104 "schema": "public",
2105 "table": "workspaces"
2106 },
2107 {
2108 "type": "text",
2109 "typeSchema": null,
2110 "notNull": false,
2111 "dimensions": 0,
2112 "default": null,
2113 "generated": null,
2114 "identity": null,
2115 "name": "created_by_account_id",
2116 "entityType": "columns",
2117 "schema": "public",
2118 "table": "workspaces"
2119 },
2120 {
2121 "type": "bigint",
2122 "typeSchema": null,
2123 "notNull": true,
2124 "dimensions": 0,
2125 "default": null,
2126 "generated": null,
2127 "identity": null,
2128 "name": "created_at",
2129 "entityType": "columns",
2130 "schema": "public",
2131 "table": "workspaces"
2132 },
2133 {
2134 "type": "bigint",
2135 "typeSchema": null,
2136 "notNull": true,
2137 "dimensions": 0,
2138 "default": null,
2139 "generated": null,
2140 "identity": null,
2141 "name": "updated_at",
2142 "entityType": "columns",
2143 "schema": "public",
2144 "table": "workspaces"
2145 },
2146 {
2147 "nameExplicit": true,
2148 "columns": [
2149 {
2150 "value": "provider",
2151 "isExpression": false,
2152 "asc": true,
2153 "nullsFirst": false,
2154 "opclass": null
2155 },
2156 {
2157 "value": "subject",
2158 "isExpression": false,
2159 "asc": true,
2160 "nullsFirst": false,
2161 "opclass": null
2162 }
2163 ],
2164 "isUnique": true,
2165 "where": null,
2166 "with": "",
2167 "method": "btree",
2168 "concurrently": false,
2169 "name": "accounts_provider_subject_idx",
2170 "entityType": "indexes",
2171 "schema": "public",
2172 "table": "accounts"
2173 },
2174 {
2175 "nameExplicit": true,
2176 "columns": [
2177 {
2178 "value": "updated_at",
2179 "isExpression": false,
2180 "asc": true,
2181 "nullsFirst": false,
2182 "opclass": null
2183 },
2184 {
2185 "value": "id",
2186 "isExpression": false,
2187 "asc": true,
2188 "nullsFirst": false,
2189 "opclass": null
2190 }
2191 ],
2192 "isUnique": false,
2193 "where": null,
2194 "with": "",
2195 "method": "btree",
2196 "concurrently": false,
2197 "name": "accounts_updated_idx",
2198 "entityType": "indexes",
2199 "schema": "public",
2200 "table": "accounts"
2201 },
2202 {
2203 "nameExplicit": true,
2204 "columns": [
2205 {
2206 "value": "execution_id",
2207 "isExpression": false,
2208 "asc": true,
2209 "nullsFirst": false,
2210 "opclass": null
2211 },
2212 {
2213 "value": "updated_at",
2214 "isExpression": false,
2215 "asc": true,
2216 "nullsFirst": false,
2217 "opclass": null
2218 },
2219 {
2220 "value": "id",
2221 "isExpression": false,
2222 "asc": true,
2223 "nullsFirst": false,
2224 "opclass": null
2225 }
2226 ],
2227 "isUnique": false,
2228 "where": null,
2229 "with": "",
2230 "method": "btree",
2231 "concurrently": false,
2232 "name": "execution_interactions_execution_idx",
2233 "entityType": "indexes",
2234 "schema": "public",
2235 "table": "execution_interactions"
2236 },
2237 {
2238 "nameExplicit": true,
2239 "columns": [
2240 {
2241 "value": "workspace_id",
2242 "isExpression": false,
2243 "asc": true,
2244 "nullsFirst": false,
2245 "opclass": null
2246 },
2247 {
2248 "value": "updated_at",
2249 "isExpression": false,
2250 "asc": true,
2251 "nullsFirst": false,
2252 "opclass": null
2253 },
2254 {
2255 "value": "id",
2256 "isExpression": false,
2257 "asc": true,
2258 "nullsFirst": false,
2259 "opclass": null
2260 }
2261 ],
2262 "isUnique": false,
2263 "where": null,
2264 "with": "",
2265 "method": "btree",
2266 "concurrently": false,
2267 "name": "executions_workspace_idx",
2268 "entityType": "indexes",
2269 "schema": "public",
2270 "table": "executions"
2271 },
2272 {
2273 "nameExplicit": true,
2274 "columns": [
2275 {
2276 "value": "organization_id",
2277 "isExpression": false,
2278 "asc": true,
2279 "nullsFirst": false,
2280 "opclass": null
2281 }
2282 ],
2283 "isUnique": false,
2284 "where": null,
2285 "with": "",
2286 "method": "btree",
2287 "concurrently": false,
2288 "name": "organization_memberships_org_idx",
2289 "entityType": "indexes",
2290 "schema": "public",
2291 "table": "organization_memberships"
2292 },
2293 {
2294 "nameExplicit": true,
2295 "columns": [
2296 {
2297 "value": "account_id",
2298 "isExpression": false,
2299 "asc": true,
2300 "nullsFirst": false,
2301 "opclass": null
2302 }
2303 ],
2304 "isUnique": false,
2305 "where": null,
2306 "with": "",
2307 "method": "btree",
2308 "concurrently": false,
2309 "name": "organization_memberships_account_idx",
2310 "entityType": "indexes",
2311 "schema": "public",
2312 "table": "organization_memberships"
2313 },
2314 {
2315 "nameExplicit": true,
2316 "columns": [
2317 {
2318 "value": "organization_id",
2319 "isExpression": false,
2320 "asc": true,
2321 "nullsFirst": false,
2322 "opclass": null
2323 },
2324 {
2325 "value": "account_id",
2326 "isExpression": false,
2327 "asc": true,
2328 "nullsFirst": false,
2329 "opclass": null
2330 }
2331 ],
2332 "isUnique": true,
2333 "where": null,
2334 "with": "",
2335 "method": "btree",
2336 "concurrently": false,
2337 "name": "organization_memberships_org_account_idx",
2338 "entityType": "indexes",
2339 "schema": "public",
2340 "table": "organization_memberships"
2341 },
2342 {
2343 "nameExplicit": true,
2344 "columns": [
2345 {
2346 "value": "slug",
2347 "isExpression": false,
2348 "asc": true,
2349 "nullsFirst": false,
2350 "opclass": null
2351 }
2352 ],
2353 "isUnique": true,
2354 "where": null,
2355 "with": "",
2356 "method": "btree",
2357 "concurrently": false,
2358 "name": "organizations_slug_idx",
2359 "entityType": "indexes",
2360 "schema": "public",
2361 "table": "organizations"
2362 },
2363 {
2364 "nameExplicit": true,
2365 "columns": [
2366 {
2367 "value": "updated_at",
2368 "isExpression": false,
2369 "asc": true,
2370 "nullsFirst": false,
2371 "opclass": null
2372 },
2373 {
2374 "value": "id",
2375 "isExpression": false,
2376 "asc": true,
2377 "nullsFirst": false,
2378 "opclass": null
2379 }
2380 ],
2381 "isUnique": false,
2382 "where": null,
2383 "with": "",
2384 "method": "btree",
2385 "concurrently": false,
2386 "name": "organizations_updated_idx",
2387 "entityType": "indexes",
2388 "schema": "public",
2389 "table": "organizations"
2390 },
2391 {
2392 "nameExplicit": true,
2393 "columns": [
2394 {
2395 "value": "workspace_id",
2396 "isExpression": false,
2397 "asc": true,
2398 "nullsFirst": false,
2399 "opclass": null
2400 },
2401 {
2402 "value": "updated_at",
2403 "isExpression": false,
2404 "asc": true,
2405 "nullsFirst": false,
2406 "opclass": null
2407 },
2408 {
2409 "value": "id",
2410 "isExpression": false,
2411 "asc": true,
2412 "nullsFirst": false,
2413 "opclass": null
2414 }
2415 ],
2416 "isUnique": false,
2417 "where": null,
2418 "with": "",
2419 "method": "btree",
2420 "concurrently": false,
2421 "name": "policies_workspace_idx",
2422 "entityType": "indexes",
2423 "schema": "public",
2424 "table": "policies"
2425 },
2426 {
2427 "nameExplicit": true,
2428 "columns": [
2429 {
2430 "value": "updated_at",
2431 "isExpression": false,
2432 "asc": true,
2433 "nullsFirst": false,
2434 "opclass": null
2435 },
2436 {
2437 "value": "id",
2438 "isExpression": false,
2439 "asc": true,
2440 "nullsFirst": false,
2441 "opclass": null
2442 }
2443 ],
2444 "isUnique": false,
2445 "where": null,
2446 "with": "",
2447 "method": "btree",
2448 "concurrently": false,
2449 "name": "secret_materials_updated_idx",
2450 "entityType": "indexes",
2451 "schema": "public",
2452 "table": "secret_materials"
2453 },
2454 {
2455 "nameExplicit": true,
2456 "columns": [
2457 {
2458 "value": "workspace_id",
2459 "isExpression": false,
2460 "asc": true,
2461 "nullsFirst": false,
2462 "opclass": null
2463 },
2464 {
2465 "value": "updated_at",
2466 "isExpression": false,
2467 "asc": true,
2468 "nullsFirst": false,
2469 "opclass": null
2470 },
2471 {
2472 "value": "id",
2473 "isExpression": false,
2474 "asc": true,
2475 "nullsFirst": false,
2476 "opclass": null
2477 }
2478 ],
2479 "isUnique": false,
2480 "where": null,
2481 "with": "",
2482 "method": "btree",
2483 "concurrently": false,
2484 "name": "source_auth_sessions_workspace_idx",
2485 "entityType": "indexes",
2486 "schema": "public",
2487 "table": "source_auth_sessions"
2488 },
2489 {
2490 "nameExplicit": true,
2491 "columns": [
2492 {
2493 "value": "state",
2494 "isExpression": false,
2495 "asc": true,
2496 "nullsFirst": false,
2497 "opclass": null
2498 }
2499 ],
2500 "isUnique": true,
2501 "where": null,
2502 "with": "",
2503 "method": "btree",
2504 "concurrently": false,
2505 "name": "source_auth_sessions_state_idx",
2506 "entityType": "indexes",
2507 "schema": "public",
2508 "table": "source_auth_sessions"
2509 },
2510 {
2511 "nameExplicit": true,
2512 "columns": [
2513 {
2514 "value": "workspace_id",
2515 "isExpression": false,
2516 "asc": true,
2517 "nullsFirst": false,
2518 "opclass": null
2519 },
2520 {
2521 "value": "updated_at",
2522 "isExpression": false,
2523 "asc": true,
2524 "nullsFirst": false,
2525 "opclass": null
2526 },
2527 {
2528 "value": "source_id",
2529 "isExpression": false,
2530 "asc": true,
2531 "nullsFirst": false,
2532 "opclass": null
2533 }
2534 ],
2535 "isUnique": false,
2536 "where": null,
2537 "with": "",
2538 "method": "btree",
2539 "concurrently": false,
2540 "name": "source_credential_bindings_workspace_idx",
2541 "entityType": "indexes",
2542 "schema": "public",
2543 "table": "source_credential_bindings"
2544 },
2545 {
2546 "nameExplicit": true,
2547 "columns": [
2548 {
2549 "value": "workspace_id",
2550 "isExpression": false,
2551 "asc": true,
2552 "nullsFirst": false,
2553 "opclass": null
2554 },
2555 {
2556 "value": "name",
2557 "isExpression": false,
2558 "asc": true,
2559 "nullsFirst": false,
2560 "opclass": null
2561 }
2562 ],
2563 "isUnique": true,
2564 "where": null,
2565 "with": "",
2566 "method": "btree",
2567 "concurrently": false,
2568 "name": "sources_workspace_name_idx",
2569 "entityType": "indexes",
2570 "schema": "public",
2571 "table": "sources"
2572 },
2573 {
2574 "nameExplicit": true,
2575 "columns": [
2576 {
2577 "value": "workspace_id",
2578 "isExpression": false,
2579 "asc": true,
2580 "nullsFirst": false,
2581 "opclass": null
2582 },
2583 {
2584 "value": "path",
2585 "isExpression": false,
2586 "asc": true,
2587 "nullsFirst": false,
2588 "opclass": null
2589 },
2590 {
2591 "value": "position",
2592 "isExpression": false,
2593 "asc": true,
2594 "nullsFirst": false,
2595 "opclass": null
2596 }
2597 ],
2598 "isUnique": false,
2599 "where": null,
2600 "with": "",
2601 "method": "btree",
2602 "concurrently": false,
2603 "name": "tool_artifact_parameters_lookup_idx",
2604 "entityType": "indexes",
2605 "schema": "public",
2606 "table": "tool_artifact_parameters"
2607 },
2608 {
2609 "nameExplicit": true,
2610 "columns": [
2611 {
2612 "value": "workspace_id",
2613 "isExpression": false,
2614 "asc": true,
2615 "nullsFirst": false,
2616 "opclass": null
2617 },
2618 {
2619 "value": "path",
2620 "isExpression": false,
2621 "asc": true,
2622 "nullsFirst": false,
2623 "opclass": null
2624 },
2625 {
2626 "value": "position",
2627 "isExpression": false,
2628 "asc": true,
2629 "nullsFirst": false,
2630 "opclass": null
2631 }
2632 ],
2633 "isUnique": false,
2634 "where": null,
2635 "with": "",
2636 "method": "btree",
2637 "concurrently": false,
2638 "name": "tool_artifact_ref_hint_keys_lookup_idx",
2639 "entityType": "indexes",
2640 "schema": "public",
2641 "table": "tool_artifact_ref_hint_keys"
2642 },
2643 {
2644 "nameExplicit": true,
2645 "columns": [
2646 {
2647 "value": "workspace_id",
2648 "isExpression": false,
2649 "asc": true,
2650 "nullsFirst": false,
2651 "opclass": null
2652 },
2653 {
2654 "value": "path",
2655 "isExpression": false,
2656 "asc": true,
2657 "nullsFirst": false,
2658 "opclass": null
2659 },
2660 {
2661 "value": "position",
2662 "isExpression": false,
2663 "asc": true,
2664 "nullsFirst": false,
2665 "opclass": null
2666 }
2667 ],
2668 "isUnique": false,
2669 "where": null,
2670 "with": "",
2671 "method": "btree",
2672 "concurrently": false,
2673 "name": "tool_artifact_request_body_content_types_lookup_idx",
2674 "entityType": "indexes",
2675 "schema": "public",
2676 "table": "tool_artifact_request_body_content_types"
2677 },
2678 {
2679 "nameExplicit": true,
2680 "columns": [
2681 {
2682 "value": "workspace_id",
2683 "isExpression": false,
2684 "asc": true,
2685 "nullsFirst": false,
2686 "opclass": null
2687 },
2688 {
2689 "value": "source_id",
2690 "isExpression": false,
2691 "asc": true,
2692 "nullsFirst": false,
2693 "opclass": null
2694 },
2695 {
2696 "value": "updated_at",
2697 "isExpression": false,
2698 "asc": true,
2699 "nullsFirst": false,
2700 "opclass": null
2701 },
2702 {
2703 "value": "path",
2704 "isExpression": false,
2705 "asc": true,
2706 "nullsFirst": false,
2707 "opclass": null
2708 }
2709 ],
2710 "isUnique": false,
2711 "where": null,
2712 "with": "",
2713 "method": "btree",
2714 "concurrently": false,
2715 "name": "tool_artifacts_workspace_source_idx",
2716 "entityType": "indexes",
2717 "schema": "public",
2718 "table": "tool_artifacts"
2719 },
2720 {
2721 "nameExplicit": true,
2722 "columns": [
2723 {
2724 "value": "workspace_id",
2725 "isExpression": false,
2726 "asc": true,
2727 "nullsFirst": false,
2728 "opclass": null
2729 },
2730 {
2731 "value": "search_namespace",
2732 "isExpression": false,
2733 "asc": true,
2734 "nullsFirst": false,
2735 "opclass": null
2736 },
2737 {
2738 "value": "path",
2739 "isExpression": false,
2740 "asc": true,
2741 "nullsFirst": false,
2742 "opclass": null
2743 }
2744 ],
2745 "isUnique": false,
2746 "where": null,
2747 "with": "",
2748 "method": "btree",
2749 "concurrently": false,
2750 "name": "tool_artifacts_workspace_namespace_idx",
2751 "entityType": "indexes",
2752 "schema": "public",
2753 "table": "tool_artifacts"
2754 },
2755 {
2756 "nameExplicit": true,
2757 "columns": [
2758 {
2759 "value": "organization_id",
2760 "isExpression": false,
2761 "asc": true,
2762 "nullsFirst": false,
2763 "opclass": null
2764 }
2765 ],
2766 "isUnique": false,
2767 "where": null,
2768 "with": "",
2769 "method": "btree",
2770 "concurrently": false,
2771 "name": "workspaces_org_idx",
2772 "entityType": "indexes",
2773 "schema": "public",
2774 "table": "workspaces"
2775 },
2776 {
2777 "nameExplicit": true,
2778 "columns": [
2779 {
2780 "value": "organization_id",
2781 "isExpression": false,
2782 "asc": true,
2783 "nullsFirst": false,
2784 "opclass": null
2785 },
2786 {
2787 "value": "name",
2788 "isExpression": false,
2789 "asc": true,
2790 "nullsFirst": false,
2791 "opclass": null
2792 }
2793 ],
2794 "isUnique": true,
2795 "where": null,
2796 "with": "",
2797 "method": "btree",
2798 "concurrently": false,
2799 "name": "workspaces_org_name_idx",
2800 "entityType": "indexes",
2801 "schema": "public",
2802 "table": "workspaces"
2803 },
2804 {
2805 "columns": [
2806 "workspace_id",
2807 "source_id"
2808 ],
2809 "nameExplicit": false,
2810 "name": "source_credential_bindings_pkey",
2811 "entityType": "pks",
2812 "schema": "public",
2813 "table": "source_credential_bindings"
2814 },
2815 {
2816 "columns": [
2817 "workspace_id",
2818 "source_id"
2819 ],
2820 "nameExplicit": false,
2821 "name": "sources_pkey",
2822 "entityType": "pks",
2823 "schema": "public",
2824 "table": "sources"
2825 },
2826 {
2827 "columns": [
2828 "workspace_id",
2829 "path",
2830 "position"
2831 ],
2832 "nameExplicit": false,
2833 "name": "tool_artifact_parameters_pkey",
2834 "entityType": "pks",
2835 "schema": "public",
2836 "table": "tool_artifact_parameters"
2837 },
2838 {
2839 "columns": [
2840 "workspace_id",
2841 "path",
2842 "position"
2843 ],
2844 "nameExplicit": false,
2845 "name": "tool_artifact_ref_hint_keys_pkey",
2846 "entityType": "pks",
2847 "schema": "public",
2848 "table": "tool_artifact_ref_hint_keys"
2849 },
2850 {
2851 "columns": [
2852 "workspace_id",
2853 "path",
2854 "position"
2855 ],
2856 "nameExplicit": false,
2857 "name": "tool_artifact_request_body_content_types_pkey",
2858 "entityType": "pks",
2859 "schema": "public",
2860 "table": "tool_artifact_request_body_content_types"
2861 },
2862 {
2863 "columns": [
2864 "workspace_id",
2865 "path"
2866 ],
2867 "nameExplicit": false,
2868 "name": "tool_artifacts_pkey",
2869 "entityType": "pks",
2870 "schema": "public",
2871 "table": "tool_artifacts"
2872 },
2873 {
2874 "columns": [
2875 "id"
2876 ],
2877 "nameExplicit": false,
2878 "name": "accounts_pkey",
2879 "schema": "public",
2880 "table": "accounts",
2881 "entityType": "pks"
2882 },
2883 {
2884 "columns": [
2885 "id"
2886 ],
2887 "nameExplicit": false,
2888 "name": "execution_interactions_pkey",
2889 "schema": "public",
2890 "table": "execution_interactions",
2891 "entityType": "pks"
2892 },
2893 {
2894 "columns": [
2895 "id"
2896 ],
2897 "nameExplicit": false,
2898 "name": "executions_pkey",
2899 "schema": "public",
2900 "table": "executions",
2901 "entityType": "pks"
2902 },
2903 {
2904 "columns": [
2905 "id"
2906 ],
2907 "nameExplicit": false,
2908 "name": "local_installations_pkey",
2909 "schema": "public",
2910 "table": "local_installations",
2911 "entityType": "pks"
2912 },
2913 {
2914 "columns": [
2915 "id"
2916 ],
2917 "nameExplicit": false,
2918 "name": "organization_memberships_pkey",
2919 "schema": "public",
2920 "table": "organization_memberships",
2921 "entityType": "pks"
2922 },
2923 {
2924 "columns": [
2925 "id"
2926 ],
2927 "nameExplicit": false,
2928 "name": "organizations_pkey",
2929 "schema": "public",
2930 "table": "organizations",
2931 "entityType": "pks"
2932 },
2933 {
2934 "columns": [
2935 "id"
2936 ],
2937 "nameExplicit": false,
2938 "name": "policies_pkey",
2939 "schema": "public",
2940 "table": "policies",
2941 "entityType": "pks"
2942 },
2943 {
2944 "columns": [
2945 "id"
2946 ],
2947 "nameExplicit": false,
2948 "name": "secret_materials_pkey",
2949 "schema": "public",
2950 "table": "secret_materials",
2951 "entityType": "pks"
2952 },
2953 {
2954 "columns": [
2955 "id"
2956 ],
2957 "nameExplicit": false,
2958 "name": "source_auth_sessions_pkey",
2959 "schema": "public",
2960 "table": "source_auth_sessions",
2961 "entityType": "pks"
2962 },
2963 {
2964 "columns": [
2965 "id"
2966 ],
2967 "nameExplicit": false,
2968 "name": "workspaces_pkey",
2969 "schema": "public",
2970 "table": "workspaces",
2971 "entityType": "pks"
2972 },
2973 {
2974 "value": "\"provider\" in ('local', 'workos', 'service')",
2975 "name": "accounts_provider_check",
2976 "entityType": "checks",
2977 "schema": "public",
2978 "table": "accounts"
2979 },
2980 {
2981 "value": "\"status\" in ('pending', 'resolved', 'cancelled')",
2982 "name": "execution_interactions_status_check",
2983 "entityType": "checks",
2984 "schema": "public",
2985 "table": "execution_interactions"
2986 },
2987 {
2988 "value": "\"status\" in ('pending', 'running', 'waiting_for_interaction', 'completed', 'failed', 'cancelled')",
2989 "name": "executions_status_check",
2990 "entityType": "checks",
2991 "schema": "public",
2992 "table": "executions"
2993 },
2994 {
2995 "value": "\"role\" in ('viewer', 'editor', 'admin', 'owner')",
2996 "name": "organization_memberships_role_check",
2997 "entityType": "checks",
2998 "schema": "public",
2999 "table": "organization_memberships"
3000 },
3001 {
3002 "value": "\"status\" in ('invited', 'active', 'suspended', 'removed')",
3003 "name": "organization_memberships_status_check",
3004 "entityType": "checks",
3005 "schema": "public",
3006 "table": "organization_memberships"
3007 },
3008 {
3009 "value": "\"status\" in ('active', 'suspended', 'archived')",
3010 "name": "organizations_status_check",
3011 "entityType": "checks",
3012 "schema": "public",
3013 "table": "organizations"
3014 },
3015 {
3016 "value": "\"resource_type\" in ('all_tools', 'source', 'namespace', 'tool_path')",
3017 "name": "policies_resource_type_check",
3018 "entityType": "checks",
3019 "schema": "public",
3020 "table": "policies"
3021 },
3022 {
3023 "value": "\"match_type\" in ('glob', 'exact')",
3024 "name": "policies_match_type_check",
3025 "entityType": "checks",
3026 "schema": "public",
3027 "table": "policies"
3028 },
3029 {
3030 "value": "\"effect\" in ('allow', 'deny')",
3031 "name": "policies_effect_check",
3032 "entityType": "checks",
3033 "schema": "public",
3034 "table": "policies"
3035 },
3036 {
3037 "value": "\"approval_mode\" in ('auto', 'required')",
3038 "name": "policies_approval_mode_check",
3039 "entityType": "checks",
3040 "schema": "public",
3041 "table": "policies"
3042 },
3043 {
3044 "value": "\"purpose\" in ('auth_material', 'oauth_access_token', 'oauth_refresh_token', 'oauth_client_info')",
3045 "name": "secret_materials_purpose_check",
3046 "entityType": "checks",
3047 "schema": "public",
3048 "table": "secret_materials"
3049 },
3050 {
3051 "value": "\"strategy\" in ('oauth2_authorization_code')",
3052 "name": "source_auth_sessions_strategy_check",
3053 "entityType": "checks",
3054 "schema": "public",
3055 "table": "source_auth_sessions"
3056 },
3057 {
3058 "value": "\"status\" in ('pending', 'completed', 'failed', 'cancelled')",
3059 "name": "source_auth_sessions_status_check",
3060 "entityType": "checks",
3061 "schema": "public",
3062 "table": "source_auth_sessions"
3063 },
3064 {
3065 "value": "\"kind\" in ('mcp', 'openapi', 'graphql', 'internal')",
3066 "name": "sources_kind_check",
3067 "entityType": "checks",
3068 "schema": "public",
3069 "table": "sources"
3070 },
3071 {
3072 "value": "\"status\" in ('draft', 'probing', 'auth_required', 'connected', 'error')",
3073 "name": "sources_status_check",
3074 "entityType": "checks",
3075 "schema": "public",
3076 "table": "sources"
3077 },
3078 {
3079 "value": "\"transport\" is null or \"transport\" in ('auto', 'streamable-http', 'sse')",
3080 "name": "sources_transport_check",
3081 "entityType": "checks",
3082 "schema": "public",
3083 "table": "sources"
3084 },
3085 {
3086 "value": "\"auth_kind\" in ('none', 'bearer', 'oauth2')",
3087 "name": "sources_auth_kind_check",
3088 "entityType": "checks",
3089 "schema": "public",
3090 "table": "sources"
3091 },
3092 {
3093 "value": "\"location\" in ('path', 'query', 'header', 'cookie')",
3094 "name": "tool_artifact_parameters_location_check",
3095 "entityType": "checks",
3096 "schema": "public",
3097 "table": "tool_artifact_parameters"
3098 },
3099 {
3100 "value": "\"provider_kind\" in ('mcp', 'openapi')",
3101 "name": "tool_artifacts_provider_kind_check",
3102 "entityType": "checks",
3103 "schema": "public",
3104 "table": "tool_artifacts"
3105 },
3106 {
3107 "value": "\"provider_kind\" <> 'mcp'\n or (\n \"mcp_tool_name\" is not null\n and \"openapi_method\" is null\n and \"openapi_path_template\" is null\n and \"openapi_operation_hash\" is null\n and \"openapi_request_body_required\" is null\n )",
3108 "name": "tool_artifacts_mcp_shape_check",
3109 "entityType": "checks",
3110 "schema": "public",
3111 "table": "tool_artifacts"
3112 },
3113 {
3114 "value": "\"provider_kind\" <> 'openapi'\n or (\n \"mcp_tool_name\" is null\n and \"openapi_method\" in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace')\n and \"openapi_path_template\" is not null\n and \"openapi_operation_hash\" is not null\n )",
3115 "name": "tool_artifacts_openapi_shape_check",
3116 "entityType": "checks",
3117 "schema": "public",
3118 "table": "tool_artifacts"
3119 }
3120 ],
3121 "renames": []
3122}
packages/control-plane/drizzle/20260307134000_openapi_tool_identity/migration.sql+0−37
@@ -1,37 +0,0 @@
1ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_raw_tool_id" text;--> statement-breakpoint
2ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_operation_id" text;--> statement-breakpoint
3ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_tags_json" text;--> statement-breakpoint
4UPDATE "tool_artifacts"
5SET
6 "openapi_raw_tool_id" = "tool_id",
7 "openapi_operation_id" = "tool_id",
8 "openapi_tags_json" = '[]'
9WHERE "provider_kind" = 'openapi';--> statement-breakpoint
10ALTER TABLE "tool_artifacts" DROP CONSTRAINT "tool_artifacts_mcp_shape_check";--> statement-breakpoint
11ALTER TABLE "tool_artifacts" DROP CONSTRAINT "tool_artifacts_openapi_shape_check";--> statement-breakpoint
12ALTER TABLE "tool_artifacts"
13ADD CONSTRAINT "tool_artifacts_mcp_shape_check" CHECK (
14 "provider_kind" <> 'mcp'
15 OR (
16 "mcp_tool_name" is not null
17 and "openapi_method" is null
18 and "openapi_path_template" is null
19 and "openapi_operation_hash" is null
20 and "openapi_raw_tool_id" is null
21 and "openapi_operation_id" is null
22 and "openapi_tags_json" is null
23 and "openapi_request_body_required" is null
24 )
25);--> statement-breakpoint
26ALTER TABLE "tool_artifacts"
27ADD CONSTRAINT "tool_artifacts_openapi_shape_check" CHECK (
28 "provider_kind" <> 'openapi'
29 OR (
30 "mcp_tool_name" is null
31 and "openapi_method" in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace')
32 and "openapi_path_template" is not null
33 and "openapi_operation_hash" is not null
34 and "openapi_raw_tool_id" is not null
35 and "openapi_tags_json" is not null
36 )
37);
packages/control-plane/drizzle/20260307152000_source_document_text/migration.sql+0−1
@@ -1 +0,0 @@
1ALTER TABLE "sources" ADD COLUMN "source_document_text" text;
packages/control-plane/drizzle/20260308041000_credentials_model/migration.sql+0−41
@@ -1,41 +0,0 @@
1CREATE TABLE "credentials" (
2 "id" text PRIMARY KEY NOT NULL,
3 "workspace_id" text NOT NULL,
4 "auth_kind" text NOT NULL,
5 "auth_header_name" text NOT NULL,
6 "auth_prefix" text NOT NULL,
7 "token_provider_id" text NOT NULL,
8 "token_handle" text NOT NULL,
9 "refresh_token_provider_id" text,
10 "refresh_token_handle" text,
11 "created_at" bigint NOT NULL,
12 "updated_at" bigint NOT NULL,
13 CONSTRAINT "credentials_auth_kind_check" CHECK ("auth_kind" in ('bearer', 'oauth2'))
14);
15--> statement-breakpoint
16CREATE INDEX "credentials_workspace_idx" ON "credentials" ("workspace_id","updated_at","id");
17--> statement-breakpoint
18ALTER TABLE "sources" DROP CONSTRAINT "sources_auth_kind_check";
19--> statement-breakpoint
20ALTER TABLE "sources" DROP COLUMN "auth_kind";
21--> statement-breakpoint
22ALTER TABLE "sources" DROP COLUMN "auth_header_name";
23--> statement-breakpoint
24ALTER TABLE "sources" DROP COLUMN "auth_prefix";
25--> statement-breakpoint
26DROP INDEX IF EXISTS "source_credential_bindings_workspace_idx";
27--> statement-breakpoint
28DROP TABLE "source_credential_bindings";
29--> statement-breakpoint
30CREATE TABLE "source_credential_bindings" (
31 "id" text PRIMARY KEY NOT NULL,
32 "workspace_id" text NOT NULL,
33 "source_id" text NOT NULL,
34 "credential_id" text NOT NULL,
35 "created_at" bigint NOT NULL,
36 "updated_at" bigint NOT NULL
37);
38--> statement-breakpoint
39CREATE UNIQUE INDEX "source_credential_bindings_workspace_source_idx" ON "source_credential_bindings" ("workspace_id","source_id");
40--> statement-breakpoint
41CREATE INDEX "source_credential_bindings_workspace_idx" ON "source_credential_bindings" ("workspace_id","updated_at","source_id");
packages/control-plane/drizzle/20260308120000_search_and_indexes/migration.sql+0−16
@@ -1,16 +0,0 @@
1DROP INDEX IF EXISTS "organization_memberships_org_idx";--> statement-breakpoint
2DROP INDEX IF EXISTS "organization_memberships_account_idx";--> statement-breakpoint
3CREATE INDEX "organization_memberships_org_updated_idx" ON "organization_memberships" ("organization_id","updated_at","id");--> statement-breakpoint
4CREATE INDEX "organization_memberships_account_updated_idx" ON "organization_memberships" ("account_id","updated_at","id");--> statement-breakpoint
5DROP INDEX IF EXISTS "workspaces_org_idx";--> statement-breakpoint
6CREATE INDEX "workspaces_org_updated_idx" ON "workspaces" ("organization_id","updated_at","id");--> statement-breakpoint
7CREATE INDEX "sources_workspace_updated_idx" ON "sources" ("workspace_id","updated_at","source_id");--> statement-breakpoint
8CREATE INDEX "tool_artifacts_search_text_idx" ON "tool_artifacts" USING gin (to_tsvector('simple', "search_text"));--> statement-breakpoint
9DROP INDEX IF EXISTS "tool_artifact_parameters_lookup_idx";--> statement-breakpoint
10DROP INDEX IF EXISTS "tool_artifact_request_body_content_types_lookup_idx";--> statement-breakpoint
11DROP INDEX IF EXISTS "tool_artifact_ref_hint_keys_lookup_idx";--> statement-breakpoint
12CREATE INDEX "source_auth_sessions_pending_idx" ON "source_auth_sessions" ("workspace_id","source_id","status","updated_at","id");--> statement-breakpoint
13DROP INDEX IF EXISTS "policies_workspace_idx";--> statement-breakpoint
14CREATE INDEX "policies_workspace_priority_idx" ON "policies" ("workspace_id","priority" DESC,"updated_at","id");--> statement-breakpoint
15CREATE INDEX "local_installations_organization_idx" ON "local_installations" ("organization_id");--> statement-breakpoint
16CREATE INDEX "local_installations_workspace_idx" ON "local_installations" ("workspace_id");
packages/control-plane/drizzle/20260308140000_secret_materials_name/migration.sql+0−1
@@ -1 +0,0 @@
1ALTER TABLE "secret_materials" ADD COLUMN "name" text;
packages/control-plane/drizzle/20260309103000_policy_scope_and_interaction_purpose/migration.sql+0−17
@@ -1,17 +0,0 @@
1ALTER TABLE "policies" ADD COLUMN "scope_type" text;--> statement-breakpoint
2ALTER TABLE "policies" ADD COLUMN "organization_id" text;--> statement-breakpoint
3UPDATE "policies" AS "pol"
4SET
5 "scope_type" = 'workspace',
6 "organization_id" = "ws"."organization_id"
7FROM "workspaces" AS "ws"
8WHERE "pol"."workspace_id" = "ws"."id";--> statement-breakpoint
9ALTER TABLE "policies" ALTER COLUMN "scope_type" SET NOT NULL;--> statement-breakpoint
10ALTER TABLE "policies" ALTER COLUMN "organization_id" SET NOT NULL;--> statement-breakpoint
11ALTER TABLE "policies" ALTER COLUMN "workspace_id" DROP NOT NULL;--> statement-breakpoint
12ALTER TABLE "policies" ADD CONSTRAINT "policies_scope_type_check" CHECK ("scope_type" in ('organization', 'workspace'));--> statement-breakpoint
13ALTER TABLE "policies" ADD CONSTRAINT "policies_scope_consistency_check" CHECK (("scope_type" = 'organization' and "workspace_id" is null) or ("scope_type" = 'workspace' and "workspace_id" is not null));--> statement-breakpoint
14DROP INDEX IF EXISTS "policies_workspace_priority_idx";--> statement-breakpoint
15CREATE INDEX "policies_organization_priority_idx" ON "policies" ("organization_id","priority" DESC,"updated_at","id");--> statement-breakpoint
16CREATE INDEX "policies_workspace_priority_idx" ON "policies" ("workspace_id","priority" DESC,"updated_at","id");--> statement-breakpoint
17ALTER TABLE "execution_interactions" ADD COLUMN "purpose" text NOT NULL DEFAULT 'elicitation';
packages/control-plane/drizzle/20260310120000_source_recipes_and_actor_credentials/migration.sql+0−491
@@ -1,491 +0,0 @@
1ALTER TABLE "sources" ADD COLUMN "recipe_id" text;--> statement-breakpoint
2ALTER TABLE "sources" ADD COLUMN "recipe_revision_id" text;--> statement-breakpoint
3ALTER TABLE "sources" ADD COLUMN "binding_config_json" text;--> statement-breakpoint
4
5CREATE TABLE "source_recipes" (
6 "id" text PRIMARY KEY NOT NULL,
7 "kind" text NOT NULL,
8 "importer_kind" text NOT NULL,
9 "provider_key" text NOT NULL,
10 "name" text NOT NULL,
11 "summary" text,
12 "visibility" text NOT NULL,
13 "latest_revision_id" text NOT NULL,
14 "created_at" bigint NOT NULL,
15 "updated_at" bigint NOT NULL,
16 CONSTRAINT "source_recipes_kind_check" CHECK ("kind" in ('http_recipe', 'graphql_recipe', 'mcp_recipe', 'internal_recipe')),
17 CONSTRAINT "source_recipes_importer_kind_check" CHECK ("importer_kind" in (
18 'openapi',
19 'google_discovery',
20 'postman_collection',
21 'snippet_bundle',
22 'graphql_introspection',
23 'mcp_manifest',
24 'internal_manifest'
25 )),
26 CONSTRAINT "source_recipes_visibility_check" CHECK ("visibility" in ('private', 'workspace', 'organization', 'public'))
27);--> statement-breakpoint
28
29CREATE TABLE "source_recipe_revisions" (
30 "id" text PRIMARY KEY NOT NULL,
31 "recipe_id" text NOT NULL,
32 "revision_number" bigint NOT NULL,
33 "source_config_json" text NOT NULL,
34 "manifest_json" text,
35 "manifest_hash" text,
36 "created_at" bigint NOT NULL,
37 "updated_at" bigint NOT NULL
38);--> statement-breakpoint
39
40CREATE TABLE "source_recipe_documents" (
41 "id" text PRIMARY KEY NOT NULL,
42 "recipe_revision_id" text NOT NULL,
43 "document_kind" text NOT NULL,
44 "document_key" text NOT NULL,
45 "content_text" text NOT NULL,
46 "content_hash" text NOT NULL,
47 "fetched_at" bigint,
48 "created_at" bigint NOT NULL,
49 "updated_at" bigint NOT NULL,
50 CONSTRAINT "source_recipe_documents_kind_check" CHECK ("document_kind" in (
51 'google_discovery',
52 'openapi',
53 'postman_collection',
54 'postman_environment',
55 'graphql_introspection',
56 'mcp_manifest'
57 ))
58);--> statement-breakpoint
59
60CREATE TABLE "source_recipe_operations" (
61 "id" text PRIMARY KEY NOT NULL,
62 "recipe_revision_id" text NOT NULL,
63 "operation_key" text NOT NULL,
64 "transport_kind" text NOT NULL,
65 "tool_id" text NOT NULL,
66 "title" text,
67 "description" text,
68 "operation_kind" text NOT NULL,
69 "search_text" text NOT NULL,
70 "input_schema_json" text,
71 "output_schema_json" text,
72 "provider_kind" text NOT NULL,
73 "provider_data_json" text,
74 "mcp_tool_name" text,
75 "openapi_method" text,
76 "openapi_path_template" text,
77 "openapi_operation_hash" text,
78 "openapi_raw_tool_id" text,
79 "openapi_operation_id" text,
80 "openapi_tags_json" text,
81 "openapi_request_body_required" boolean,
82 "graphql_operation_type" text,
83 "graphql_operation_name" text,
84 "created_at" bigint NOT NULL,
85 "updated_at" bigint NOT NULL,
86 CONSTRAINT "source_recipe_operations_transport_kind_check" CHECK ("transport_kind" in ('http', 'graphql', 'mcp', 'internal')),
87 CONSTRAINT "source_recipe_operations_kind_check" CHECK ("operation_kind" in ('read', 'write', 'delete', 'unknown')),
88 CONSTRAINT "source_recipe_operations_provider_kind_check" CHECK ("provider_kind" in ('mcp', 'openapi', 'graphql', 'internal'))
89);--> statement-breakpoint
90
91CREATE TABLE "workspace_source_oauth_clients" (
92 "id" text PRIMARY KEY NOT NULL,
93 "workspace_id" text NOT NULL,
94 "source_id" text NOT NULL,
95 "provider_key" text NOT NULL,
96 "client_id" text NOT NULL,
97 "client_secret_provider_id" text,
98 "client_secret_handle" text,
99 "client_metadata_json" text,
100 "created_at" bigint NOT NULL,
101 "updated_at" bigint NOT NULL
102);--> statement-breakpoint
103
104UPDATE "sources"
105SET
106 "recipe_id" = 'src_recipe_' || md5("workspace_id" || ':' || "source_id"),
107 "recipe_revision_id" = 'src_recipe_rev_' || md5("workspace_id" || ':' || "source_id"),
108 "binding_config_json" = null;--> statement-breakpoint
109
110INSERT INTO "source_recipes" (
111 "id",
112 "kind",
113 "importer_kind",
114 "provider_key",
115 "name",
116 "summary",
117 "visibility",
118 "latest_revision_id",
119 "created_at",
120 "updated_at"
121)
122SELECT
123 "s"."recipe_id",
124 CASE
125 WHEN "s"."kind" = 'openapi' THEN 'http_recipe'
126 WHEN "s"."kind" = 'graphql' THEN 'graphql_recipe'
127 WHEN "s"."kind" = 'mcp' THEN 'mcp_recipe'
128 ELSE 'internal_recipe'
129 END,
130 CASE
131 WHEN "s"."kind" = 'openapi' THEN 'openapi'
132 WHEN "s"."kind" = 'graphql' THEN 'graphql_introspection'
133 WHEN "s"."kind" = 'mcp' THEN 'mcp_manifest'
134 ELSE 'internal_manifest'
135 END,
136 CASE
137 WHEN "s"."kind" = 'openapi' THEN 'generic_http'
138 WHEN "s"."kind" = 'graphql' THEN 'generic_graphql'
139 WHEN "s"."kind" = 'mcp' THEN 'generic_mcp'
140 ELSE 'generic_internal'
141 END,
142 "s"."name",
143 null,
144 'workspace',
145 "s"."recipe_revision_id",
146 "s"."created_at",
147 "s"."updated_at"
148FROM "sources" AS "s";--> statement-breakpoint
149
150INSERT INTO "source_recipe_revisions" (
151 "id",
152 "recipe_id",
153 "revision_number",
154 "source_config_json",
155 "manifest_json",
156 "manifest_hash",
157 "created_at",
158 "updated_at"
159)
160SELECT
161 "s"."recipe_revision_id",
162 "s"."recipe_id",
163 1,
164 CASE
165 WHEN "s"."kind" = 'mcp' THEN jsonb_build_object(
166 'kind', 'mcp',
167 'endpoint', "s"."endpoint",
168 'transport', "s"."transport",
169 'queryParams', COALESCE("s"."query_params_json"::jsonb, 'null'::jsonb),
170 'headers', COALESCE("s"."headers_json"::jsonb, 'null'::jsonb)
171 )::text
172 WHEN "s"."kind" = 'openapi' THEN jsonb_build_object(
173 'kind', 'openapi',
174 'endpoint', "s"."endpoint",
175 'specUrl', COALESCE("s"."spec_url", "s"."endpoint"),
176 'defaultHeaders', COALESCE("s"."default_headers_json"::jsonb, 'null'::jsonb)
177 )::text
178 WHEN "s"."kind" = 'graphql' THEN jsonb_build_object(
179 'kind', 'graphql',
180 'endpoint', "s"."endpoint",
181 'defaultHeaders', COALESCE("s"."default_headers_json"::jsonb, 'null'::jsonb)
182 )::text
183 ELSE jsonb_build_object(
184 'kind', 'internal',
185 'endpoint', "s"."endpoint"
186 )::text
187 END,
188 null,
189 "s"."source_hash",
190 "s"."created_at",
191 "s"."updated_at"
192FROM "sources" AS "s";--> statement-breakpoint
193
194INSERT INTO "source_recipe_documents" (
195 "id",
196 "recipe_revision_id",
197 "document_kind",
198 "document_key",
199 "content_text",
200 "content_hash",
201 "fetched_at",
202 "created_at",
203 "updated_at"
204)
205SELECT
206 'src_recipe_doc_' || md5("s"."workspace_id" || ':' || "s"."source_id" || ':primary'),
207 "s"."recipe_revision_id",
208 CASE
209 WHEN "s"."kind" = 'openapi' THEN 'openapi'
210 WHEN "s"."kind" = 'graphql' THEN 'graphql_introspection'
211 ELSE 'mcp_manifest'
212 END,
213 CASE
214 WHEN "s"."kind" = 'openapi' THEN COALESCE("s"."spec_url", "s"."endpoint")
215 ELSE "s"."endpoint"
216 END,
217 "s"."source_document_text",
218 md5("s"."source_document_text"),
219 "s"."updated_at",
220 "s"."created_at",
221 "s"."updated_at"
222FROM "sources" AS "s"
223WHERE "s"."source_document_text" IS NOT NULL;--> statement-breakpoint
224
225INSERT INTO "source_recipe_operations" (
226 "id",
227 "recipe_revision_id",
228 "operation_key",
229 "transport_kind",
230 "tool_id",
231 "title",
232 "description",
233 "operation_kind",
234 "search_text",
235 "input_schema_json",
236 "output_schema_json",
237 "provider_kind",
238 "provider_data_json",
239 "mcp_tool_name",
240 "openapi_method",
241 "openapi_path_template",
242 "openapi_operation_hash",
243 "openapi_raw_tool_id",
244 "openapi_operation_id",
245 "openapi_tags_json",
246 "openapi_request_body_required",
247 "graphql_operation_type",
248 "graphql_operation_name",
249 "created_at",
250 "updated_at"
251)
252SELECT
253 'src_recipe_op_' || md5("ta"."workspace_id" || ':' || "ta"."source_id" || ':' || "ta"."path"),
254 "s"."recipe_revision_id",
255 "ta"."tool_id",
256 CASE
257 WHEN "ta"."provider_kind" = 'openapi' THEN 'http'
258 ELSE 'mcp'
259 END,
260 "ta"."tool_id",
261 "ta"."title",
262 "ta"."description",
263 CASE
264 WHEN "ta"."provider_kind" = 'openapi' AND upper(COALESCE("ta"."openapi_method", '')) IN ('GET', 'HEAD') THEN 'read'
265 WHEN "ta"."provider_kind" = 'openapi' AND upper(COALESCE("ta"."openapi_method", '')) = 'DELETE' THEN 'delete'
266 WHEN "ta"."provider_kind" = 'openapi' THEN 'write'
267 ELSE 'unknown'
268 END,
269 "ta"."search_text",
270 "ta"."input_schema_json",
271 "ta"."output_schema_json",
272 "ta"."provider_kind",
273 CASE
274 WHEN "ta"."provider_kind" = 'mcp' THEN jsonb_build_object(
275 'kind', 'mcp',
276 'toolId', "ta"."tool_id",
277 'toolName', COALESCE("ta"."mcp_tool_name", "ta"."title", "ta"."path"),
278 'description', "ta"."description"
279 )::text
280 ELSE jsonb_build_object(
281 'kind', 'openapi',
282 'toolId', "ta"."tool_id",
283 'rawToolId', "ta"."openapi_raw_tool_id",
284 'operationId', "ta"."openapi_operation_id",
285 'tags', COALESCE("ta"."openapi_tags_json"::jsonb, '[]'::jsonb),
286 'method', "ta"."openapi_method",
287 'path', "ta"."openapi_path_template",
288 'operationHash', "ta"."openapi_operation_hash"
289 )::text
290 END,
291 "ta"."mcp_tool_name",
292 "ta"."openapi_method",
293 "ta"."openapi_path_template",
294 "ta"."openapi_operation_hash",
295 "ta"."openapi_raw_tool_id",
296 "ta"."openapi_operation_id",
297 "ta"."openapi_tags_json",
298 "ta"."openapi_request_body_required",
299 null,
300 null,
301 "ta"."created_at",
302 "ta"."updated_at"
303FROM "tool_artifacts" AS "ta"
304JOIN "sources" AS "s"
305 ON "s"."workspace_id" = "ta"."workspace_id"
306 AND "s"."source_id" = "ta"."source_id";--> statement-breakpoint
307
308WITH "mcp_manifests" AS (
309 SELECT
310 "s"."workspace_id",
311 "s"."source_id",
312 "s"."recipe_revision_id",
313 jsonb_build_object(
314 'version', 1,
315 'tools', COALESCE(
316 jsonb_agg(
317 jsonb_build_object(
318 'toolId', "ta"."tool_id",
319 'toolName', COALESCE("ta"."mcp_tool_name", "ta"."title", "ta"."path"),
320 'description', "ta"."description",
321 'inputSchemaJson', "ta"."input_schema_json",
322 'outputSchemaJson', "ta"."output_schema_json"
323 )
324 ORDER BY "ta"."path"
325 ) FILTER (WHERE "ta"."provider_kind" = 'mcp'),
326 '[]'::jsonb
327 )
328 )::text AS "manifest_json"
329 FROM "sources" AS "s"
330 LEFT JOIN "tool_artifacts" AS "ta"
331 ON "ta"."workspace_id" = "s"."workspace_id"
332 AND "ta"."source_id" = "s"."source_id"
333 WHERE "s"."kind" = 'mcp'
334 GROUP BY "s"."workspace_id", "s"."source_id", "s"."recipe_revision_id"
335)
336UPDATE "source_recipe_revisions" AS "rev"
337SET
338 "manifest_json" = "mcp"."manifest_json",
339 "manifest_hash" = md5("mcp"."manifest_json")
340FROM "mcp_manifests" AS "mcp"
341WHERE "rev"."id" = "mcp"."recipe_revision_id";--> statement-breakpoint
342
343INSERT INTO "source_recipe_documents" (
344 "id",
345 "recipe_revision_id",
346 "document_kind",
347 "document_key",
348 "content_text",
349 "content_hash",
350 "fetched_at",
351 "created_at",
352 "updated_at"
353)
354SELECT
355 'src_recipe_doc_' || md5("s"."workspace_id" || ':' || "s"."source_id" || ':mcp_manifest'),
356 "s"."recipe_revision_id",
357 'mcp_manifest',
358 "s"."endpoint",
359 "rev"."manifest_json",
360 md5("rev"."manifest_json"),
361 "s"."updated_at",
362 "s"."created_at",
363 "s"."updated_at"
364FROM "sources" AS "s"
365JOIN "source_recipe_revisions" AS "rev"
366 ON "rev"."id" = "s"."recipe_revision_id"
367WHERE "s"."kind" = 'mcp'
368 AND "rev"."manifest_json" IS NOT NULL
369 AND NOT EXISTS (
370 SELECT 1
371 FROM "source_recipe_documents" AS "doc"
372 WHERE "doc"."recipe_revision_id" = "s"."recipe_revision_id"
373 AND "doc"."document_kind" = 'mcp_manifest'
374 AND "doc"."document_key" = "s"."endpoint"
375 );--> statement-breakpoint
376
377ALTER TABLE "sources" ALTER COLUMN "recipe_id" SET NOT NULL;--> statement-breakpoint
378ALTER TABLE "sources" ALTER COLUMN "recipe_revision_id" SET NOT NULL;--> statement-breakpoint
379CREATE INDEX "sources_workspace_recipe_idx" ON "sources" ("workspace_id","recipe_id","updated_at","source_id");--> statement-breakpoint
380
381CREATE TABLE "workspace_source_credentials" (
382 "id" text PRIMARY KEY NOT NULL,
383 "workspace_id" text NOT NULL,
384 "source_id" text NOT NULL,
385 "actor_account_id" text,
386 "auth_kind" text NOT NULL,
387 "auth_header_name" text NOT NULL,
388 "auth_prefix" text NOT NULL,
389 "token_provider_id" text NOT NULL,
390 "token_handle" text NOT NULL,
391 "refresh_token_provider_id" text,
392 "refresh_token_handle" text,
393 "created_at" bigint NOT NULL,
394 "updated_at" bigint NOT NULL,
395 CONSTRAINT "credentials_auth_kind_check" CHECK ("auth_kind" in ('bearer', 'oauth2'))
396);--> statement-breakpoint
397
398INSERT INTO "workspace_source_credentials" (
399 "id",
400 "workspace_id",
401 "source_id",
402 "actor_account_id",
403 "auth_kind",
404 "auth_header_name",
405 "auth_prefix",
406 "token_provider_id",
407 "token_handle",
408 "refresh_token_provider_id",
409 "refresh_token_handle",
410 "created_at",
411 "updated_at"
412)
413SELECT
414 CASE
415 WHEN count(*) OVER (PARTITION BY "c"."id") = 1 THEN "c"."id"
416 ELSE "c"."id" || ':' || "scb"."source_id"
417 END,
418 "c"."workspace_id",
419 "scb"."source_id",
420 null,
421 "c"."auth_kind",
422 "c"."auth_header_name",
423 "c"."auth_prefix",
424 "c"."token_provider_id",
425 "c"."token_handle",
426 "c"."refresh_token_provider_id",
427 "c"."refresh_token_handle",
428 "c"."created_at",
429 "c"."updated_at"
430FROM "credentials" AS "c"
431JOIN "source_credential_bindings" AS "scb"
432 ON "scb"."workspace_id" = "c"."workspace_id"
433 AND "scb"."credential_id" = "c"."id";--> statement-breakpoint
434
435DROP TABLE "credentials";--> statement-breakpoint
436DROP TABLE "source_credential_bindings";--> statement-breakpoint
437CREATE INDEX "credentials_workspace_idx" ON "workspace_source_credentials" ("workspace_id","updated_at","id");--> statement-breakpoint
438CREATE UNIQUE INDEX "credentials_workspace_source_actor_idx" ON "workspace_source_credentials" ("workspace_id","source_id","actor_account_id");--> statement-breakpoint
439CREATE INDEX "credentials_workspace_source_idx" ON "workspace_source_credentials" ("workspace_id","source_id","updated_at","id");--> statement-breakpoint
440
441ALTER TABLE "source_auth_sessions" ADD COLUMN "actor_account_id" text;--> statement-breakpoint
442ALTER TABLE "source_auth_sessions" ADD COLUMN "provider_kind" text;--> statement-breakpoint
443ALTER TABLE "source_auth_sessions" ADD COLUMN "session_data_json" text;--> statement-breakpoint
444
445UPDATE "source_auth_sessions"
446SET
447 "provider_kind" = 'mcp_oauth',
448 "session_data_json" = jsonb_build_object(
449 'kind', 'mcp_oauth',
450 'endpoint', "endpoint",
451 'redirectUri', "redirect_uri",
452 'scope', "scope",
453 'resourceMetadataUrl', "resource_metadata_url",
454 'authorizationServerUrl', "authorization_server_url",
455 'resourceMetadataJson', "resource_metadata_json",
456 'authorizationServerMetadataJson', "authorization_server_metadata_json",
457 'clientInformationJson', "client_information_json",
458 'codeVerifier', "code_verifier",
459 'authorizationUrl', "authorization_url"
460 )::text;--> statement-breakpoint
461
462ALTER TABLE "source_auth_sessions" ALTER COLUMN "provider_kind" SET NOT NULL;--> statement-breakpoint
463ALTER TABLE "source_auth_sessions" ALTER COLUMN "session_data_json" SET NOT NULL;--> statement-breakpoint
464DROP INDEX IF EXISTS "source_auth_sessions_pending_idx";--> statement-breakpoint
465ALTER TABLE "source_auth_sessions" DROP CONSTRAINT "source_auth_sessions_strategy_check";--> statement-breakpoint
466ALTER TABLE "source_auth_sessions" DROP COLUMN "strategy";--> statement-breakpoint
467ALTER TABLE "source_auth_sessions" DROP COLUMN "endpoint";--> statement-breakpoint
468ALTER TABLE "source_auth_sessions" DROP COLUMN "redirect_uri";--> statement-breakpoint
469ALTER TABLE "source_auth_sessions" DROP COLUMN "scope";--> statement-breakpoint
470ALTER TABLE "source_auth_sessions" DROP COLUMN "resource_metadata_url";--> statement-breakpoint
471ALTER TABLE "source_auth_sessions" DROP COLUMN "authorization_server_url";--> statement-breakpoint
472ALTER TABLE "source_auth_sessions" DROP COLUMN "resource_metadata_json";--> statement-breakpoint
473ALTER TABLE "source_auth_sessions" DROP COLUMN "authorization_server_metadata_json";--> statement-breakpoint
474ALTER TABLE "source_auth_sessions" DROP COLUMN "client_information_json";--> statement-breakpoint
475ALTER TABLE "source_auth_sessions" DROP COLUMN "code_verifier";--> statement-breakpoint
476ALTER TABLE "source_auth_sessions" DROP COLUMN "authorization_url";--> statement-breakpoint
477ALTER TABLE "source_auth_sessions" ADD CONSTRAINT "source_auth_sessions_provider_kind_check" CHECK ("provider_kind" in ('mcp_oauth', 'oauth2_pkce'));--> statement-breakpoint
478CREATE INDEX "source_auth_sessions_pending_idx" ON "source_auth_sessions" ("workspace_id","source_id","actor_account_id","status","updated_at","id");--> statement-breakpoint
479
480CREATE INDEX "source_recipes_provider_updated_idx" ON "source_recipes" ("provider_key","updated_at","id");--> statement-breakpoint
481CREATE INDEX "source_recipes_visibility_updated_idx" ON "source_recipes" ("visibility","updated_at","id");--> statement-breakpoint
482CREATE UNIQUE INDEX "source_recipe_revisions_recipe_revision_idx" ON "source_recipe_revisions" ("recipe_id","revision_number");--> statement-breakpoint
483CREATE UNIQUE INDEX "source_recipe_revisions_recipe_manifest_idx" ON "source_recipe_revisions" ("recipe_id","manifest_hash");--> statement-breakpoint
484CREATE INDEX "source_recipe_revisions_recipe_created_idx" ON "source_recipe_revisions" ("recipe_id","created_at","id");--> statement-breakpoint
485CREATE UNIQUE INDEX "source_recipe_documents_revision_kind_key_idx" ON "source_recipe_documents" ("recipe_revision_id","document_kind","document_key");--> statement-breakpoint
486CREATE INDEX "source_recipe_documents_revision_created_idx" ON "source_recipe_documents" ("recipe_revision_id","created_at","id");--> statement-breakpoint
487CREATE UNIQUE INDEX "source_recipe_operations_revision_operation_key_idx" ON "source_recipe_operations" ("recipe_revision_id","operation_key");--> statement-breakpoint
488CREATE INDEX "source_recipe_operations_revision_tool_idx" ON "source_recipe_operations" ("recipe_revision_id","tool_id","updated_at","id");--> statement-breakpoint
489CREATE INDEX "source_recipe_operations_search_text_idx" ON "source_recipe_operations" USING gin (to_tsvector('simple', "search_text"));--> statement-breakpoint
490CREATE UNIQUE INDEX "workspace_source_oauth_clients_workspace_source_provider_idx" ON "workspace_source_oauth_clients" ("workspace_id","source_id","provider_key");--> statement-breakpoint
491CREATE INDEX "workspace_source_oauth_clients_workspace_idx" ON "workspace_source_oauth_clients" ("workspace_id","updated_at","source_id");
packages/control-plane/drizzle/20260310183000_source_recipe_schema_bundles/migration.sql+0−14
@@ -1,14 +0,0 @@
1CREATE TABLE "source_recipe_schema_bundles" (
2 "id" text PRIMARY KEY NOT NULL,
3 "recipe_revision_id" text NOT NULL,
4 "bundle_kind" text NOT NULL,
5 "refs_json" text NOT NULL,
6 "content_hash" text NOT NULL,
7 "created_at" bigint NOT NULL,
8 "updated_at" bigint NOT NULL,
9 CONSTRAINT "source_recipe_schema_bundles_kind_check" CHECK ("source_recipe_schema_bundles"."bundle_kind" in ('json_schema_ref_map'))
10);
11--> statement-breakpoint
12CREATE UNIQUE INDEX "source_recipe_schema_bundles_revision_kind_idx" ON "source_recipe_schema_bundles" USING btree ("recipe_revision_id","bundle_kind");
13--> statement-breakpoint
14CREATE INDEX "source_recipe_schema_bundles_revision_created_idx" ON "source_recipe_schema_bundles" USING btree ("recipe_revision_id","created_at","id");
packages/control-plane/drizzle/20260310223000_source_adapter_registry_strings/migration.sql+0−9
@@ -1,9 +0,0 @@
1ALTER TABLE "sources" DROP CONSTRAINT IF EXISTS "sources_kind_check";
2--> statement-breakpoint
3ALTER TABLE "source_recipes" DROP CONSTRAINT IF EXISTS "source_recipes_importer_kind_check";
4--> statement-breakpoint
5ALTER TABLE "source_recipe_documents" DROP CONSTRAINT IF EXISTS "source_recipe_documents_kind_check";
6--> statement-breakpoint
7ALTER TABLE "source_recipe_schema_bundles" DROP CONSTRAINT IF EXISTS "source_recipe_schema_bundles_kind_check";
8--> statement-breakpoint
9ALTER TABLE "source_recipe_operations" DROP CONSTRAINT IF EXISTS "source_recipe_operations_provider_kind_check";
packages/control-plane/drizzle/20260310233000_source_adapter_families_and_data_migrations/migration.sql+0−23
@@ -1,23 +0,0 @@
1ALTER TABLE "source_recipes"
2DROP CONSTRAINT IF EXISTS "source_recipes_kind_check";--> statement-breakpoint
3
4UPDATE "source_recipes"
5SET "kind" = 'http_api'
6WHERE "kind" IN ('http_recipe', 'graphql_recipe');--> statement-breakpoint
7
8UPDATE "source_recipes"
9SET "kind" = 'mcp'
10WHERE "kind" = 'mcp_recipe';--> statement-breakpoint
11
12UPDATE "source_recipes"
13SET "kind" = 'internal'
14WHERE "kind" = 'internal_recipe';--> statement-breakpoint
15
16ALTER TABLE "source_recipes"
17ADD CONSTRAINT "source_recipes_kind_check"
18CHECK ("kind" in ('http_api', 'mcp', 'internal'));--> statement-breakpoint
19
20CREATE TABLE IF NOT EXISTS "control_plane_data_migrations" (
21 "id" text PRIMARY KEY NOT NULL,
22 "applied_at" bigint NOT NULL
23);
packages/control-plane/drizzle/20260311010000_source_binding_configs/migration.sql+0−62
@@ -1,62 +0,0 @@
1UPDATE "sources"
2SET "binding_config_json" = CASE
3 WHEN "binding_config_json" IS NOT NULL AND btrim("binding_config_json") <> '' THEN "binding_config_json"
4 WHEN "kind" = 'openapi' THEN jsonb_build_object(
5 'adapterKey', 'openapi',
6 'specUrl', COALESCE("spec_url", "endpoint"),
7 'defaultHeaders',
8 CASE
9 WHEN "default_headers_json" IS NULL OR btrim("default_headers_json") = '' THEN NULL
10 ELSE "default_headers_json"::jsonb
11 END
12 )::text
13 WHEN "kind" = 'graphql' THEN jsonb_build_object(
14 'adapterKey', 'graphql',
15 'defaultHeaders',
16 CASE
17 WHEN "default_headers_json" IS NULL OR btrim("default_headers_json") = '' THEN NULL
18 ELSE "default_headers_json"::jsonb
19 END
20 )::text
21 WHEN "kind" = 'mcp' THEN jsonb_build_object(
22 'adapterKey', 'mcp',
23 'transport', "transport",
24 'queryParams',
25 CASE
26 WHEN "query_params_json" IS NULL OR btrim("query_params_json") = '' THEN NULL
27 ELSE "query_params_json"::jsonb
28 END,
29 'headers',
30 CASE
31 WHEN "headers_json" IS NULL OR btrim("headers_json") = '' THEN NULL
32 ELSE "headers_json"::jsonb
33 END
34 )::text
35 ELSE jsonb_build_object(
36 'adapterKey', "kind"
37 )::text
38END;--> statement-breakpoint
39
40ALTER TABLE "sources"
41ALTER COLUMN "binding_config_json" SET NOT NULL;--> statement-breakpoint
42
43ALTER TABLE "sources"
44DROP CONSTRAINT IF EXISTS "sources_transport_check";--> statement-breakpoint
45
46ALTER TABLE "sources"
47DROP COLUMN IF EXISTS "transport";--> statement-breakpoint
48
49ALTER TABLE "sources"
50DROP COLUMN IF EXISTS "query_params_json";--> statement-breakpoint
51
52ALTER TABLE "sources"
53DROP COLUMN IF EXISTS "headers_json";--> statement-breakpoint
54
55ALTER TABLE "sources"
56DROP COLUMN IF EXISTS "spec_url";--> statement-breakpoint
57
58ALTER TABLE "sources"
59DROP COLUMN IF EXISTS "default_headers_json";--> statement-breakpoint
60
61ALTER TABLE "sources"
62DROP COLUMN IF EXISTS "source_document_text";
packages/control-plane/drizzle/20260311133000_import_auth_slots_and_materialization_hash/migration.sql+0−82
@@ -1,82 +0,0 @@
1ALTER TABLE "sources" ADD COLUMN "import_auth_policy" text;--> statement-breakpoint
2
3UPDATE "sources"
4SET "import_auth_policy" = CASE
5 WHEN "kind" = 'internal' THEN 'none'
6 ELSE 'reuse_runtime'
7END
8WHERE "import_auth_policy" IS NULL;--> statement-breakpoint
9
10ALTER TABLE "sources"
11ALTER COLUMN "import_auth_policy" SET NOT NULL;--> statement-breakpoint
12
13ALTER TABLE "sources"
14DROP CONSTRAINT IF EXISTS "sources_import_auth_policy_check";--> statement-breakpoint
15
16ALTER TABLE "sources"
17ADD CONSTRAINT "sources_import_auth_policy_check"
18CHECK ("import_auth_policy" in ('none', 'reuse_runtime', 'separate'));--> statement-breakpoint
19
20ALTER TABLE "source_recipe_revisions" ADD COLUMN "materialization_hash" text;--> statement-breakpoint
21
22UPDATE "source_recipe_revisions"
23SET "materialization_hash" = "manifest_hash"
24WHERE "materialization_hash" IS NULL;--> statement-breakpoint
25
26DROP INDEX IF EXISTS "source_recipe_revisions_recipe_manifest_idx";--> statement-breakpoint
27
28CREATE UNIQUE INDEX "source_recipe_revisions_recipe_materialization_idx"
29ON "source_recipe_revisions" ("recipe_id","materialization_hash");--> statement-breakpoint
30
31CREATE INDEX "source_recipe_revisions_recipe_manifest_idx"
32ON "source_recipe_revisions" ("recipe_id","manifest_hash");--> statement-breakpoint
33
34ALTER TABLE "workspace_source_credentials" ADD COLUMN "slot" text;--> statement-breakpoint
35
36UPDATE "workspace_source_credentials"
37SET "slot" = 'runtime'
38WHERE "slot" IS NULL;--> statement-breakpoint
39
40ALTER TABLE "workspace_source_credentials"
41ALTER COLUMN "slot" SET NOT NULL;--> statement-breakpoint
42
43DROP INDEX IF EXISTS "credentials_workspace_source_actor_idx";--> statement-breakpoint
44
45ALTER TABLE "workspace_source_credentials"
46DROP CONSTRAINT IF EXISTS "credentials_slot_check";--> statement-breakpoint
47
48ALTER TABLE "workspace_source_credentials"
49ADD CONSTRAINT "credentials_slot_check"
50CHECK ("slot" in ('runtime', 'import'));--> statement-breakpoint
51
52CREATE UNIQUE INDEX "credentials_workspace_source_actor_idx"
53ON "workspace_source_credentials" ("workspace_id","source_id","actor_account_id","slot");--> statement-breakpoint
54
55ALTER TABLE "source_auth_sessions" ADD COLUMN "credential_slot" text;--> statement-breakpoint
56
57UPDATE "source_auth_sessions"
58SET "credential_slot" = 'runtime'
59WHERE "credential_slot" IS NULL;--> statement-breakpoint
60
61ALTER TABLE "source_auth_sessions"
62ALTER COLUMN "credential_slot" SET NOT NULL;--> statement-breakpoint
63
64DROP INDEX IF EXISTS "source_auth_sessions_pending_idx";--> statement-breakpoint
65
66ALTER TABLE "source_auth_sessions"
67DROP CONSTRAINT IF EXISTS "source_auth_sessions_credential_slot_check";--> statement-breakpoint
68
69ALTER TABLE "source_auth_sessions"
70ADD CONSTRAINT "source_auth_sessions_credential_slot_check"
71CHECK ("credential_slot" in ('runtime', 'import'));--> statement-breakpoint
72
73CREATE INDEX "source_auth_sessions_pending_idx"
74ON "source_auth_sessions" (
75 "workspace_id",
76 "source_id",
77 "actor_account_id",
78 "credential_slot",
79 "status",
80 "updated_at",
81 "id"
82);--> statement-breakpoint
packages/control-plane/drizzle/20260311170000_thin_source_recipe_operations/migration.sql+0−73
@@ -1,73 +0,0 @@
1UPDATE "source_recipe_operations"
2SET "provider_data_json" = CASE
3 WHEN "provider_kind" = 'openapi' THEN jsonb_strip_nulls(jsonb_build_object(
4 'kind', 'openapi',
5 'toolId', "tool_id",
6 'rawToolId', COALESCE("openapi_raw_tool_id", "tool_id"),
7 'operationId', "openapi_operation_id",
8 'group', CASE
9 WHEN position('.' in "tool_id") > 0 THEN split_part("tool_id", '.', 1)
10 ELSE "tool_id"
11 END,
12 'leaf', CASE
13 WHEN position('.' in "tool_id") > 0 THEN substring("tool_id" from position('.' in "tool_id") + 1)
14 ELSE "tool_id"
15 END,
16 'tags', CASE
17 WHEN "openapi_tags_json" IS NOT NULL THEN "openapi_tags_json"::jsonb
18 ELSE '[]'::jsonb
19 END,
20 'method', COALESCE("openapi_method", 'get'),
21 'path', COALESCE("openapi_path_template", '/'),
22 'operationHash', COALESCE(
23 "openapi_operation_hash",
24 md5(COALESCE("tool_id", '') || ':' || COALESCE("openapi_path_template", '/'))
25 ),
26 'invocation', jsonb_build_object(
27 'method', COALESCE("openapi_method", 'get'),
28 'pathTemplate', COALESCE("openapi_path_template", '/'),
29 'parameters', '[]'::jsonb,
30 'requestBody', CASE
31 WHEN "openapi_request_body_required" IS NULL THEN null
32 ELSE jsonb_build_object(
33 'required', "openapi_request_body_required",
34 'contentTypes', '[]'::jsonb
35 )
36 END
37 )
38 ))::text
39 WHEN "provider_kind" = 'graphql' THEN jsonb_build_object(
40 'kind', 'graphql',
41 'toolKind', 'field',
42 'toolId', "tool_id",
43 'rawToolId', null,
44 'group', null,
45 'leaf', null,
46 'fieldName', null,
47 'operationType', "graphql_operation_type",
48 'operationName', "graphql_operation_name",
49 'operationDocument', null,
50 'queryTypeName', null,
51 'mutationTypeName', null,
52 'subscriptionTypeName', null
53 )::text
54 WHEN "provider_kind" = 'mcp' THEN jsonb_build_object(
55 'kind', 'mcp',
56 'toolId', "tool_id",
57 'toolName', COALESCE("mcp_tool_name", "title", "tool_id"),
58 'description', "description"
59 )::text
60 ELSE "provider_data_json"
61END
62WHERE "provider_data_json" IS NULL;--> statement-breakpoint
63
64ALTER TABLE "source_recipe_operations" DROP COLUMN "mcp_tool_name";--> statement-breakpoint
65ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_method";--> statement-breakpoint
66ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_path_template";--> statement-breakpoint
67ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_operation_hash";--> statement-breakpoint
68ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_raw_tool_id";--> statement-breakpoint
69ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_operation_id";--> statement-breakpoint
70ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_tags_json";--> statement-breakpoint
71ALTER TABLE "source_recipe_operations" DROP COLUMN "openapi_request_body_required";--> statement-breakpoint
72ALTER TABLE "source_recipe_operations" DROP COLUMN "graphql_operation_type";--> statement-breakpoint
73ALTER TABLE "source_recipe_operations" DROP COLUMN "graphql_operation_name";--> statement-breakpoint
packages/control-plane/drizzle/20260311223000_drop_tool_artifacts/migration.sql+0−7
@@ -1,7 +0,0 @@
1DROP TABLE IF EXISTS "tool_artifact_parameters";
2--> statement-breakpoint
3DROP TABLE IF EXISTS "tool_artifact_request_body_content_types";
4--> statement-breakpoint
5DROP TABLE IF EXISTS "tool_artifact_ref_hint_keys";
6--> statement-breakpoint
7DROP TABLE IF EXISTS "tool_artifacts";
packages/control-plane/drizzle/20260312013000_adapter_keys_and_versioned_binding_configs/migration.sql+0−55
@@ -1,55 +0,0 @@
1ALTER TABLE "source_recipes"
2RENAME COLUMN "importer_kind" TO "adapter_key";--> statement-breakpoint
3
4ALTER TABLE "control_plane_data_migrations"
5RENAME TO "control_plane_code_migrations";--> statement-breakpoint
6
7UPDATE "source_recipes"
8SET "adapter_key" = CASE
9 WHEN "adapter_key" = 'graphql_introspection' THEN 'graphql'
10 WHEN "adapter_key" = 'mcp_manifest' THEN 'mcp'
11 ELSE "adapter_key"
12END
13WHERE "adapter_key" IN ('graphql_introspection', 'mcp_manifest');--> statement-breakpoint
14
15UPDATE "sources"
16SET "binding_config_json" = jsonb_build_object(
17 'adapterKey', "kind",
18 'version', 1,
19 'payload',
20 CASE
21 WHEN "kind" = 'openapi' THEN jsonb_build_object(
22 'specUrl', COALESCE(("binding_config_json"::jsonb ->> 'specUrl'), "endpoint"),
23 'defaultHeaders', COALESCE(("binding_config_json"::jsonb -> 'defaultHeaders'), 'null'::jsonb)
24 )
25 WHEN "kind" = 'graphql' THEN jsonb_build_object(
26 'defaultHeaders', COALESCE(("binding_config_json"::jsonb -> 'defaultHeaders'), 'null'::jsonb)
27 )
28 WHEN "kind" = 'mcp' THEN jsonb_build_object(
29 'transport', COALESCE(("binding_config_json"::jsonb -> 'transport'), 'null'::jsonb),
30 'queryParams', COALESCE(("binding_config_json"::jsonb -> 'queryParams'), 'null'::jsonb),
31 'headers', COALESCE(("binding_config_json"::jsonb -> 'headers'), 'null'::jsonb)
32 )
33 ELSE '{}'::jsonb
34 END
35)::text
36WHERE "binding_config_json" IS NOT NULL
37 AND (
38 ("binding_config_json"::jsonb -> 'version') IS NULL
39 OR ("binding_config_json"::jsonb -> 'payload') IS NULL
40 );--> statement-breakpoint
41
42UPDATE "sources"
43SET "binding_config_json" = jsonb_set(
44 "binding_config_json"::jsonb,
45 '{adapterKey}',
46 to_jsonb(
47 CASE
48 WHEN "binding_config_json"::jsonb ->> 'adapterKey' = 'graphql_introspection' THEN 'graphql'
49 WHEN "binding_config_json"::jsonb ->> 'adapterKey' = 'mcp_manifest' THEN 'mcp'
50 ELSE "binding_config_json"::jsonb ->> 'adapterKey'
51 END
52 )
53)::text
54WHERE "binding_config_json" IS NOT NULL
55 AND ("binding_config_json"::jsonb ->> 'adapterKey') IN ('graphql_introspection', 'mcp_manifest');
packages/control-plane/drizzle/20260312113000_auth_artifacts_and_leases/migration.sql+0−132
@@ -1,132 +0,0 @@
1ALTER TABLE "workspace_source_credentials"
2RENAME TO "workspace_source_auth_artifacts";--> statement-breakpoint
3
4ALTER TABLE "workspace_source_auth_artifacts"
5ADD COLUMN "artifact_kind" text;--> statement-breakpoint
6
7ALTER TABLE "workspace_source_auth_artifacts"
8ADD COLUMN "config_json" text;--> statement-breakpoint
9
10ALTER TABLE "workspace_source_auth_artifacts"
11ADD COLUMN "grant_set_json" text;--> statement-breakpoint
12
13UPDATE "workspace_source_auth_artifacts"
14SET
15 "artifact_kind" = CASE
16 WHEN "auth_kind" = 'bearer' THEN 'static_bearer'
17 WHEN "auth_kind" = 'oauth2' THEN 'static_oauth2'
18 ELSE 'static_placements'
19 END,
20 "config_json" = CASE
21 WHEN "auth_kind" = 'bearer' THEN jsonb_build_object(
22 'headerName', "auth_header_name",
23 'prefix', "auth_prefix",
24 'token', jsonb_build_object(
25 'providerId', "token_provider_id",
26 'handle', "token_handle"
27 )
28 )::text
29 WHEN "auth_kind" = 'oauth2' THEN jsonb_build_object(
30 'headerName', "auth_header_name",
31 'prefix', "auth_prefix",
32 'accessToken', jsonb_build_object(
33 'providerId', "token_provider_id",
34 'handle', "token_handle"
35 ),
36 'refreshToken',
37 CASE
38 WHEN "refresh_token_provider_id" IS NOT NULL AND "refresh_token_handle" IS NOT NULL
39 THEN jsonb_build_object(
40 'providerId', "refresh_token_provider_id",
41 'handle', "refresh_token_handle"
42 )
43 ELSE 'null'::jsonb
44 END
45 )::text
46 ELSE jsonb_build_object('placements', '[]'::jsonb)::text
47 END
48WHERE "artifact_kind" IS NULL OR "config_json" IS NULL;--> statement-breakpoint
49
50ALTER TABLE "workspace_source_auth_artifacts"
51ALTER COLUMN "artifact_kind" SET NOT NULL;--> statement-breakpoint
52
53ALTER TABLE "workspace_source_auth_artifacts"
54ALTER COLUMN "config_json" SET NOT NULL;--> statement-breakpoint
55
56DROP INDEX IF EXISTS "credentials_workspace_idx";--> statement-breakpoint
57DROP INDEX IF EXISTS "credentials_workspace_source_actor_idx";--> statement-breakpoint
58DROP INDEX IF EXISTS "credentials_workspace_source_idx";--> statement-breakpoint
59
60ALTER TABLE "workspace_source_auth_artifacts"
61DROP CONSTRAINT IF EXISTS "credentials_auth_kind_check";--> statement-breakpoint
62
63ALTER TABLE "workspace_source_auth_artifacts"
64DROP CONSTRAINT IF EXISTS "credentials_slot_check";--> statement-breakpoint
65
66ALTER TABLE "workspace_source_auth_artifacts"
67DROP COLUMN "auth_kind";--> statement-breakpoint
68
69ALTER TABLE "workspace_source_auth_artifacts"
70DROP COLUMN "auth_header_name";--> statement-breakpoint
71
72ALTER TABLE "workspace_source_auth_artifacts"
73DROP COLUMN "auth_prefix";--> statement-breakpoint
74
75ALTER TABLE "workspace_source_auth_artifacts"
76DROP COLUMN "token_provider_id";--> statement-breakpoint
77
78ALTER TABLE "workspace_source_auth_artifacts"
79DROP COLUMN "token_handle";--> statement-breakpoint
80
81ALTER TABLE "workspace_source_auth_artifacts"
82DROP COLUMN "refresh_token_provider_id";--> statement-breakpoint
83
84ALTER TABLE "workspace_source_auth_artifacts"
85DROP COLUMN "refresh_token_handle";--> statement-breakpoint
86
87ALTER TABLE "workspace_source_auth_artifacts"
88ADD CONSTRAINT "auth_artifacts_slot_check"
89CHECK ("slot" in ('runtime', 'import'));--> statement-breakpoint
90
91CREATE INDEX "auth_artifacts_workspace_idx"
92ON "workspace_source_auth_artifacts" ("workspace_id","updated_at","id");--> statement-breakpoint
93
94CREATE UNIQUE INDEX "auth_artifacts_workspace_source_actor_idx"
95ON "workspace_source_auth_artifacts" ("workspace_id","source_id","actor_account_id","slot");--> statement-breakpoint
96
97CREATE INDEX "auth_artifacts_workspace_source_idx"
98ON "workspace_source_auth_artifacts" ("workspace_id","source_id","updated_at","id");--> statement-breakpoint
99
100CREATE TABLE "workspace_source_auth_leases" (
101 "id" text PRIMARY KEY NOT NULL,
102 "auth_artifact_id" text NOT NULL,
103 "workspace_id" text NOT NULL,
104 "source_id" text NOT NULL,
105 "actor_account_id" text,
106 "slot" text NOT NULL,
107 "placements_json" text NOT NULL,
108 "expires_at" bigint,
109 "refresh_after" bigint,
110 "created_at" bigint NOT NULL,
111 "updated_at" bigint NOT NULL
112);--> statement-breakpoint
113
114ALTER TABLE "workspace_source_auth_leases"
115ADD CONSTRAINT "auth_leases_slot_check"
116CHECK ("slot" in ('runtime', 'import'));--> statement-breakpoint
117
118CREATE UNIQUE INDEX "auth_leases_auth_artifact_idx"
119ON "workspace_source_auth_leases" ("auth_artifact_id");--> statement-breakpoint
120
121CREATE INDEX "auth_leases_workspace_source_idx"
122ON "workspace_source_auth_leases" (
123 "workspace_id",
124 "source_id",
125 "actor_account_id",
126 "slot",
127 "updated_at",
128 "id"
129);--> statement-breakpoint
130
131ALTER TABLE "source_auth_sessions"
132DROP CONSTRAINT IF EXISTS "source_auth_sessions_provider_kind_check";--> statement-breakpoint
packages/control-plane/drizzle/20260312143000_auth_lease_template_placements/migration.sql+0−40
@@ -1,40 +0,0 @@
1ALTER TABLE "workspace_source_auth_leases"
2ADD COLUMN "placements_template_json" text;--> statement-breakpoint
3
4UPDATE "workspace_source_auth_leases"
5SET "placements_template_json" = COALESCE(
6 (
7 SELECT jsonb_agg(
8 CASE
9 WHEN placement ->> 'location' = 'body' THEN jsonb_build_object(
10 'location', 'body',
11 'path', placement ->> 'path',
12 'parts', jsonb_build_array(
13 jsonb_build_object(
14 'kind', 'literal',
15 'value', COALESCE(placement ->> 'value', '')
16 )
17 )
18 )
19 ELSE jsonb_build_object(
20 'location', placement ->> 'location',
21 'name', placement ->> 'name',
22 'parts', jsonb_build_array(
23 jsonb_build_object(
24 'kind', 'literal',
25 'value', COALESCE(placement ->> 'value', '')
26 )
27 )
28 )
29 END
30 )::text
31 FROM jsonb_array_elements(COALESCE("placements_json"::jsonb, '[]'::jsonb)) AS placement
32 ),
33 '[]'
34);--> statement-breakpoint
35
36ALTER TABLE "workspace_source_auth_leases"
37ALTER COLUMN "placements_template_json" SET NOT NULL;--> statement-breakpoint
38
39ALTER TABLE "workspace_source_auth_leases"
40DROP COLUMN "placements_json";--> statement-breakpoint
packages/control-plane/drizzle/20260312233000_execution_steps_and_private_interaction_responses/migration.sql+0−25
@@ -1,25 +0,0 @@
1ALTER TABLE "execution_interactions"
2ADD COLUMN "response_private_json" text;--> statement-breakpoint
3
4CREATE TABLE "execution_steps" (
5 "id" text PRIMARY KEY NOT NULL,
6 "execution_id" text NOT NULL,
7 "sequence" bigint NOT NULL,
8 "kind" text NOT NULL,
9 "status" text NOT NULL,
10 "path" text NOT NULL,
11 "args_json" text NOT NULL,
12 "result_json" text,
13 "error_text" text,
14 "interaction_id" text,
15 "created_at" bigint NOT NULL,
16 "updated_at" bigint NOT NULL,
17 CONSTRAINT "execution_steps_kind_check" CHECK ("execution_steps"."kind" in ('tool_call')),
18 CONSTRAINT "execution_steps_status_check" CHECK ("execution_steps"."status" in ('pending', 'waiting', 'completed', 'failed'))
19);--> statement-breakpoint
20
21CREATE UNIQUE INDEX "execution_steps_execution_sequence_idx"
22ON "execution_steps" ("execution_id", "sequence");--> statement-breakpoint
23
24CREATE INDEX "execution_steps_execution_updated_idx"
25ON "execution_steps" ("execution_id", "updated_at", "id");
packages/control-plane/src/api/local/http.ts+5−5
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
12import {12import {
13 ENV_SECRET_PROVIDER_ID,13 ENV_SECRET_PROVIDER_ID,
14 KEYCHAIN_SECRET_PROVIDER_ID,14 KEYCHAIN_SECRET_PROVIDER_ID,
15 POSTGRES_SECRET_PROVIDER_ID,15 LOCAL_SECRET_PROVIDER_ID,
16 parseSecretStoreProviderId,16 parseSecretStoreProviderId,
17} from "../../runtime/secret-material-providers";17} from "../../runtime/secret-material-providers";
18import { ControlPlaneStore } from "../../runtime/store";18import { ControlPlaneStore } from "../../runtime/store";
@@ -35,8 +35,8 @@ const SECRET_STORE_PROVIDER_ENV = "EXECUTOR_SECRET_STORE_PROVIDER"; · 16 unmodified lines
35const getInstanceConfig = (): Effect.Effect<InstanceConfig> => {35const getInstanceConfig = (): Effect.Effect<InstanceConfig> => {
36 const providers: SecretProvider[] = [36 const providers: SecretProvider[] = [
37 {37 {
38 id: POSTGRES_SECRET_PROVIDER_ID,38 id: LOCAL_SECRET_PROVIDER_ID,
39 name: "Database",39 name: "Local store",
40 canStore: true,40 canStore: true,
41 },41 },
42 ];42 ];
@@ -57,7 +57,7 @@ const getInstanceConfig = (): Effect.Effect<InstanceConfig> => { · 14 unmodified lines
57 57
58 const defaultStoreProvider =58 const defaultStoreProvider =
59 parseSecretStoreProviderId(process.env[SECRET_STORE_PROVIDER_ENV])59 parseSecretStoreProviderId(process.env[SECRET_STORE_PROVIDER_ENV])
60 ?? POSTGRES_SECRET_PROVIDER_ID;60 ?? LOCAL_SECRET_PROVIDER_ID;
61 61
62 return Effect.succeed({62 return Effect.succeed({
63 platform: process.platform,63 platform: process.platform,
@@ -133,7 +133,7 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 69 unmodified lines
133 return {133 return {
134 id,134 id,
135 name,135 name,
136 providerId: POSTGRES_SECRET_PROVIDER_ID,136 providerId: LOCAL_SECRET_PROVIDER_ID,
137 purpose,137 purpose,
138 createdAt: now,138 createdAt: now,
139 updatedAt: now,139 updatedAt: now,
packages/control-plane/src/index.ts+0−1
@@ -1,5 +1,4 @@
1export * from "./schema/index";1export * from "./schema/index";
2export * from "./persistence/index";
3export * from "./api/index";2export * from "./api/index";
4export * from "./runtime/index";3export * from "./runtime/index";
5export * from "./client";4export * from "./client";
packages/control-plane/src/persistence/client.ts+0−70
@@ -1,70 +0,0 @@
1import * as Effect from "effect/Effect";
2
3import {
4 type ControlPlanePersistenceError,
5 toPersistenceError,
6} from "./persistence-errors";
7import type { DrizzleDb, SqlBackend } from "./sql-runtime";
8
9export type DrizzleSession = Pick<DrizzleDb, "select" | "insert" | "update" | "delete">;
10
11export type DrizzleClient = {
12 backend: SqlBackend;
13 db: DrizzleDb;
14 use: <A>(
15 operation: string,
16 run: (db: DrizzleSession) => Promise<A>,
17 ) => Effect.Effect<A, ControlPlanePersistenceError>;
18 useTx: <A>(
19 operation: string,
20 run: (tx: DrizzleSession) => Promise<A>,
21 ) => Effect.Effect<A, ControlPlanePersistenceError>;
22};
23
24const createSerializer = (backend: SqlBackend) => {
25 let queue = Promise.resolve<void>(undefined);
26
27 return <A>(run: () => Promise<A>): Promise<A> => {
28 if (backend !== "pglite") {
29 return run();
30 }
31
32 const next: Promise<A> = queue.then(run, run);
33 queue = next.then(
34 () => undefined,
35 () => undefined,
36 );
37 return next;
38 };
39};
40
41export const createDrizzleClient = (input: {
42 backend: SqlBackend;
43 db: DrizzleDb;
44}): DrizzleClient => {
45 const serialize = createSerializer(input.backend);
46
47 const use = <A>(
48 operation: string,
49 run: (db: DrizzleSession) => Promise<A>,
50 ): Effect.Effect<A, ControlPlanePersistenceError> =>
51 Effect.tryPromise({
52 try: () => serialize(() => run(input.db)),
53 catch: (cause) => toPersistenceError(operation, cause),
54 });
55
56 const useTx = <A>(
57 operation: string,
58 run: (tx: DrizzleSession) => Promise<A>,
59 ): Effect.Effect<A, ControlPlanePersistenceError> =>
60 use(operation, () =>
61 input.db.transaction(async (tx) => run(tx)),
62 );
63
64 return {
65 backend: input.backend,
66 db: input.db,
67 use,
68 useTx,
69 };
70};
packages/control-plane/src/persistence/code-migrations.ts+0−234
@@ -1,234 +0,0 @@
1import {
2 type McpSourceAuthSessionData,
3 type SourceAuthSession,
4 type StoredSourceRecipeDocumentRecord,
5 type StoredSourceRecord,
6} from "#schema";
7import {
8 JsonObjectSchema,
9 McpSourceAuthSessionDataJsonSchema,
10} from "#schema";
11import * as Either from "effect/Either";
12import * as Effect from "effect/Effect";
13import * as ParseResult from "effect/ParseResult";
14import * as Schema from "effect/Schema";
15
16import { rebuildGraphqlRecipeRevisionContent } from "../runtime/source-adapters/graphql";
17import { rebuildOpenApiRecipeRevisionContent } from "../runtime/source-adapters/openapi";
18import type { SqlControlPlaneRows } from "./index";
19
20type DataMigration = {
21 id: string;
22 run: (rows: SqlControlPlaneRows) => Effect.Effect<void, Error, never>;
23};
24
25const LegacyMcpSourceAuthSessionDataSchema = Schema.Struct({
26 kind: Schema.Literal("mcp_oauth"),
27 endpoint: Schema.String,
28 redirectUri: Schema.String,
29 scope: Schema.NullOr(Schema.String),
30 resourceMetadataUrl: Schema.NullOr(Schema.String),
31 authorizationServerUrl: Schema.NullOr(Schema.String),
32 resourceMetadataJson: Schema.NullOr(Schema.String),
33 authorizationServerMetadataJson: Schema.NullOr(Schema.String),
34 clientInformationJson: Schema.NullOr(Schema.String),
35 codeVerifier: Schema.NullOr(Schema.String),
36 authorizationUrl: Schema.NullOr(Schema.String),
37});
38
39const decodeLegacyMcpSourceAuthSessionDataJson = Schema.decodeUnknownEither(
40 Schema.parseJson(LegacyMcpSourceAuthSessionDataSchema),
41);
42
43const decodeMcpSourceAuthSessionDataJson = Schema.decodeUnknownEither(
44 McpSourceAuthSessionDataJsonSchema,
45);
46
47const encodeMcpSourceAuthSessionDataJson = Schema.encodeSync(
48 McpSourceAuthSessionDataJsonSchema,
49);
50
51const decodeJsonObjectFromJson = Schema.decodeUnknownEither(
52 Schema.parseJson(JsonObjectSchema),
53);
54
55const decodeLegacyJsonObject = (value: string | null) => {
56 if (value === null) {
57 return null;
58 }
59
60 const decoded = decodeJsonObjectFromJson(value);
61 return Either.isRight(decoded) ? decoded.right : null;
62};
63
64const repairLegacyMcpSourceAuthSession = (input: {
65 rows: SqlControlPlaneRows;
66 session: SourceAuthSession;
67}): Effect.Effect<void, never, never> => {
68 if (input.session.providerKind !== "mcp_oauth") {
69 return Effect.void;
70 }
71
72 if (
73 Either.isRight(
74 decodeMcpSourceAuthSessionDataJson(input.session.sessionDataJson),
75 )
76 ) {
77 return Effect.void;
78 }
79
80 const legacy = decodeLegacyMcpSourceAuthSessionDataJson(
81 input.session.sessionDataJson,
82 );
83 if (Either.isLeft(legacy)) {
84 console.warn(
85 `Skipping source auth session data migration for ${input.session.id}: ${ParseResult.TreeFormatter.formatErrorSync(legacy.left)}`,
86 );
87 return Effect.void;
88 }
89
90 const repaired: McpSourceAuthSessionData = {
91 kind: "mcp_oauth",
92 endpoint: legacy.right.endpoint,
93 redirectUri: legacy.right.redirectUri,
94 scope: legacy.right.scope,
95 resourceMetadataUrl: legacy.right.resourceMetadataUrl,
96 authorizationServerUrl: legacy.right.authorizationServerUrl,
97 resourceMetadata: decodeLegacyJsonObject(legacy.right.resourceMetadataJson),
98 authorizationServerMetadata: decodeLegacyJsonObject(
99 legacy.right.authorizationServerMetadataJson,
100 ),
101 clientInformation: decodeLegacyJsonObject(
102 legacy.right.clientInformationJson,
103 ),
104 codeVerifier: legacy.right.codeVerifier,
105 authorizationUrl: legacy.right.authorizationUrl,
106 };
107
108 return input.rows.sourceAuthSessions
109 .update(input.session.id, {
110 sessionDataJson: encodeMcpSourceAuthSessionDataJson(repaired),
111 })
112 .pipe(Effect.asVoid, Effect.orDie);
113};
114
115const migrateLegacySourceAuthSessions: DataMigration = {
116 id: "20260310_repair_legacy_mcp_oauth_sessions",
117 run: (rows) =>
118 Effect.gen(function* () {
119 const sourceAuthSessions = yield* rows.sourceAuthSessions.listAll();
120 yield* Effect.forEach(
121 sourceAuthSessions,
122 (session) =>
123 repairLegacyMcpSourceAuthSession({
124 rows,
125 session,
126 }),
127 { discard: true },
128 );
129 }),
130};
131
132const rebuildPersistedSourceRecipes: DataMigration = {
133 id: "20260310_rebuild_persisted_source_recipes",
134 run: (rows) =>
135 Effect.gen(function* () {
136 const sourceRecords = yield* rows.sources.listAll();
137 if (sourceRecords.length === 0) {
138 return;
139 }
140
141 const revisionIds = [
142 ...new Set(sourceRecords.map((sourceRecord) => sourceRecord.recipeRevisionId)),
143 ];
144 const [revisions, documents] = yield* Effect.all([
145 rows.sourceRecipeRevisions.listByIds(revisionIds),
146 rows.sourceRecipeDocuments.listByRevisionIds(revisionIds),
147 ]);
148
149 const revisionById = new Map(
150 revisions.map((revision) => [revision.id, revision]),
151 );
152 const sourceRecordByRevisionId = new Map<string, StoredSourceRecord>();
153 for (const sourceRecord of sourceRecords) {
154 if (!sourceRecordByRevisionId.has(sourceRecord.recipeRevisionId)) {
155 sourceRecordByRevisionId.set(sourceRecord.recipeRevisionId, sourceRecord);
156 }
157 }
158
159 const documentsByRevisionId = new Map<string, StoredSourceRecipeDocumentRecord[]>();
160 for (const document of documents) {
161 const existing = documentsByRevisionId.get(document.recipeRevisionId) ?? [];
162 existing.push(document);
163 documentsByRevisionId.set(document.recipeRevisionId, existing);
164 }
165
166 yield* Effect.forEach(
167 revisionIds,
168 (revisionId) =>
169 Effect.gen(function* () {
170 const sourceRecord = sourceRecordByRevisionId.get(revisionId);
171 const revision = revisionById.get(revisionId);
172 const documentsForRevision = documentsByRevisionId.get(revisionId) ?? [];
173 if (!sourceRecord || !revision) {
174 return;
175 }
176
177 if (sourceRecord.kind === "openapi") {
178 if (documentsForRevision.length === 0) {
179 return;
180 }
181 yield* rebuildOpenApiRecipeRevisionContent({
182 rows,
183 sourceRecord,
184 revision,
185 documents: documentsForRevision,
186 });
187 return;
188 }
189
190 if (sourceRecord.kind !== "graphql") {
191 return;
192 }
193
194 if (documentsForRevision.length === 0) {
195 return;
196 }
197
198 yield* rebuildGraphqlRecipeRevisionContent({
199 rows,
200 sourceRecord,
201 revision,
202 documents: documentsForRevision,
203 });
204 }),
205 { discard: true },
206 );
207 }),
208};
209
210const codeMigrations = [
211 rebuildPersistedSourceRecipes,
212 migrateLegacySourceAuthSessions,
213] as const satisfies readonly DataMigration[];
214
215export const runCodeMigrations = (
216 rows: SqlControlPlaneRows,
217): Effect.Effect<void, Error, never> =>
218 Effect.gen(function* () {
219 const applied = new Set(
220 (yield* rows.codeMigrations.listAll()).map((migration) => migration.id),
221 );
222
223 for (const migration of codeMigrations) {
224 if (applied.has(migration.id)) {
225 continue;
226 }
227
228 yield* migration.run(rows);
229 yield* rows.codeMigrations.upsert({
230 id: migration.id,
231 appliedAt: Date.now(),
232 });
233 }
234 });
packages/control-plane/src/persistence/index.ts+0−182
@@ -1,182 +0,0 @@
1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5
6import { createDrizzleClient, type DrizzleClient } from "./client";
7import {
8 createAccountsRepo,
9 createAuthArtifactsRepo,
10 createAuthLeasesRepo,
11 createCodeMigrationsRepo,
12 createExecutionInteractionsRepo,
13 createExecutionStepsRepo,
14 createExecutionsRepo,
15 createSecretMaterialsRepo,
16 createSourceAuthSessionsRepo,
17 createSourceOauthClientsRepo,
18 createSourceRecipeDocumentsRepo,
19 createSourceRecipeSchemaBundlesRepo,
20 createSourceRecipeOperationsRepo,
21 createSourceRecipeRevisionsRepo,
22 createSourceRecipesRepo,
23 createSourcesRepo,
24} from "./repos";
25import { drizzleSchema, tableNames, type DrizzleTables } from "./schema";
26import {
27 createSqlRuntime,
28 runMigrations,
29 type CreateSqlRuntimeOptions,
30 type DrizzleDb,
31 type SqlBackend,
32 type SqlRuntime,
33} from "./sql-runtime";
34import { runCodeMigrations } from "./code-migrations";
35
36export { tableNames, type DrizzleTables } from "./schema";
37export {
38 ControlPlanePersistenceError,
39 toPersistenceError,
40 type PersistenceErrorKind,
41} from "./persistence-errors";
42export { createDrizzleClient, type DrizzleClient } from "./client";
43export {
44 createSqlRuntime,
45 runMigrations,
46 type SqlRuntime,
47 type SqlBackend,
48 type DrizzleDb,
49 type CreateSqlRuntimeOptions,
50} from "./sql-runtime";
51
52const createRows = (client: DrizzleClient, tables: DrizzleTables = drizzleSchema) => ({
53 accounts: createAccountsRepo(client, tables),
54 sources: createSourcesRepo(client, tables),
55 sourceRecipes: createSourceRecipesRepo(client, tables),
56 sourceRecipeRevisions: createSourceRecipeRevisionsRepo(client, tables),
57 sourceRecipeDocuments: createSourceRecipeDocumentsRepo(client, tables),
58 sourceRecipeSchemaBundles: createSourceRecipeSchemaBundlesRepo(client, tables),
59 sourceRecipeOperations: createSourceRecipeOperationsRepo(client, tables),
60 codeMigrations: createCodeMigrationsRepo(client, tables),
61 authArtifacts: createAuthArtifactsRepo(client, tables),
62 authLeases: createAuthLeasesRepo(client, tables),
63 sourceOauthClients: createSourceOauthClientsRepo(client, tables),
64 secretMaterials: createSecretMaterialsRepo(client, tables),
65 sourceAuthSessions: createSourceAuthSessionsRepo(client, tables),
66 executions: createExecutionsRepo(client, tables),
67 executionInteractions: createExecutionInteractionsRepo(client, tables),
68 executionSteps: createExecutionStepsRepo(client, tables),
69});
70
71export type SqlControlPlaneRows = ReturnType<typeof createRows>;
72
73export type SqlControlPlanePersistence = {
74 backend: SqlBackend;
75 db: DrizzleDb;
76 rows: SqlControlPlaneRows;
77 close: () => Promise<void>;
78};
79
80export class SqlControlPlanePersistenceService extends Context.Tag(
81 "#persistence/SqlControlPlanePersistenceService",
82)<SqlControlPlanePersistenceService, SqlControlPlanePersistence>() {}
83
84export class SqlControlPlaneRowsService extends Context.Tag(
85 "#persistence/SqlControlPlaneRowsService",
86)<SqlControlPlaneRowsService, SqlControlPlaneRows>() {}
87
88export class SqlPersistenceBootstrapError extends Data.TaggedError(
89 "SqlPersistenceBootstrapError",
90)<{
91 message: string;
92 details: string | null;
93}> {}
94
95const toBootstrapError = (cause: unknown): SqlPersistenceBootstrapError => {
96 const details = cause instanceof Error ? cause.message : String(cause);
97 return new SqlPersistenceBootstrapError({
98 message: `Failed initializing SQL control-plane persistence: ${details}`,
99 details,
100 });
101};
102
103const createRuntimeEffect = (options: CreateSqlRuntimeOptions) =>
104 Effect.tryPromise({
105 try: () => createSqlRuntime(options),
106 catch: toBootstrapError,
107 });
108
109const runMigrationsEffect = (
110 runtime: SqlRuntime,
111 migrationsFolder: string | undefined,
112) =>
113 Effect.tryPromise({
114 try: () => runMigrations(runtime, { migrationsFolder }),
115 catch: toBootstrapError,
116 });
117
118const closeRuntimeEffect = (runtime: SqlRuntime) =>
119 Effect.tryPromise({
120 try: () => runtime.close(),
121 catch: (cause) =>
122 cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
123 }).pipe(Effect.orDie);
124
125export type CreateSqlControlPlanePersistenceOptions =
126 & CreateSqlRuntimeOptions
127 & {
128 runCodeMigrations?: boolean;
129 };
130
131export const createSqlControlPlanePersistence = (
132 options: CreateSqlControlPlanePersistenceOptions,
133): Effect.Effect<SqlControlPlanePersistence, SqlPersistenceBootstrapError> =>
134 Effect.flatMap(createRuntimeEffect(options), (runtime) =>
135 runMigrationsEffect(runtime, options.migrationsFolder).pipe(
136 Effect.map(() => {
137 const client = createDrizzleClient({
138 backend: runtime.backend,
139 db: runtime.db,
140 });
141 const rows = createRows(client);
142
143 return {
144 backend: runtime.backend,
145 db: runtime.db,
146 rows,
147 close: () => runtime.close(),
148 } satisfies SqlControlPlanePersistence;
149 }),
150 Effect.flatMap((persistence) =>
151 options.runCodeMigrations === false
152 ? Effect.succeed(persistence)
153 : runCodeMigrations(persistence.rows).pipe(
154 Effect.mapError(toBootstrapError),
155 Effect.map(() => persistence),
156 )),
157 Effect.catchAll((error) =>
158 closeRuntimeEffect(runtime).pipe(
159 Effect.zipRight(Effect.fail(error)),
160 )),
161 ));
162
163export const SqlControlPlanePersistenceLive = (
164 options: CreateSqlControlPlanePersistenceOptions,
165) =>
166 Layer.scoped(
167 SqlControlPlanePersistenceService,
168 Effect.acquireRelease(
169 createSqlControlPlanePersistence(options),
170 (persistence) =>
171 Effect.tryPromise({
172 try: () => persistence.close(),
173 catch: (cause) =>
174 cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")),
175 }).pipe(Effect.orDie),
176 ),
177 );
178
179export const SqlControlPlaneRowsLive = Layer.effect(
180 SqlControlPlaneRowsService,
181 Effect.map(SqlControlPlanePersistenceService, (persistence) => persistence.rows),
182);
packages/control-plane/src/persistence/persistence-errors.ts+0−92
@@ -1,92 +0,0 @@
1import * as Data from "effect/Data";
2
3export type PersistenceErrorKind =
4 | "unique_violation"
5 | "not_null_violation"
6 | "check_violation"
7 | "foreign_key_violation"
8 | "serialization_failure"
9 | "unknown";
10
11type ErrorMetadata = {
12 code: string | null;
13 constraint: string | null;
14 table: string | null;
15 details: string | null;
16 message: string;
17 kind: PersistenceErrorKind;
18};
19
20export class ControlPlanePersistenceError extends Data.TaggedError(
21 "ControlPlanePersistenceError",
22)<{
23 operation: string;
24 message: string;
25 details: string | null;
26 code: string | null;
27 constraint: string | null;
28 table: string | null;
29 kind: PersistenceErrorKind;
30 cause: unknown;
31}> {}
32
33const asRecord = (value: unknown): Record<string, unknown> | null =>
34 typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
35
36const readString = (value: unknown): string | null =>
37 typeof value === "string" && value.length > 0 ? value : null;
38
39const classifyKind = (code: string | null): PersistenceErrorKind => {
40 switch (code) {
41 case "23505":
42 return "unique_violation";
43 case "23502":
44 return "not_null_violation";
45 case "23514":
46 return "check_violation";
47 case "23503":
48 return "foreign_key_violation";
49 case "40001":
50 return "serialization_failure";
51 default:
52 return "unknown";
53 }
54};
55
56const toErrorMetadata = (cause: unknown): ErrorMetadata => {
57 const record = asRecord(cause);
58 const code = readString(record?.code);
59 const constraint =
60 readString(record?.constraint)
61 ?? readString(record?.constraint_name)
62 ?? readString(record?.constraintName);
63 const table = readString(record?.table) ?? readString(record?.table_name);
64 const details = readString(record?.detail) ?? readString(record?.details);
65 const message = cause instanceof Error ? cause.message : String(cause);
66
67 return {
68 code,
69 constraint,
70 table,
71 details,
72 message,
73 kind: classifyKind(code),
74 };
75};
76
77export const toPersistenceError = (
78 operation: string,
79 cause: unknown,
80): ControlPlanePersistenceError => {
81 const metadata = toErrorMetadata(cause);
82 return new ControlPlanePersistenceError({
83 operation,
84 message: `Control-plane persistence failed during ${operation}`,
85 details: metadata.details ?? metadata.message,
86 code: metadata.code,
87 constraint: metadata.constraint,
88 table: metadata.table,
89 kind: metadata.kind,
90 cause,
91 });
92};
packages/control-plane/src/persistence/post-migrations.test.ts+0−915
@@ -1,915 +0,0 @@
1import { cpSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2import { tmpdir } from "node:os";
3import path from "node:path";
4import { fileURLToPath } from "node:url";
5
6import { describe, expect, it } from "@effect/vitest";
7import {
8 AccountIdSchema,
9 McpSourceAuthSessionDataJsonSchema,
10 SourceAuthSessionIdSchema,
11 decodeAuthLeasePlacementTemplates,
12 SourceIdSchema,
13 SourceRecipeIdSchema,
14 SourceRecipeRevisionIdSchema,
15 WorkspaceIdSchema,
16} from "#schema";
17import * as Effect from "effect/Effect";
18import * as Option from "effect/Option";
19import * as Schema from "effect/Schema";
20import * as Scope from "effect/Scope";
21import { buildSchema, getIntrospectionQuery, graphqlSync } from "graphql";
22import { sql } from "drizzle-orm";
23
24import {
25 createSqlControlPlanePersistence,
26 type SqlControlPlanePersistence,
27} from "./index";
28import { runCodeMigrations } from "./code-migrations";
29
30const openApiBindingConfigJson = (specUrl: string): string =>
31 JSON.stringify({
32 adapterKey: "openapi",
33 version: 1,
34 payload: {
35 specUrl,
36 defaultHeaders: null,
37 },
38 });
39
40const graphqlBindingConfigJson = (): string =>
41 JSON.stringify({
42 adapterKey: "graphql",
43 version: 1,
44 payload: {
45 defaultHeaders: null,
46 },
47 });
48
49const baseRevisionRecord = (input: {
50 id: ReturnType<typeof SourceRecipeRevisionIdSchema.make>;
51 recipeId: ReturnType<typeof SourceRecipeIdSchema.make>;
52 revisionNumber: number;
53 sourceConfigJson: string;
54 manifestJson?: string | null;
55 manifestHash?: string | null;
56 materializationHash?: string | null;
57 createdAt: number;
58 updatedAt: number;
59}) => ({
60 id: input.id,
61 recipeId: input.recipeId,
62 revisionNumber: input.revisionNumber,
63 sourceConfigJson: input.sourceConfigJson,
64 manifestJson: input.manifestJson ?? null,
65 manifestHash: input.manifestHash ?? null,
66 materializationHash: input.materializationHash ?? null,
67 createdAt: input.createdAt,
68 updatedAt: input.updatedAt,
69});
70
71const drizzleDir = path.resolve(
72 path.dirname(fileURLToPath(import.meta.url)),
73 "../../drizzle",
74);
75
76const copyMigrationsBefore = (cutoffDirectoryName: string): string => {
77 const targetDir = mkdtempSync(path.join(tmpdir(), "executor-cp-migrations-"));
78 for (const directoryName of readdirSync(drizzleDir).sort()) {
79 if (directoryName >= cutoffDirectoryName) {
80 continue;
81 }
82
83 cpSync(
84 path.join(drizzleDir, directoryName),
85 path.join(targetDir, directoryName),
86 { recursive: true },
87 );
88 }
89
90 return targetDir;
91};
92
93const makePersistence: Effect.Effect<
94 SqlControlPlanePersistence,
95 unknown,
96 Scope.Scope
97> = Effect.acquireRelease(
98 createSqlControlPlanePersistence({
99 localDataDir: ":memory:",
100 }),
101 (persistence) =>
102 Effect.tryPromise({
103 try: () => persistence.close(),
104 catch: (cause) =>
105 cause instanceof Error ? cause : new Error(String(cause)),
106 }).pipe(Effect.orDie),
107);
108
109const seedMigratedSourceRecipe = (input: {
110 persistence: SqlControlPlanePersistence;
111 kind: "openapi" | "graphql";
112 workspaceId: ReturnType<typeof WorkspaceIdSchema.make>;
113 sourceId: ReturnType<typeof SourceIdSchema.make>;
114 documentText: string;
115}): Effect.Effect<
116 {
117 recipeRevisionId: ReturnType<typeof SourceRecipeRevisionIdSchema.make>;
118 },
119 unknown,
120 never
121> =>
122 Effect.gen(function* () {
123 const now = Date.now();
124 const accountId = AccountIdSchema.make(`acc_${input.sourceId}`);
125 const recipeId = SourceRecipeIdSchema.make(`src_recipe_${input.sourceId}`);
126 const recipeRevisionId = SourceRecipeRevisionIdSchema.make(
127 `src_recipe_rev_${input.sourceId}`,
128 );
129
130 yield* input.persistence.rows.sourceRecipes.upsert({
131 id: recipeId,
132 kind: "http_api",
133 adapterKey:
134 input.kind === "openapi" ? "openapi" : "graphql",
135 providerKey:
136 input.kind === "openapi" ? "generic_http" : "generic_graphql",
137 name: input.kind === "openapi" ? "GitHub" : "GraphQL Demo",
138 summary: null,
139 visibility: "workspace",
140 latestRevisionId: recipeRevisionId,
141 createdAt: now,
142 updatedAt: now,
143 });
144 yield* input.persistence.rows.sourceRecipeRevisions.upsert(baseRevisionRecord({
145 id: recipeRevisionId,
146 recipeId,
147 revisionNumber: 1,
148 sourceConfigJson: JSON.stringify(
149 input.kind === "openapi"
150 ? {
151 kind: "openapi",
152 endpoint: "https://api.example.com",
153 specUrl: "https://api.example.com/openapi.json",
154 defaultHeaders: null,
155 }
156 : {
157 kind: "graphql",
158 endpoint: "https://api.example.com/graphql",
159 defaultHeaders: null,
160 },
161 ),
162 manifestJson: null,
163 manifestHash: null,
164 materializationHash: null,
165 createdAt: now,
166 updatedAt: now,
167 }));
168 yield* input.persistence.rows.sources.insert({
169 id: input.sourceId,
170 workspaceId: input.workspaceId,
171 recipeId,
172 recipeRevisionId,
173 name: input.kind === "openapi" ? "GitHub" : "GraphQL Demo",
174 kind: input.kind,
175 endpoint:
176 input.kind === "openapi"
177 ? "https://api.example.com"
178 : "https://api.example.com/graphql",
179 status: "connected",
180 enabled: true,
181 namespace: input.kind === "openapi" ? "github" : "graphql",
182 importAuthPolicy: "reuse_runtime",
183 bindingConfigJson:
184 input.kind === "openapi"
185 ? openApiBindingConfigJson("https://api.example.com/openapi.json")
186 : graphqlBindingConfigJson(),
187 sourceHash: null,
188 lastError: null,
189 createdAt: now,
190 updatedAt: now,
191 });
192 yield* input.persistence.rows.sourceRecipeDocuments.replaceForRevision({
193 recipeRevisionId,
194 documents: [
195 {
196 id: `src_recipe_doc_${input.sourceId}`,
197 recipeRevisionId,
198 documentKind:
199 input.kind === "openapi" ? "openapi" : "graphql_introspection",
200 documentKey:
201 input.kind === "openapi"
202 ? "https://api.example.com/openapi.json"
203 : "https://api.example.com/graphql",
204 contentText: input.documentText,
205 contentHash: `hash_${input.sourceId}`,
206 fetchedAt: now,
207 createdAt: now,
208 updatedAt: now,
209 },
210 ],
211 });
212
213 return { recipeRevisionId };
214 });
215
216describe("code-migrations", () => {
217 it.scoped("repairs migrated OpenAPI recipes from stored documents", () =>
218 Effect.gen(function* () {
219 const persistence = yield* makePersistence;
220 const workspaceId = WorkspaceIdSchema.make("ws_post_migration_openapi");
221 const sourceId = SourceIdSchema.make("src_post_migration_openapi");
222 yield* persistence.rows.codeMigrations.clearAll();
223
224 const openApiDocument = JSON.stringify({
225 openapi: "3.0.3",
226 info: {
227 title: "GitHub",
228 version: "1.0.0",
229 },
230 paths: {
231 "/repos/{owner}/{repo}": {
232 get: {
233 operationId: "repos.getRepo",
234 parameters: [
235 {
236 name: "owner",
237 in: "path",
238 required: true,
239 schema: { type: "string" },
240 },
241 {
242 name: "repo",
243 in: "path",
244 required: true,
245 schema: { type: "string" },
246 },
247 ],
248 responses: {
249 200: {
250 description: "ok",
251 content: {
252 "application/json": {
253 schema: {
254 $ref: "#/components/schemas/Repository",
255 },
256 },
257 },
258 },
259 },
260 },
261 },
262 },
263 components: {
264 schemas: {
265 Repository: {
266 type: "object",
267 properties: {
268 full_name: { type: "string" },
269 },
270 required: ["full_name"],
271 },
272 },
273 },
274 });
275
276 const { recipeRevisionId } = yield* seedMigratedSourceRecipe({
277 persistence,
278 kind: "openapi",
279 workspaceId,
280 sourceId,
281 documentText: openApiDocument,
282 });
283
284 yield* runCodeMigrations(persistence.rows);
285
286 const revision =
287 yield* persistence.rows.sourceRecipeRevisions.getById(recipeRevisionId);
288 expect(Option.isSome(revision)).toBe(true);
289 expect(revision.pipe(Option.getOrNull)?.manifestJson).not.toBeNull();
290 expect(
291 (yield* persistence.rows.sourceRecipeSchemaBundles.listByRevisionId(
292 recipeRevisionId,
293 )).length,
294 ).toBe(1);
295 expect(
296 (yield* persistence.rows.sourceRecipeOperations.listByRevisionId(
297 recipeRevisionId,
298 )).length,
299 ).toBeGreaterThan(0);
300 }),
301 60_000,
302 );
303
304 it.scoped("repairs migrated GraphQL recipes from stored documents", () =>
305 Effect.gen(function* () {
306 const persistence = yield* makePersistence;
307 const workspaceId = WorkspaceIdSchema.make("ws_post_migration_graphql");
308 const sourceId = SourceIdSchema.make("src_post_migration_graphql");
309 yield* persistence.rows.codeMigrations.clearAll();
310 const schema = buildSchema(`
311 type Query {
312 viewer: User!
313 }
314
315 type Mutation {
316 createIssue(title: String!): Issue!
317 }
318
319 type User {
320 login: String!
321 }
322
323 type Issue {
324 id: ID!
325 title: String!
326 }
327 `);
328 const graphqlDocument = JSON.stringify(
329 graphqlSync({
330 schema,
331 source: getIntrospectionQuery(),
332 }),
333 );
334
335 const { recipeRevisionId } = yield* seedMigratedSourceRecipe({
336 persistence,
337 kind: "graphql",
338 workspaceId,
339 sourceId,
340 documentText: graphqlDocument,
341 });
342
343 yield* runCodeMigrations(persistence.rows);
344
345 const revision =
346 yield* persistence.rows.sourceRecipeRevisions.getById(recipeRevisionId);
347 expect(Option.isSome(revision)).toBe(true);
348 expect(revision.pipe(Option.getOrNull)?.manifestJson).not.toBeNull();
349 expect(
350 (yield* persistence.rows.sourceRecipeSchemaBundles.listByRevisionId(
351 recipeRevisionId,
352 )).length,
353 ).toBe(1);
354 expect(
355 (yield* persistence.rows.sourceRecipeOperations.listByRevisionId(
356 recipeRevisionId,
357 )).length,
358 ).toBeGreaterThan(0);
359 }),
360 60_000,
361 );
362
363 it.scoped("repairs legacy MCP source auth session payloads", () =>
364 Effect.gen(function* () {
365 const persistence = yield* makePersistence;
366 const workspaceId = WorkspaceIdSchema.make("ws_post_migration_session");
367 const sourceId = SourceIdSchema.make("src_post_migration_session");
368 yield* persistence.rows.codeMigrations.clearAll();
369 const sessionId = SourceAuthSessionIdSchema.make(
370 "src_auth_post_migration_session",
371 );
372 const now = Date.now();
373
374 yield* persistence.rows.sourceAuthSessions.insert({
375 id: sessionId,
376 workspaceId,
377 sourceId,
378 actorAccountId: null,
379 executionId: null,
380 interactionId: null,
381 providerKind: "mcp_oauth",
382 credentialSlot: "runtime",
383 status: "pending",
384 state: "state_post_migration_session",
385 sessionDataJson: JSON.stringify({
386 kind: "mcp_oauth",
387 endpoint: "https://api.github.com",
388 redirectUri: "http://127.0.0.1/callback",
389 scope: null,
390 resourceMetadataUrl:
391 "https://api.github.com/.well-known/oauth-protected-resource",
392 authorizationServerUrl: "https://github.com/login/oauth",
393 resourceMetadataJson: '{"issuer":"https://api.github.com"}',
394 authorizationServerMetadataJson:
395 '{"token_endpoint":"https://github.com/login/oauth/access_token"}',
396 clientInformationJson: '{"client_id":"abc123"}',
397 codeVerifier: "verifier",
398 authorizationUrl: "https://github.com/login/oauth/authorize",
399 }),
400 errorText: null,
401 completedAt: null,
402 createdAt: now,
403 updatedAt: now,
404 });
405
406 yield* runCodeMigrations(persistence.rows);
407
408 const session =
409 yield* persistence.rows.sourceAuthSessions.getById(sessionId);
410 expect(Option.isSome(session)).toBe(true);
411 const repairedSession = session.pipe(Option.getOrNull);
412 expect(repairedSession).not.toBeNull();
413 const repaired = Schema.decodeUnknownSync(
414 McpSourceAuthSessionDataJsonSchema,
415 )(repairedSession!.sessionDataJson);
416 expect(repaired.resourceMetadata).toEqual({
417 issuer: "https://api.github.com",
418 });
419 expect(repaired.authorizationServerMetadata).toEqual({
420 token_endpoint: "https://github.com/login/oauth/access_token",
421 });
422 expect(repaired.clientInformation).toEqual({
423 client_id: "abc123",
424 });
425 }),
426 );
427
428 it("upgrades legacy source binding columns into adapter-owned binding config", async () => {
429 const localDataDir = mkdtempSync(path.join(tmpdir(), "executor-cp-db-"));
430 const legacyMigrationsDir = copyMigrationsBefore(
431 "20260311010000_source_binding_configs",
432 );
433 const workspaceId = WorkspaceIdSchema.make("ws_legacy_source_bindings");
434 const accountId = AccountIdSchema.make("acc_legacy_source_bindings");
435 const now = Date.now();
436 let legacyPersistence: SqlControlPlanePersistence | null = null;
437 let upgradedPersistence: SqlControlPlanePersistence | null = null;
438
439 try {
440 legacyPersistence = await Effect.runPromise(
441 createSqlControlPlanePersistence({
442 localDataDir,
443 migrationsFolder: legacyMigrationsDir,
444 runCodeMigrations: false,
445 }),
446 );
447
448 const seedRecipe = async (input: {
449 sourceId: string;
450 kind: "openapi" | "graphql" | "mcp";
451 adapterKey: string;
452 providerKey: string;
453 name: string;
454 endpoint: string;
455 sourceConfigJson: string;
456 }) => {
457 const recipeId = SourceRecipeIdSchema.make(`src_recipe_${input.sourceId}`);
458 const recipeRevisionId = SourceRecipeRevisionIdSchema.make(
459 `src_recipe_rev_${input.sourceId}`,
460 );
461
462 await legacyPersistence!.db.execute(sql`
463 INSERT INTO "source_recipes" (
464 "id",
465 "kind",
466 "importer_kind",
467 "provider_key",
468 "name",
469 "summary",
470 "visibility",
471 "latest_revision_id",
472 "created_at",
473 "updated_at"
474 ) VALUES (
475 ${recipeId},
476 ${input.kind === "mcp" ? "mcp" : "http_api"},
477 ${input.adapterKey},
478 ${input.providerKey},
479 ${input.name},
480 NULL,
481 'workspace',
482 ${recipeRevisionId},
483 ${now},
484 ${now}
485 )
486 `);
487 await legacyPersistence!.db.execute(sql`
488 INSERT INTO "source_recipe_revisions" (
489 "id",
490 "recipe_id",
491 "revision_number",
492 "source_config_json",
493 "manifest_json",
494 "manifest_hash",
495 "created_at",
496 "updated_at"
497 ) VALUES (
498 ${recipeRevisionId},
499 ${recipeId},
500 1,
501 ${input.sourceConfigJson},
502 NULL,
503 NULL,
504 ${now},
505 ${now}
506 )
507 `);
508
509 return { recipeId, recipeRevisionId };
510 };
511
512 const openapi = await seedRecipe({
513 sourceId: "src_legacy_openapi",
514 kind: "openapi",
515 adapterKey: "openapi",
516 providerKey: "generic_http",
517 name: "Legacy OpenAPI",
518 endpoint: "https://api.example.com",
519 sourceConfigJson: JSON.stringify({
520 kind: "openapi",
521 endpoint: "https://api.example.com",
522 specUrl: "https://api.example.com/openapi.json",
523 defaultHeaders: { accept: "application/json" },
524 }),
525 });
526 const graphql = await seedRecipe({
527 sourceId: "src_legacy_graphql",
528 kind: "graphql",
529 adapterKey: "graphql_introspection",
530 providerKey: "generic_graphql",
531 name: "Legacy GraphQL",
532 endpoint: "https://api.example.com/graphql",
533 sourceConfigJson: JSON.stringify({
534 kind: "graphql",
535 endpoint: "https://api.example.com/graphql",
536 defaultHeaders: { accept: "application/json" },
537 }),
538 });
539 const mcp = await seedRecipe({
540 sourceId: "src_legacy_mcp",
541 kind: "mcp",
542 adapterKey: "mcp_manifest",
543 providerKey: "generic_mcp",
544 name: "Legacy MCP",
545 endpoint: "https://api.example.com/mcp",
546 sourceConfigJson: JSON.stringify({
547 kind: "mcp",
548 endpoint: "https://api.example.com/mcp",
549 transport: "streamable-http",
550 queryParams: { tenant: "acme" },
551 headers: { "x-tenant": "acme" },
552 }),
553 });
554
555 await legacyPersistence.db.execute(sql`
556 INSERT INTO "sources" (
557 "workspace_id",
558 "source_id",
559 "recipe_id",
560 "recipe_revision_id",
561 "name",
562 "kind",
563 "endpoint",
564 "status",
565 "enabled",
566 "namespace",
567 "binding_config_json",
568 "transport",
569 "query_params_json",
570 "headers_json",
571 "spec_url",
572 "default_headers_json",
573 "source_hash",
574 "source_document_text",
575 "last_error",
576 "created_at",
577 "updated_at"
578 ) VALUES
579 (
580 ${workspaceId},
581 ${SourceIdSchema.make("src_legacy_openapi")},
582 ${openapi.recipeId},
583 ${openapi.recipeRevisionId},
584 'Legacy OpenAPI',
585 'openapi',
586 'https://api.example.com',
587 'connected',
588 true,
589 'legacy.openapi',
590 NULL,
591 NULL,
592 NULL,
593 NULL,
594 'https://api.example.com/openapi.json',
595 '{"accept":"application/json"}',
596 NULL,
597 NULL,
598 NULL,
599 ${now},
600 ${now}
601 ),
602 (
603 ${workspaceId},
604 ${SourceIdSchema.make("src_legacy_graphql")},
605 ${graphql.recipeId},
606 ${graphql.recipeRevisionId},
607 'Legacy GraphQL',
608 'graphql',
609 'https://api.example.com/graphql',
610 'connected',
611 true,
612 'legacy.graphql',
613 NULL,
614 NULL,
615 NULL,
616 NULL,
617 NULL,
618 '{"accept":"application/json"}',
619 NULL,
620 NULL,
621 NULL,
622 ${now},
623 ${now}
624 ),
625 (
626 ${workspaceId},
627 ${SourceIdSchema.make("src_legacy_mcp")},
628 ${mcp.recipeId},
629 ${mcp.recipeRevisionId},
630 'Legacy MCP',
631 'mcp',
632 'https://api.example.com/mcp',
633 'connected',
634 true,
635 'legacy.mcp',
636 NULL,
637 'streamable-http',
638 '{"tenant":"acme"}',
639 '{"x-tenant":"acme"}',
640 NULL,
641 NULL,
642 NULL,
643 NULL,
644 NULL,
645 ${now},
646 ${now}
647 );
648 `);
649
650 await legacyPersistence.close();
651 legacyPersistence = null;
652
653 upgradedPersistence = await Effect.runPromise(
654 createSqlControlPlanePersistence({
655 localDataDir,
656 }),
657 );
658
659 const openApiSource = await Effect.runPromise(
660 upgradedPersistence.rows.sources.getByWorkspaceAndId(
661 workspaceId,
662 SourceIdSchema.make("src_legacy_openapi"),
663 ),
664 );
665 expect(
666 JSON.parse(Option.getOrThrow(openApiSource).bindingConfigJson),
667 ).toEqual({
668 adapterKey: "openapi",
669 version: 1,
670 payload: {
671 specUrl: "https://api.example.com/openapi.json",
672 defaultHeaders: {
673 accept: "application/json",
674 },
675 },
676 });
677
678 const graphqlSource = await Effect.runPromise(
679 upgradedPersistence.rows.sources.getByWorkspaceAndId(
680 workspaceId,
681 SourceIdSchema.make("src_legacy_graphql"),
682 ),
683 );
684 expect(
685 JSON.parse(Option.getOrThrow(graphqlSource).bindingConfigJson),
686 ).toEqual({
687 adapterKey: "graphql",
688 version: 1,
689 payload: {
690 defaultHeaders: {
691 accept: "application/json",
692 },
693 },
694 });
695
696 const graphqlRecipe = await Effect.runPromise(
697 upgradedPersistence.rows.sourceRecipes.getById(graphql.recipeId),
698 );
699 expect(Option.getOrNull(graphqlRecipe)?.adapterKey).toBe("graphql");
700
701 const mcpSource = await Effect.runPromise(
702 upgradedPersistence.rows.sources.getByWorkspaceAndId(
703 workspaceId,
704 SourceIdSchema.make("src_legacy_mcp"),
705 ),
706 );
707 expect(
708 JSON.parse(Option.getOrThrow(mcpSource).bindingConfigJson),
709 ).toEqual({
710 adapterKey: "mcp",
711 version: 1,
712 payload: {
713 transport: "streamable-http",
714 queryParams: {
715 tenant: "acme",
716 },
717 headers: {
718 "x-tenant": "acme",
719 },
720 },
721 });
722
723 const mcpRecipe = await Effect.runPromise(
724 upgradedPersistence.rows.sourceRecipes.getById(mcp.recipeId),
725 );
726 expect(Option.getOrNull(mcpRecipe)?.adapterKey).toBe("mcp");
727 } finally {
728 await legacyPersistence?.close().catch(() => undefined);
729 await upgradedPersistence?.close().catch(() => undefined);
730 rmSync(localDataDir, { recursive: true, force: true });
731 rmSync(legacyMigrationsDir, { recursive: true, force: true });
732 }
733 });
734
735 it("upgrades auth leases into template-based placement storage", async () => {
736 const localDataDir = mkdtempSync(path.join(tmpdir(), "executor-cp-db-"));
737 const legacyMigrationsDir = copyMigrationsBefore(
738 "20260312143000_auth_lease_template_placements",
739 );
740 const workspaceId = WorkspaceIdSchema.make("ws_legacy_auth_lease");
741 const accountId = AccountIdSchema.make("acc_legacy_auth_lease");
742 const sourceId = SourceIdSchema.make("src_legacy_auth_lease");
743 const recipeId = SourceRecipeIdSchema.make("src_recipe_legacy_auth_lease");
744 const recipeRevisionId = SourceRecipeRevisionIdSchema.make(
745 "src_recipe_rev_legacy_auth_lease",
746 );
747 const authArtifactId = "auth_art_legacy_auth_lease";
748 const now = Date.now();
749 let legacyPersistence: SqlControlPlanePersistence | null = null;
750 let upgradedPersistence: SqlControlPlanePersistence | null = null;
751
752 try {
753 legacyPersistence = await Effect.runPromise(
754 createSqlControlPlanePersistence({
755 localDataDir,
756 migrationsFolder: legacyMigrationsDir,
757 runCodeMigrations: false,
758 }),
759 );
760
761 await Effect.runPromise(legacyPersistence.rows.sourceRecipes.upsert({
762 id: recipeId,
763 kind: "http_api",
764 adapterKey: "openapi",
765 providerKey: "generic_http",
766 name: "Legacy OpenAPI",
767 summary: null,
768 visibility: "workspace",
769 latestRevisionId: recipeRevisionId,
770 createdAt: now,
771 updatedAt: now,
772 }));
773 await Effect.runPromise(legacyPersistence.rows.sourceRecipeRevisions.upsert(baseRevisionRecord({
774 id: recipeRevisionId,
775 recipeId,
776 revisionNumber: 1,
777 sourceConfigJson: JSON.stringify({
778 kind: "openapi",
779 endpoint: "https://api.example.com",
780 specUrl: "https://api.example.com/openapi.json",
781 }),
782 createdAt: now,
783 updatedAt: now,
784 })));
785 await legacyPersistence.db.execute(sql`
786 INSERT INTO "sources" (
787 "source_id",
788 "workspace_id",
789 "recipe_id",
790 "recipe_revision_id",
791 "name",
792 "kind",
793 "endpoint",
794 "status",
795 "enabled",
796 "namespace",
797 "import_auth_policy",
798 "binding_config_json",
799 "source_hash",
800 "last_error",
801 "created_at",
802 "updated_at"
803 ) VALUES (
804 ${sourceId},
805 ${workspaceId},
806 ${recipeId},
807 ${recipeRevisionId},
808 ${"Legacy OpenAPI"},
809 ${"openapi"},
810 ${"https://api.example.com"},
811 ${"connected"},
812 ${true},
813 ${"legacy.openapi"},
814 ${"reuse_runtime"},
815 ${openApiBindingConfigJson("https://api.example.com/openapi.json")},
816 NULL,
817 NULL,
818 ${now},
819 ${now}
820 )
821 `);
822 await Effect.runPromise(legacyPersistence.rows.authArtifacts.upsert({
823 id: authArtifactId as any,
824 workspaceId,
825 sourceId,
826 actorAccountId: accountId,
827 slot: "runtime",
828 artifactKind: "static_bearer",
829 configJson: JSON.stringify({
830 headerName: "Authorization",
831 prefix: "Bearer ",
832 token: {
833 providerId: "postgres",
834 handle: "sec_legacy_auth_lease_token",
835 },
836 }),
837 grantSetJson: null,
838 createdAt: now,
839 updatedAt: now,
840 }));
841
842 await legacyPersistence.db.execute(sql`
843 INSERT INTO "workspace_source_auth_leases" (
844 "id",
845 "auth_artifact_id",
846 "workspace_id",
847 "source_id",
848 "actor_account_id",
849 "slot",
850 "placements_json",
851 "expires_at",
852 "refresh_after",
853 "created_at",
854 "updated_at"
855 ) VALUES (
856 'auth_lease_legacy_auth_lease',
857 ${authArtifactId},
858 ${workspaceId},
859 ${sourceId},
860 ${accountId},
861 'runtime',
862 '[{"location":"header","name":"Authorization","value":"Bearer abc"},{"location":"query","name":"api_key","value":"q123"},{"location":"cookie","name":"sid","value":"cookie123"},{"location":"body","path":"auth.token","value":"body123"}]',
863 NULL,
864 NULL,
865 ${now},
866 ${now}
867 );
868 `);
869
870 await legacyPersistence.close();
871 legacyPersistence = null;
872
873 upgradedPersistence = await Effect.runPromise(
874 createSqlControlPlanePersistence({
875 localDataDir,
876 }),
877 );
878
879 const leaseOption = await Effect.runPromise(
880 upgradedPersistence.rows.authLeases.getByAuthArtifactId(authArtifactId as any),
881 );
882 expect(Option.isSome(leaseOption)).toBe(true);
883 const lease = Option.getOrNull(leaseOption);
884 expect(lease).not.toBeNull();
885 expect(lease?.placementsTemplateJson).not.toBeNull();
886 expect(decodeAuthLeasePlacementTemplates(lease!)).toEqual([
887 {
888 location: "header",
889 name: "Authorization",
890 parts: [{ kind: "literal", value: "Bearer abc" }],
891 },
892 {
893 location: "query",
894 name: "api_key",
895 parts: [{ kind: "literal", value: "q123" }],
896 },
897 {
898 location: "cookie",
899 name: "sid",
900 parts: [{ kind: "literal", value: "cookie123" }],
901 },
902 {
903 location: "body",
904 path: "auth.token",
905 parts: [{ kind: "literal", value: "body123" }],
906 },
907 ]);
908 } finally {
909 await legacyPersistence?.close().catch(() => undefined);
910 await upgradedPersistence?.close().catch(() => undefined);
911 rmSync(localDataDir, { recursive: true, force: true });
912 rmSync(legacyMigrationsDir, { recursive: true, force: true });
913 }
914 });
915});
packages/control-plane/src/persistence/repos/accounts-repo.ts+0−71
@@ -1,71 +0,0 @@
1import { type Account, AccountSchema } from "#schema";
2import * as Option from "effect/Option";
3import { Schema } from "effect";
4import { and, eq } from "drizzle-orm";
5
6import type { DrizzleClient } from "../client";
7import type { DrizzleTables } from "../schema";
8import { firstOption } from "./shared";
9
10const decodeAccount = Schema.decodeUnknownSync(AccountSchema);
11
12export const createAccountsRepo = (
13 client: DrizzleClient,
14 tables: DrizzleTables,
15) => ({
16 getById: (accountId: Account["id"]) =>
17 client.use("rows.accounts.get_by_id", async (db) => {
18 const rows = await db
19 .select()
20 .from(tables.accountsTable)
21 .where(eq(tables.accountsTable.id, accountId))
22 .limit(1);
23
24 const row = firstOption(rows);
25 return Option.isSome(row)
26 ? Option.some(decodeAccount(row.value))
27 : Option.none<Account>();
28 }),
29
30 getByProviderAndSubject: (
31 provider: Account["provider"],
32 subject: Account["subject"],
33 ) =>
34 client.use("rows.accounts.get_by_provider_and_subject", async (db) => {
35 const rows = await db
36 .select()
37 .from(tables.accountsTable)
38 .where(
39 and(
40 eq(tables.accountsTable.provider, provider),
41 eq(tables.accountsTable.subject, subject),
42 ),
43 )
44 .limit(1);
45
46 const row = firstOption(rows);
47 return Option.isSome(row)
48 ? Option.some(decodeAccount(row.value))
49 : Option.none<Account>();
50 }),
51
52 insert: (account: Account) =>
53 client.use("rows.accounts.insert", async (db) => {
54 await db.insert(tables.accountsTable).values(account);
55 }),
56
57 upsert: (account: Account) =>
58 client.use("rows.accounts.upsert", async (db) => {
59 await db
60 .insert(tables.accountsTable)
61 .values(account)
62 .onConflictDoUpdate({
63 target: [tables.accountsTable.provider, tables.accountsTable.subject],
64 set: {
65 email: account.email,
66 displayName: account.displayName,
67 updatedAt: account.updatedAt,
68 },
69 });
70 }),
71});
packages/control-plane/src/persistence/repos/auth-artifacts-repo.ts+0−226
@@ -1,226 +0,0 @@
1import {
2 AuthArtifactSchema,
3 type AuthArtifact,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, asc, eq, inArray, isNull, or } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption } from "./shared";
12
13const decodeAuthArtifact = Schema.decodeUnknownSync(AuthArtifactSchema);
14
15const authArtifactUpdateSet = (artifact: AuthArtifact) => {
16 const {
17 id: _id,
18 workspaceId: _workspaceId,
19 sourceId: _sourceId,
20 actorAccountId: _actorAccountId,
21 createdAt: _createdAt,
22 ...patch
23 } = artifact;
24 return patch;
25};
26
27export const createAuthArtifactsRepo = (
28 client: DrizzleClient,
29 tables: DrizzleTables,
30) => ({
31 listByWorkspaceId: (workspaceId: AuthArtifact["workspaceId"]) =>
32 client.use("rows.auth_artifacts.list_by_workspace", async (db) => {
33 const rows = await db
34 .select()
35 .from(tables.authArtifactsTable)
36 .where(eq(tables.authArtifactsTable.workspaceId, workspaceId))
37 .orderBy(
38 asc(tables.authArtifactsTable.updatedAt),
39 asc(tables.authArtifactsTable.id),
40 );
41
42 return rows.map((row) => decodeAuthArtifact(row));
43 }),
44
45 listByWorkspaceAndSourceId: (input: {
46 workspaceId: AuthArtifact["workspaceId"];
47 sourceId: AuthArtifact["sourceId"];
48 }) =>
49 client.use("rows.auth_artifacts.list_by_workspace_source", async (db) => {
50 const rows = await db
51 .select()
52 .from(tables.authArtifactsTable)
53 .where(
54 and(
55 eq(tables.authArtifactsTable.workspaceId, input.workspaceId),
56 eq(tables.authArtifactsTable.sourceId, input.sourceId),
57 ),
58 )
59 .orderBy(
60 asc(tables.authArtifactsTable.updatedAt),
61 asc(tables.authArtifactsTable.id),
62 );
63
64 return rows.map((row) => decodeAuthArtifact(row));
65 }),
66
67 listByWorkspaceSourceAndActor: (input: {
68 workspaceId: AuthArtifact["workspaceId"];
69 sourceId: AuthArtifact["sourceId"];
70 actorAccountId: AuthArtifact["actorAccountId"];
71 }) =>
72 client.use("rows.auth_artifacts.list_by_workspace_source_actor", async (db) => {
73 const rows = await db
74 .select()
75 .from(tables.authArtifactsTable)
76 .where(
77 and(
78 eq(tables.authArtifactsTable.workspaceId, input.workspaceId),
79 eq(tables.authArtifactsTable.sourceId, input.sourceId),
80 or(
81 input.actorAccountId === null
82 ? isNull(tables.authArtifactsTable.actorAccountId)
83 : eq(tables.authArtifactsTable.actorAccountId, input.actorAccountId),
84 input.actorAccountId === null
85 ? undefined
86 : isNull(tables.authArtifactsTable.actorAccountId),
87 ),
88 ),
89 )
90 .orderBy(
91 asc(tables.authArtifactsTable.slot),
92 asc(tables.authArtifactsTable.actorAccountId),
93 asc(tables.authArtifactsTable.updatedAt),
94 asc(tables.authArtifactsTable.id),
95 );
96
97 return rows.map((row) => decodeAuthArtifact(row));
98 }),
99
100 getByWorkspaceSourceAndActor: (input: {
101 workspaceId: AuthArtifact["workspaceId"];
102 sourceId: AuthArtifact["sourceId"];
103 actorAccountId: AuthArtifact["actorAccountId"];
104 slot: AuthArtifact["slot"];
105 }) =>
106 client.use("rows.auth_artifacts.get_by_workspace_source_actor", async (db) => {
107 const rows = await db
108 .select()
109 .from(tables.authArtifactsTable)
110 .where(
111 and(
112 eq(tables.authArtifactsTable.workspaceId, input.workspaceId),
113 eq(tables.authArtifactsTable.sourceId, input.sourceId),
114 eq(tables.authArtifactsTable.slot, input.slot),
115 input.actorAccountId === null
116 ? isNull(tables.authArtifactsTable.actorAccountId)
117 : eq(tables.authArtifactsTable.actorAccountId, input.actorAccountId),
118 ),
119 )
120 .limit(1);
121
122 const row = firstOption(rows);
123 return Option.isSome(row)
124 ? Option.some(decodeAuthArtifact(row.value))
125 : Option.none<AuthArtifact>();
126 }),
127
128 upsert: (artifact: AuthArtifact) =>
129 client.use("rows.auth_artifacts.upsert", async (db) => {
130 if (artifact.actorAccountId === null) {
131 const existingRows = await db
132 .select({
133 id: tables.authArtifactsTable.id,
134 })
135 .from(tables.authArtifactsTable)
136 .where(
137 and(
138 eq(tables.authArtifactsTable.workspaceId, artifact.workspaceId),
139 eq(tables.authArtifactsTable.sourceId, artifact.sourceId),
140 eq(tables.authArtifactsTable.slot, artifact.slot),
141 isNull(tables.authArtifactsTable.actorAccountId),
142 ),
143 )
144 .orderBy(
145 asc(tables.authArtifactsTable.updatedAt),
146 asc(tables.authArtifactsTable.id),
147 );
148
149 const existing = firstOption(existingRows);
150 if (Option.isSome(existing)) {
151 await db
152 .update(tables.authArtifactsTable)
153 .set(authArtifactUpdateSet(artifact))
154 .where(eq(tables.authArtifactsTable.id, existing.value.id));
155
156 const duplicateIds = existingRows.slice(1).map((row) => row.id);
157 if (duplicateIds.length > 0) {
158 await db
159 .delete(tables.authArtifactsTable)
160 .where(inArray(tables.authArtifactsTable.id, duplicateIds));
161 }
162
163 return;
164 }
165 }
166
167 await db
168 .insert(tables.authArtifactsTable)
169 .values(artifact)
170 .onConflictDoUpdate({
171 target: [
172 tables.authArtifactsTable.workspaceId,
173 tables.authArtifactsTable.sourceId,
174 tables.authArtifactsTable.actorAccountId,
175 tables.authArtifactsTable.slot,
176 ],
177 set: {
178 ...authArtifactUpdateSet(artifact),
179 },
180 });
181 }),
182
183 removeByWorkspaceSourceAndActor: (input: {
184 workspaceId: AuthArtifact["workspaceId"];
185 sourceId: AuthArtifact["sourceId"];
186 actorAccountId: AuthArtifact["actorAccountId"];
187 slot?: AuthArtifact["slot"];
188 }) =>
189 client.use("rows.auth_artifacts.remove_by_workspace_source_actor", async (db) => {
190 const deleted = await db
191 .delete(tables.authArtifactsTable)
192 .where(
193 and(
194 eq(tables.authArtifactsTable.workspaceId, input.workspaceId),
195 eq(tables.authArtifactsTable.sourceId, input.sourceId),
196 input.slot === undefined
197 ? undefined
198 : eq(tables.authArtifactsTable.slot, input.slot),
199 input.actorAccountId === null
200 ? isNull(tables.authArtifactsTable.actorAccountId)
201 : eq(tables.authArtifactsTable.actorAccountId, input.actorAccountId),
202 ),
203 )
204 .returning();
205
206 return deleted.length > 0;
207 }),
208
209 removeByWorkspaceAndSourceId: (input: {
210 workspaceId: AuthArtifact["workspaceId"];
211 sourceId: AuthArtifact["sourceId"];
212 }) =>
213 client.use("rows.auth_artifacts.remove_by_workspace_source", async (db) => {
214 const deleted = await db
215 .delete(tables.authArtifactsTable)
216 .where(
217 and(
218 eq(tables.authArtifactsTable.workspaceId, input.workspaceId),
219 eq(tables.authArtifactsTable.sourceId, input.sourceId),
220 ),
221 )
222 .returning();
223
224 return deleted.length;
225 }),
226});
packages/control-plane/src/persistence/repos/auth-leases-repo.ts+0−145
@@ -1,145 +0,0 @@
1import {
2 AuthLeaseSchema,
3 type AuthLease,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, asc, eq, isNull } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeAuthLease = Schema.decodeUnknownSync(AuthLeaseSchema);
14
15export const createAuthLeasesRepo = (
16 client: DrizzleClient,
17 tables: DrizzleTables,
18) => ({
19 getByAuthArtifactId: (authArtifactId: AuthLease["authArtifactId"]) =>
20 client.use("rows.auth_leases.get_by_auth_artifact_id", async (db) => {
21 const rows = await db
22 .select()
23 .from(tables.authLeasesTable)
24 .where(eq(tables.authLeasesTable.authArtifactId, authArtifactId))
25 .limit(1);
26
27 const row = firstOption(rows);
28 return Option.isSome(row)
29 ? Option.some(decodeAuthLease(row.value))
30 : Option.none<AuthLease>();
31 }),
32
33 listByWorkspaceId: (workspaceId: AuthLease["workspaceId"]) =>
34 client.use("rows.auth_leases.list_by_workspace", async (db) => {
35 const rows = await db
36 .select()
37 .from(tables.authLeasesTable)
38 .where(eq(tables.authLeasesTable.workspaceId, workspaceId))
39 .orderBy(
40 asc(tables.authLeasesTable.updatedAt),
41 asc(tables.authLeasesTable.id),
42 );
43
44 return rows.map((row) => decodeAuthLease(row));
45 }),
46
47 getByWorkspaceSourceAndActor: (input: {
48 workspaceId: AuthLease["workspaceId"];
49 sourceId: AuthLease["sourceId"];
50 actorAccountId: AuthLease["actorAccountId"];
51 slot: AuthLease["slot"];
52 }) =>
53 client.use("rows.auth_leases.get_by_workspace_source_actor", async (db) => {
54 const rows = await db
55 .select()
56 .from(tables.authLeasesTable)
57 .where(
58 and(
59 eq(tables.authLeasesTable.workspaceId, input.workspaceId),
60 eq(tables.authLeasesTable.sourceId, input.sourceId),
61 eq(tables.authLeasesTable.slot, input.slot),
62 input.actorAccountId === null
63 ? isNull(tables.authLeasesTable.actorAccountId)
64 : eq(tables.authLeasesTable.actorAccountId, input.actorAccountId),
65 ),
66 )
67 .orderBy(
68 asc(tables.authLeasesTable.updatedAt),
69 asc(tables.authLeasesTable.id),
70 )
71 .limit(1);
72
73 const row = firstOption(rows);
74 return Option.isSome(row)
75 ? Option.some(decodeAuthLease(row.value))
76 : Option.none<AuthLease>();
77 }),
78
79 upsert: (lease: AuthLease) =>
80 client.use("rows.auth_leases.upsert", async (db) => {
81 await db
82 .insert(tables.authLeasesTable)
83 .values(lease)
84 .onConflictDoUpdate({
85 target: [tables.authLeasesTable.authArtifactId],
86 set: {
87 ...withoutCreatedAt(lease),
88 },
89 });
90 }),
91
92 removeByAuthArtifactId: (authArtifactId: AuthLease["authArtifactId"]) =>
93 client.use("rows.auth_leases.remove_by_auth_artifact_id", async (db) => {
94 const deleted = await db
95 .delete(tables.authLeasesTable)
96 .where(eq(tables.authLeasesTable.authArtifactId, authArtifactId))
97 .returning();
98
99 return deleted.length > 0;
100 }),
101
102 removeByWorkspaceSourceAndActor: (input: {
103 workspaceId: AuthLease["workspaceId"];
104 sourceId: AuthLease["sourceId"];
105 actorAccountId: AuthLease["actorAccountId"];
106 slot?: AuthLease["slot"];
107 }) =>
108 client.use("rows.auth_leases.remove_by_workspace_source_actor", async (db) => {
109 const deleted = await db
110 .delete(tables.authLeasesTable)
111 .where(
112 and(
113 eq(tables.authLeasesTable.workspaceId, input.workspaceId),
114 eq(tables.authLeasesTable.sourceId, input.sourceId),
115 input.slot === undefined
116 ? undefined
117 : eq(tables.authLeasesTable.slot, input.slot),
118 input.actorAccountId === null
119 ? isNull(tables.authLeasesTable.actorAccountId)
120 : eq(tables.authLeasesTable.actorAccountId, input.actorAccountId),
121 ),
122 )
123 .returning();
124
125 return deleted.length > 0;
126 }),
127
128 removeByWorkspaceAndSourceId: (input: {
129 workspaceId: AuthLease["workspaceId"];
130 sourceId: AuthLease["sourceId"];
131 }) =>
132 client.use("rows.auth_leases.remove_by_workspace_source", async (db) => {
133 const deleted = await db
134 .delete(tables.authLeasesTable)
135 .where(
136 and(
137 eq(tables.authLeasesTable.workspaceId, input.workspaceId),
138 eq(tables.authLeasesTable.sourceId, input.sourceId),
139 ),
140 )
141 .returning();
142
143 return deleted.length;
144 }),
145});
packages/control-plane/src/persistence/repos/code-migrations-repo.ts+0−41
@@ -1,41 +0,0 @@
1import type {
2 StoredCodeMigrationRecord,
3} from "#schema";
4import { StoredCodeMigrationRecordSchema } from "#schema";
5import { Schema } from "effect";
6import { asc } from "drizzle-orm";
7
8import type { DrizzleClient } from "../client";
9import type { DrizzleTables } from "../schema";
10
11const decodeStoredCodeMigrationRecord = Schema.decodeUnknownSync(
12 StoredCodeMigrationRecordSchema,
13);
14
15export const createCodeMigrationsRepo = (
16 client: DrizzleClient,
17 tables: DrizzleTables,
18) => ({
19 listAll: () =>
20 client.use("rows.code_migrations.list_all", async (db) => {
21 const rows = await db
22 .select()
23 .from(tables.codeMigrationsTable)
24 .orderBy(asc(tables.codeMigrationsTable.appliedAt));
25
26 return rows.map((row) => decodeStoredCodeMigrationRecord(row));
27 }),
28
29 upsert: (record: StoredCodeMigrationRecord) =>
30 client.use("rows.code_migrations.upsert", async (db) => {
31 await db
32 .insert(tables.codeMigrationsTable)
33 .values(record)
34 .onConflictDoNothing();
35 }),
36
37 clearAll: () =>
38 client.use("rows.code_migrations.clear_all", async (db) => {
39 await db.delete(tables.codeMigrationsTable);
40 }),
41});
packages/control-plane/src/persistence/repos/execution-interactions-repo.ts+0−96
@@ -1,96 +0,0 @@
1import {
2 type ExecutionInteraction,
3 ExecutionInteractionSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, desc, eq } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption } from "./shared";
12
13const decodeExecutionInteraction = Schema.decodeUnknownSync(
14 ExecutionInteractionSchema,
15);
16
17export const createExecutionInteractionsRepo = (
18 client: DrizzleClient,
19 tables: DrizzleTables,
20) => ({
21 getById: (interactionId: ExecutionInteraction["id"]) =>
22 client.use("rows.execution_interactions.get_by_id", async (db) => {
23 const rows = await db
24 .select()
25 .from(tables.executionInteractionsTable)
26 .where(eq(tables.executionInteractionsTable.id, interactionId))
27 .limit(1);
28
29 const row = firstOption(rows);
30 return Option.isSome(row)
31 ? Option.some(decodeExecutionInteraction(row.value))
32 : Option.none<ExecutionInteraction>();
33 }),
34
35 listByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
36 client.use("rows.execution_interactions.list_by_execution_id", async (db) => {
37 const rows = await db
38 .select()
39 .from(tables.executionInteractionsTable)
40 .where(eq(tables.executionInteractionsTable.executionId, executionId))
41 .orderBy(
42 desc(tables.executionInteractionsTable.updatedAt),
43 desc(tables.executionInteractionsTable.id),
44 );
45
46 return rows.map((row) => decodeExecutionInteraction(row));
47 }),
48
49 getPendingByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
50 client.use(
51 "rows.execution_interactions.get_pending_by_execution_id",
52 async (db) => {
53 const rows = await db
54 .select()
55 .from(tables.executionInteractionsTable)
56 .where(
57 and(
58 eq(tables.executionInteractionsTable.executionId, executionId),
59 eq(tables.executionInteractionsTable.status, "pending"),
60 ),
61 )
62 .orderBy(
63 desc(tables.executionInteractionsTable.updatedAt),
64 desc(tables.executionInteractionsTable.id),
65 )
66 .limit(1);
67
68 const row = firstOption(rows);
69 return Option.isSome(row)
70 ? Option.some(decodeExecutionInteraction(row.value))
71 : Option.none<ExecutionInteraction>();
72 },
73 ),
74
75 insert: (interaction: ExecutionInteraction) =>
76 client.use("rows.execution_interactions.insert", async (db) => {
77 await db.insert(tables.executionInteractionsTable).values(interaction);
78 }),
79
80 update: (
81 interactionId: ExecutionInteraction["id"],
82 patch: Partial<Omit<ExecutionInteraction, "id" | "executionId" | "createdAt">>,
83 ) =>
84 client.use("rows.execution_interactions.update", async (db) => {
85 const rows = await db
86 .update(tables.executionInteractionsTable)
87 .set(patch)
88 .where(eq(tables.executionInteractionsTable.id, interactionId))
89 .returning();
90
91 const row = firstOption(rows);
92 return Option.isSome(row)
93 ? Option.some(decodeExecutionInteraction(row.value))
94 : Option.none<ExecutionInteraction>();
95 }),
96});
packages/control-plane/src/persistence/repos/execution-steps-repo.ts+0−89
@@ -1,89 +0,0 @@
1import {
2 type ExecutionStep,
3 ExecutionStepSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, desc, eq } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption } from "./shared";
12
13const decodeExecutionStep = Schema.decodeUnknownSync(ExecutionStepSchema);
14
15export const createExecutionStepsRepo = (
16 client: DrizzleClient,
17 tables: DrizzleTables,
18) => ({
19 getByExecutionAndSequence: (
20 executionId: ExecutionStep["executionId"],
21 sequence: ExecutionStep["sequence"],
22 ) =>
23 client.use("rows.execution_steps.get_by_execution_and_sequence", async (db) => {
24 const rows = await db
25 .select()
26 .from(tables.executionStepsTable)
27 .where(
28 and(
29 eq(tables.executionStepsTable.executionId, executionId),
30 eq(tables.executionStepsTable.sequence, sequence),
31 ),
32 )
33 .limit(1);
34
35 const row = firstOption(rows);
36 return Option.isSome(row)
37 ? Option.some(decodeExecutionStep(row.value))
38 : Option.none<ExecutionStep>();
39 }),
40
41 listByExecutionId: (executionId: ExecutionStep["executionId"]) =>
42 client.use("rows.execution_steps.list_by_execution_id", async (db) => {
43 const rows = await db
44 .select()
45 .from(tables.executionStepsTable)
46 .where(eq(tables.executionStepsTable.executionId, executionId))
47 .orderBy(
48 tables.executionStepsTable.sequence,
49 desc(tables.executionStepsTable.updatedAt),
50 );
51
52 return rows.map((row) => decodeExecutionStep(row));
53 }),
54
55 insert: (step: ExecutionStep) =>
56 client.use("rows.execution_steps.insert", async (db) => {
57 await db.insert(tables.executionStepsTable).values(step);
58 }),
59
60 deleteByExecutionId: (executionId: ExecutionStep["executionId"]) =>
61 client.use("rows.execution_steps.delete_by_execution_id", async (db) => {
62 await db
63 .delete(tables.executionStepsTable)
64 .where(eq(tables.executionStepsTable.executionId, executionId));
65 }),
66
67 updateByExecutionAndSequence: (
68 executionId: ExecutionStep["executionId"],
69 sequence: ExecutionStep["sequence"],
70 patch: Partial<Omit<ExecutionStep, "id" | "executionId" | "sequence" | "createdAt">>,
71 ) =>
72 client.use("rows.execution_steps.update_by_execution_and_sequence", async (db) => {
73 const rows = await db
74 .update(tables.executionStepsTable)
75 .set(patch)
76 .where(
77 and(
78 eq(tables.executionStepsTable.executionId, executionId),
79 eq(tables.executionStepsTable.sequence, sequence),
80 ),
81 )
82 .returning();
83
84 const row = firstOption(rows);
85 return Option.isSome(row)
86 ? Option.some(decodeExecutionStep(row.value))
87 : Option.none<ExecutionStep>();
88 }),
89});
packages/control-plane/src/persistence/repos/executions-repo.ts+0−75
@@ -1,75 +0,0 @@
1import { type Execution, ExecutionSchema } from "#schema";
2import * as Option from "effect/Option";
3import { Schema } from "effect";
4import { and, eq } from "drizzle-orm";
5
6import type { DrizzleClient } from "../client";
7import type { DrizzleTables } from "../schema";
8import { firstOption } from "./shared";
9
10const decodeExecution = Schema.decodeUnknownSync(ExecutionSchema);
11
12export const createExecutionsRepo = (
13 client: DrizzleClient,
14 tables: DrizzleTables,
15) => ({
16 getById: (executionId: Execution["id"]) =>
17 client.use("rows.executions.get_by_id", async (db) => {
18 const rows = await db
19 .select()
20 .from(tables.executionsTable)
21 .where(eq(tables.executionsTable.id, executionId))
22 .limit(1);
23
24 const row = firstOption(rows);
25 return Option.isSome(row)
26 ? Option.some(decodeExecution(row.value))
27 : Option.none<Execution>();
28 }),
29
30 getByWorkspaceAndId: (
31 workspaceId: Execution["workspaceId"],
32 executionId: Execution["id"],
33 ) =>
34 client.use("rows.executions.get_by_workspace_and_id", async (db) => {
35 const rows = await db
36 .select()
37 .from(tables.executionsTable)
38 .where(
39 and(
40 eq(tables.executionsTable.workspaceId, workspaceId),
41 eq(tables.executionsTable.id, executionId),
42 ),
43 )
44 .limit(1);
45
46 const row = firstOption(rows);
47 return Option.isSome(row)
48 ? Option.some(decodeExecution(row.value))
49 : Option.none<Execution>();
50 }),
51
52 insert: (execution: Execution) =>
53 client.use("rows.executions.insert", async (db) => {
54 await db.insert(tables.executionsTable).values(execution);
55 }),
56
57 update: (
58 executionId: Execution["id"],
59 patch: Partial<
60 Omit<Execution, "id" | "workspaceId" | "createdByAccountId" | "createdAt">
61 >,
62 ) =>
63 client.use("rows.executions.update", async (db) => {
64 const rows = await db
65 .update(tables.executionsTable)
66 .set(patch)
67 .where(eq(tables.executionsTable.id, executionId))
68 .returning();
69
70 const row = firstOption(rows);
71 return Option.isSome(row)
72 ? Option.some(decodeExecution(row.value))
73 : Option.none<Execution>();
74 }),
75});
packages/control-plane/src/persistence/repos/index.ts+0−16
@@ -1,16 +0,0 @@
1export { createAccountsRepo } from "./accounts-repo";
2export { createSourcesRepo } from "./sources-repo";
3export { createSourceRecipesRepo } from "./source-recipes-repo";
4export { createSourceRecipeRevisionsRepo } from "./source-recipe-revisions-repo";
5export { createSourceRecipeDocumentsRepo } from "./source-recipe-documents-repo";
6export { createSourceRecipeSchemaBundlesRepo } from "./source-recipe-schema-bundles-repo";
7export { createSourceRecipeOperationsRepo } from "./source-recipe-operations-repo";
8export { createCodeMigrationsRepo } from "./code-migrations-repo";
9export { createAuthArtifactsRepo } from "./auth-artifacts-repo";
10export { createAuthLeasesRepo } from "./auth-leases-repo";
11export { createSourceOauthClientsRepo } from "./source-oauth-clients-repo";
12export { createSecretMaterialsRepo } from "./secret-materials-repo";
13export { createSourceAuthSessionsRepo } from "./source-auth-sessions-repo";
14export { createExecutionsRepo } from "./executions-repo";
15export { createExecutionInteractionsRepo } from "./execution-interactions-repo";
16export { createExecutionStepsRepo } from "./execution-steps-repo";
packages/control-plane/src/persistence/repos/secret-materials-repo.ts+0−144
@@ -1,144 +0,0 @@
1import {
2 authArtifactSecretRefs,
3 type SecretMaterial,
4 SecretMaterialSchema,
5} from "#schema";
6import * as Option from "effect/Option";
7import { Schema } from "effect";
8import { eq, desc, and } from "drizzle-orm";
9
10import type { DrizzleClient } from "../client";
11import type { DrizzleTables } from "../schema";
12import { firstOption, withoutCreatedAt } from "./shared";
13
14const decodeSecretMaterial = Schema.decodeUnknownSync(SecretMaterialSchema);
15
16export const createSecretMaterialsRepo = (
17 client: DrizzleClient,
18 tables: DrizzleTables,
19) => ({
20 getById: (id: SecretMaterial["id"]) =>
21 client.use("rows.secret_materials.get_by_id", async (db) => {
22 const rows = await db
23 .select()
24 .from(tables.secretMaterialsTable)
25 .where(eq(tables.secretMaterialsTable.id, id))
26 .limit(1);
27
28 const row = firstOption(rows);
29 return Option.isSome(row)
30 ? Option.some(decodeSecretMaterial(row.value))
31 : Option.none<SecretMaterial>();
32 }),
33
34 listAll: () =>
35 client.use("rows.secret_materials.list_all", async (db) => {
36 const rows = await db
37 .select({
38 id: tables.secretMaterialsTable.id,
39 name: tables.secretMaterialsTable.name,
40 purpose: tables.secretMaterialsTable.purpose,
41 createdAt: tables.secretMaterialsTable.createdAt,
42 updatedAt: tables.secretMaterialsTable.updatedAt,
43 })
44 .from(tables.secretMaterialsTable)
45 .orderBy(desc(tables.secretMaterialsTable.updatedAt));
46
47 return rows;
48 }),
49
50 upsert: (material: SecretMaterial) =>
51 client.use("rows.secret_materials.upsert", async (db) => {
52 await db
53 .insert(tables.secretMaterialsTable)
54 .values(material)
55 .onConflictDoUpdate({
56 target: [tables.secretMaterialsTable.id],
57 set: {
58 ...withoutCreatedAt(material),
59 },
60 });
61 }),
62
63 updateById: (id: SecretMaterial["id"], update: { name?: string | null; value?: string }) =>
64 client.use("rows.secret_materials.update_by_id", async (db) => {
65 const set: Record<string, unknown> = { updatedAt: Date.now() };
66 if (update.name !== undefined) set.name = update.name;
67 if (update.value !== undefined) set.value = update.value;
68
69 const updated = await db
70 .update(tables.secretMaterialsTable)
71 .set(set)
72 .where(eq(tables.secretMaterialsTable.id, id))
73 .returning({
74 id: tables.secretMaterialsTable.id,
75 name: tables.secretMaterialsTable.name,
76 purpose: tables.secretMaterialsTable.purpose,
77 createdAt: tables.secretMaterialsTable.createdAt,
78 updatedAt: tables.secretMaterialsTable.updatedAt,
79 });
80
81 const row = firstOption(updated);
82 return Option.isSome(row) ? Option.some(row.value) : Option.none();
83 }),
84
85 removeById: (id: SecretMaterial["id"]) =>
86 client.use("rows.secret_materials.remove", async (db) => {
87 const deleted = await db
88 .delete(tables.secretMaterialsTable)
89 .where(eq(tables.secretMaterialsTable.id, id))
90 .returning();
91
92 return deleted.length > 0;
93 }),
94
95 /**
96 * For each secret, find the sources that reference it via auth artifacts.
97 * Returns a map of secretId -> Array<{ sourceId, sourceName }>.
98 */
99 listLinkedSources: () =>
100 client.use("rows.secret_materials.list_linked_sources", async (db) => {
101 const rows = await db
102 .select({
103 artifactKind: tables.authArtifactsTable.artifactKind,
104 configJson: tables.authArtifactsTable.configJson,
105 sourceId: tables.authArtifactsTable.sourceId,
106 sourceName: tables.sourcesTable.name,
107 })
108 .from(tables.authArtifactsTable)
109 .innerJoin(
110 tables.sourcesTable,
111 and(
112 eq(tables.authArtifactsTable.workspaceId, tables.sourcesTable.workspaceId),
113 eq(tables.authArtifactsTable.sourceId, tables.sourcesTable.sourceId),
114 ),
115 );
116
117 const result = new Map<string, Array<{ sourceId: string; sourceName: string }>>();
118
119 for (const row of rows) {
120 const addLink = (secretId: string) => {
121 let links = result.get(secretId);
122 if (!links) {
123 links = [];
124 result.set(secretId, links);
125 }
126 // Avoid duplicate entries
127 if (!links.some((l) => l.sourceId === row.sourceId)) {
128 links.push({ sourceId: row.sourceId, sourceName: row.sourceName });
129 }
130 };
131
132 for (const ref of authArtifactSecretRefs({
133 artifactKind: row.artifactKind,
134 configJson: row.configJson,
135 })) {
136 if (ref.providerId === "postgres") {
137 addLink(ref.handle);
138 }
139 }
140 }
141
142 return result;
143 }),
144});
packages/control-plane/src/persistence/repos/shared.ts+0−126
@@ -1,126 +0,0 @@
1import { authArtifactSecretRefs, type AuthArtifact } from "#schema";
2import { inArray } from "drizzle-orm";
3import * as Option from "effect/Option";
4
5import type { DrizzleClient } from "../client";
6import type { DrizzleTables } from "../schema";
7
8export const firstOption = <A>(rows: ReadonlyArray<A>): Option.Option<A> =>
9 rows.length > 0 ? Option.some(rows[0] as A) : Option.none<A>();
10
11export const withoutCreatedAt = <A extends { createdAt: unknown }>(
12 value: A,
13): Omit<A, "createdAt"> => {
14 const { createdAt: _createdAt, ...rest } = value;
15 return rest;
16};
17
18const POSTGRES_SECRET_PROVIDER_ID = "postgres";
19
20export const postgresSecretHandlesFromAuthArtifacts = (
21 artifacts: ReadonlyArray<Pick<AuthArtifact, "artifactKind" | "configJson">>,
22): ReadonlyArray<string> => {
23 const handles = new Set<string>();
24
25 for (const artifact of artifacts) {
26 for (const ref of authArtifactSecretRefs(artifact)) {
27 if (ref.providerId === POSTGRES_SECRET_PROVIDER_ID) {
28 handles.add(ref.handle);
29 }
30 }
31 }
32
33 return [...handles];
34};
35
36export const chunkArray = <A>(
37 values: ReadonlyArray<A>,
38 chunkSize: number,
39): ReadonlyArray<ReadonlyArray<A>> => {
40 if (chunkSize <= 0) {
41 throw new Error(`chunkSize must be positive, received ${chunkSize}`);
42 }
43
44 const chunks: Array<ReadonlyArray<A>> = [];
45 for (let index = 0; index < values.length; index += chunkSize) {
46 chunks.push(values.slice(index, index + chunkSize));
47 }
48
49 return chunks;
50};
51
52export const cleanupOrphanedSourceRecipes = async (input: {
53 tx: Parameters<Parameters<DrizzleClient["useTx"]>[1]>[0];
54 tables: DrizzleTables;
55 candidateRecipeIds: ReadonlyArray<string>;
56 candidateRecipeRevisionIds: ReadonlyArray<string>;
57}) => {
58 const recipeIds = [...new Set(input.candidateRecipeIds)];
59 const recipeRevisionIds = [...new Set(input.candidateRecipeRevisionIds)];
60
61 const remainingRevisionIds = recipeRevisionIds.length > 0
62 ? (
63 await input.tx
64 .select({ recipeRevisionId: input.tables.sourcesTable.recipeRevisionId })
65 .from(input.tables.sourcesTable)
66 .where(inArray(input.tables.sourcesTable.recipeRevisionId, recipeRevisionIds))
67 ).map((row) => row.recipeRevisionId)
68 : [];
69 const orphanRevisionIds = recipeRevisionIds.filter(
70 (recipeRevisionId) => !remainingRevisionIds.includes(recipeRevisionId),
71 );
72
73 if (orphanRevisionIds.length > 0) {
74 await input.tx
75 .delete(input.tables.sourceRecipeDocumentsTable)
76 .where(inArray(input.tables.sourceRecipeDocumentsTable.recipeRevisionId, orphanRevisionIds));
77 await input.tx
78 .delete(input.tables.sourceRecipeSchemaBundlesTable)
79 .where(inArray(input.tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, orphanRevisionIds));
80 await input.tx
81 .delete(input.tables.sourceRecipeOperationsTable)
82 .where(inArray(input.tables.sourceRecipeOperationsTable.recipeRevisionId, orphanRevisionIds));
83 }
84
85 const remainingRecipeIds = recipeIds.length > 0
86 ? (
87 await input.tx
88 .select({ recipeId: input.tables.sourcesTable.recipeId })
89 .from(input.tables.sourcesTable)
90 .where(inArray(input.tables.sourcesTable.recipeId, recipeIds))
91 ).map((row) => row.recipeId)
92 : [];
93 const orphanRecipeIds = recipeIds.filter(
94 (recipeId) => !remainingRecipeIds.includes(recipeId),
95 );
96
97 if (orphanRecipeIds.length === 0) {
98 return;
99 }
100
101 const orphanRecipeRevisionIds = (
102 await input.tx
103 .select({ id: input.tables.sourceRecipeRevisionsTable.id })
104 .from(input.tables.sourceRecipeRevisionsTable)
105 .where(inArray(input.tables.sourceRecipeRevisionsTable.recipeId, orphanRecipeIds))
106 ).map((revision) => revision.id);
107
108 if (orphanRecipeRevisionIds.length > 0) {
109 await input.tx
110 .delete(input.tables.sourceRecipeDocumentsTable)
111 .where(inArray(input.tables.sourceRecipeDocumentsTable.recipeRevisionId, orphanRecipeRevisionIds));
112 await input.tx
113 .delete(input.tables.sourceRecipeSchemaBundlesTable)
114 .where(inArray(input.tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, orphanRecipeRevisionIds));
115 await input.tx
116 .delete(input.tables.sourceRecipeOperationsTable)
117 .where(inArray(input.tables.sourceRecipeOperationsTable.recipeRevisionId, orphanRecipeRevisionIds));
118 }
119
120 await input.tx
121 .delete(input.tables.sourceRecipeRevisionsTable)
122 .where(inArray(input.tables.sourceRecipeRevisionsTable.recipeId, orphanRecipeIds));
123 await input.tx
124 .delete(input.tables.sourceRecipesTable)
125 .where(inArray(input.tables.sourceRecipesTable.id, orphanRecipeIds));
126};
packages/control-plane/src/persistence/repos/source-auth-sessions-repo.ts+0−163
@@ -1,163 +0,0 @@
1import {
2 type SourceAuthSession,
3 SourceAuthSessionSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, asc, eq, isNull } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeSourceAuthSession = Schema.decodeUnknownSync(SourceAuthSessionSchema);
14
15type Mutable<T> = { -readonly [K in keyof T]: T[K] };
16type SourceAuthSessionPatch = Partial<
17 Omit<Mutable<SourceAuthSession>, "id" | "workspaceId" | "sourceId" | "createdAt">
18>;
19
20export const createSourceAuthSessionsRepo = (
21 client: DrizzleClient,
22 tables: DrizzleTables,
23) => ({
24 listAll: () =>
25 client.use("rows.source_auth_sessions.list_all", async (db) => {
26 const rows = await db
27 .select()
28 .from(tables.sourceAuthSessionsTable)
29 .orderBy(asc(tables.sourceAuthSessionsTable.updatedAt), asc(tables.sourceAuthSessionsTable.id));
30
31 return rows.map((row) => decodeSourceAuthSession(row));
32 }),
33
34 listByWorkspaceId: (workspaceId: SourceAuthSession["workspaceId"]) =>
35 client.use("rows.source_auth_sessions.list_by_workspace", async (db) => {
36 const rows = await db
37 .select()
38 .from(tables.sourceAuthSessionsTable)
39 .where(eq(tables.sourceAuthSessionsTable.workspaceId, workspaceId))
40 .orderBy(asc(tables.sourceAuthSessionsTable.updatedAt), asc(tables.sourceAuthSessionsTable.id));
41
42 return rows.map((row) => decodeSourceAuthSession(row));
43 }),
44
45 getById: (id: SourceAuthSession["id"]) =>
46 client.use("rows.source_auth_sessions.get_by_id", async (db) => {
47 const rows = await db
48 .select()
49 .from(tables.sourceAuthSessionsTable)
50 .where(eq(tables.sourceAuthSessionsTable.id, id))
51 .limit(1);
52
53 const row = firstOption(rows);
54 return Option.isSome(row)
55 ? Option.some(decodeSourceAuthSession(row.value))
56 : Option.none<SourceAuthSession>();
57 }),
58
59 getByState: (state: SourceAuthSession["state"]) =>
60 client.use("rows.source_auth_sessions.get_by_state", async (db) => {
61 const rows = await db
62 .select()
63 .from(tables.sourceAuthSessionsTable)
64 .where(eq(tables.sourceAuthSessionsTable.state, state))
65 .limit(1);
66
67 const row = firstOption(rows);
68 return Option.isSome(row)
69 ? Option.some(decodeSourceAuthSession(row.value))
70 : Option.none<SourceAuthSession>();
71 }),
72
73 getPendingByWorkspaceSourceAndActor: (input: {
74 workspaceId: SourceAuthSession["workspaceId"];
75 sourceId: SourceAuthSession["sourceId"];
76 actorAccountId: SourceAuthSession["actorAccountId"];
77 credentialSlot?: SourceAuthSession["credentialSlot"];
78 }) =>
79 client.use(
80 "rows.source_auth_sessions.get_pending_by_workspace_source_actor",
81 async (db) => {
82 const rows = await db
83 .select()
84 .from(tables.sourceAuthSessionsTable)
85 .where(
86 and(
87 eq(tables.sourceAuthSessionsTable.workspaceId, input.workspaceId),
88 eq(tables.sourceAuthSessionsTable.sourceId, input.sourceId),
89 input.credentialSlot === undefined
90 ? undefined
91 : eq(tables.sourceAuthSessionsTable.credentialSlot, input.credentialSlot),
92 input.actorAccountId === null
93 ? isNull(tables.sourceAuthSessionsTable.actorAccountId)
94 : eq(tables.sourceAuthSessionsTable.actorAccountId, input.actorAccountId),
95 eq(tables.sourceAuthSessionsTable.status, "pending"),
96 ),
97 )
98 .orderBy(
99 asc(tables.sourceAuthSessionsTable.updatedAt),
100 asc(tables.sourceAuthSessionsTable.id),
101 )
102 .limit(1);
103
104 const row = firstOption(rows);
105 return Option.isSome(row)
106 ? Option.some(decodeSourceAuthSession(row.value))
107 : Option.none<SourceAuthSession>();
108 },
109 ),
110
111 insert: (session: SourceAuthSession) =>
112 client.use("rows.source_auth_sessions.insert", async (db) => {
113 await db.insert(tables.sourceAuthSessionsTable).values(session);
114 }),
115
116 update: (
117 id: SourceAuthSession["id"],
118 patch: SourceAuthSessionPatch,
119 ) =>
120 client.use("rows.source_auth_sessions.update", async (db) => {
121 const rows = await db
122 .update(tables.sourceAuthSessionsTable)
123 .set(patch)
124 .where(eq(tables.sourceAuthSessionsTable.id, id))
125 .returning();
126
127 const row = firstOption(rows);
128 return Option.isSome(row)
129 ? Option.some(decodeSourceAuthSession(row.value))
130 : Option.none<SourceAuthSession>();
131 }),
132
133 upsert: (session: SourceAuthSession) =>
134 client.use("rows.source_auth_sessions.upsert", async (db) => {
135 await db
136 .insert(tables.sourceAuthSessionsTable)
137 .values(session)
138 .onConflictDoUpdate({
139 target: [tables.sourceAuthSessionsTable.id],
140 set: {
141 ...withoutCreatedAt(session),
142 },
143 });
144 }),
145
146 removeByWorkspaceAndSourceId: (
147 workspaceId: SourceAuthSession["workspaceId"],
148 sourceId: SourceAuthSession["sourceId"],
149 ) =>
150 client.use("rows.source_auth_sessions.remove_by_workspace_source", async (db) => {
151 const deleted = await db
152 .delete(tables.sourceAuthSessionsTable)
153 .where(
154 and(
155 eq(tables.sourceAuthSessionsTable.workspaceId, workspaceId),
156 eq(tables.sourceAuthSessionsTable.sourceId, sourceId),
157 ),
158 )
159 .returning();
160
161 return deleted.length > 0;
162 }),
163});
packages/control-plane/src/persistence/repos/source-oauth-clients-repo.ts+0−79
@@ -1,79 +0,0 @@
1import {
2 type WorkspaceSourceOauthClient,
3 WorkspaceSourceOauthClientSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, eq } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeWorkspaceSourceOauthClient = Schema.decodeUnknownSync(
14 WorkspaceSourceOauthClientSchema,
15);
16
17export const createSourceOauthClientsRepo = (
18 client: DrizzleClient,
19 tables: DrizzleTables,
20) => ({
21 getByWorkspaceSourceAndProvider: (input: {
22 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
23 sourceId: WorkspaceSourceOauthClient["sourceId"];
24 providerKey: string;
25 }) =>
26 client.use("rows.source_oauth_clients.get_by_workspace_source_provider", async (db) => {
27 const rows = await db
28 .select()
29 .from(tables.workspaceSourceOauthClientsTable)
30 .where(
31 and(
32 eq(tables.workspaceSourceOauthClientsTable.workspaceId, input.workspaceId),
33 eq(tables.workspaceSourceOauthClientsTable.sourceId, input.sourceId),
34 eq(tables.workspaceSourceOauthClientsTable.providerKey, input.providerKey),
35 ),
36 )
37 .limit(1);
38
39 const row = firstOption(rows);
40 return Option.isSome(row)
41 ? Option.some(decodeWorkspaceSourceOauthClient(row.value))
42 : Option.none<WorkspaceSourceOauthClient>();
43 }),
44
45 upsert: (oauthClient: WorkspaceSourceOauthClient) =>
46 client.use("rows.source_oauth_clients.upsert", async (db) => {
47 await db
48 .insert(tables.workspaceSourceOauthClientsTable)
49 .values(oauthClient)
50 .onConflictDoUpdate({
51 target: [
52 tables.workspaceSourceOauthClientsTable.workspaceId,
53 tables.workspaceSourceOauthClientsTable.sourceId,
54 tables.workspaceSourceOauthClientsTable.providerKey,
55 ],
56 set: {
57 ...withoutCreatedAt(oauthClient),
58 },
59 });
60 }),
61
62 removeByWorkspaceAndSourceId: (input: {
63 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
64 sourceId: WorkspaceSourceOauthClient["sourceId"];
65 }) =>
66 client.use("rows.source_oauth_clients.remove_by_workspace_source", async (db) => {
67 const deleted = await db
68 .delete(tables.workspaceSourceOauthClientsTable)
69 .where(
70 and(
71 eq(tables.workspaceSourceOauthClientsTable.workspaceId, input.workspaceId),
72 eq(tables.workspaceSourceOauthClientsTable.sourceId, input.sourceId),
73 ),
74 )
75 .returning();
76
77 return deleted.length;
78 }),
79});
packages/control-plane/src/persistence/repos/source-recipe-documents-repo.ts+0−85
@@ -1,85 +0,0 @@
1import {
2 type StoredSourceRecipeDocumentRecord,
3 StoredSourceRecipeDocumentRecordSchema,
4} from "#schema";
5import { Schema } from "effect";
6import { asc, eq, inArray } from "drizzle-orm";
7
8import type { DrizzleClient } from "../client";
9import type { DrizzleTables } from "../schema";
10import { chunkArray } from "./shared";
11
12const decodeStoredSourceRecipeDocumentRecord = Schema.decodeUnknownSync(
13 StoredSourceRecipeDocumentRecordSchema,
14);
15
16const RECIPE_DOCUMENT_INSERT_BATCH_SIZE = 500;
17
18export const createSourceRecipeDocumentsRepo = (
19 client: DrizzleClient,
20 tables: DrizzleTables,
21) => ({
22 listByRevisionId: (recipeRevisionId: StoredSourceRecipeDocumentRecord["recipeRevisionId"]) =>
23 client.use("rows.source_recipe_documents.list_by_revision", async (db) => {
24 const rows = await db
25 .select()
26 .from(tables.sourceRecipeDocumentsTable)
27 .where(eq(tables.sourceRecipeDocumentsTable.recipeRevisionId, recipeRevisionId))
28 .orderBy(
29 asc(tables.sourceRecipeDocumentsTable.documentKind),
30 asc(tables.sourceRecipeDocumentsTable.documentKey),
31 );
32
33 return rows.map((row) => decodeStoredSourceRecipeDocumentRecord(row));
34 }),
35
36 listByRevisionIds: (
37 recipeRevisionIds: readonly StoredSourceRecipeDocumentRecord["recipeRevisionId"][],
38 ) =>
39 client.use("rows.source_recipe_documents.list_by_revisions", async (db) => {
40 if (recipeRevisionIds.length === 0) {
41 return [] as StoredSourceRecipeDocumentRecord[];
42 }
43
44 const rows = await db
45 .select()
46 .from(tables.sourceRecipeDocumentsTable)
47 .where(inArray(tables.sourceRecipeDocumentsTable.recipeRevisionId, [...recipeRevisionIds]))
48 .orderBy(
49 asc(tables.sourceRecipeDocumentsTable.recipeRevisionId),
50 asc(tables.sourceRecipeDocumentsTable.documentKind),
51 asc(tables.sourceRecipeDocumentsTable.documentKey),
52 );
53
54 return rows.map((row) => decodeStoredSourceRecipeDocumentRecord(row));
55 }),
56
57 replaceForRevision: (input: {
58 recipeRevisionId: StoredSourceRecipeDocumentRecord["recipeRevisionId"];
59 documents: readonly StoredSourceRecipeDocumentRecord[];
60 }) =>
61 client.useTx("rows.source_recipe_documents.replace_for_revision", async (tx) => {
62 await tx
63 .delete(tables.sourceRecipeDocumentsTable)
64 .where(eq(tables.sourceRecipeDocumentsTable.recipeRevisionId, input.recipeRevisionId));
65
66 if (input.documents.length > 0) {
67 for (const batch of chunkArray(
68 input.documents,
69 RECIPE_DOCUMENT_INSERT_BATCH_SIZE,
70 )) {
71 await tx.insert(tables.sourceRecipeDocumentsTable).values([...batch]);
72 }
73 }
74 }),
75
76 removeByRevisionId: (recipeRevisionId: StoredSourceRecipeDocumentRecord["recipeRevisionId"]) =>
77 client.use("rows.source_recipe_documents.remove_by_revision", async (db) => {
78 const deleted = await db
79 .delete(tables.sourceRecipeDocumentsTable)
80 .where(eq(tables.sourceRecipeDocumentsTable.recipeRevisionId, recipeRevisionId))
81 .returning();
82
83 return deleted.length;
84 }),
85});
packages/control-plane/src/persistence/repos/source-recipe-operations-repo.ts+0−86
@@ -1,86 +0,0 @@
1import {
2 type StoredSourceRecipeOperationRecord,
3 StoredSourceRecipeOperationRecordSchema,
4} from "#schema";
5import { Schema } from "effect";
6import { asc, eq, inArray } from "drizzle-orm";
7
8import type { DrizzleClient } from "../client";
9import type { DrizzleTables } from "../schema";
10import { chunkArray } from "./shared";
11
12const decodeStoredSourceRecipeOperationRecord = Schema.decodeUnknownSync(
13 StoredSourceRecipeOperationRecordSchema,
14);
15
16// Keep wide recipe-operation inserts well below Postgres/PGlite parameter limits.
17const RECIPE_OPERATION_INSERT_BATCH_SIZE = 200;
18
19export const createSourceRecipeOperationsRepo = (
20 client: DrizzleClient,
21 tables: DrizzleTables,
22) => ({
23 listByRevisionId: (recipeRevisionId: StoredSourceRecipeOperationRecord["recipeRevisionId"]) =>
24 client.use("rows.source_recipe_operations.list_by_revision", async (db) => {
25 const rows = await db
26 .select()
27 .from(tables.sourceRecipeOperationsTable)
28 .where(eq(tables.sourceRecipeOperationsTable.recipeRevisionId, recipeRevisionId))
29 .orderBy(
30 asc(tables.sourceRecipeOperationsTable.toolId),
31 asc(tables.sourceRecipeOperationsTable.id),
32 );
33
34 return rows.map((row) => decodeStoredSourceRecipeOperationRecord(row));
35 }),
36
37 listByRevisionIds: (
38 recipeRevisionIds: readonly StoredSourceRecipeOperationRecord["recipeRevisionId"][],
39 ) =>
40 client.use("rows.source_recipe_operations.list_by_revisions", async (db) => {
41 if (recipeRevisionIds.length === 0) {
42 return [] as StoredSourceRecipeOperationRecord[];
43 }
44
45 const rows = await db
46 .select()
47 .from(tables.sourceRecipeOperationsTable)
48 .where(inArray(tables.sourceRecipeOperationsTable.recipeRevisionId, [...recipeRevisionIds]))
49 .orderBy(
50 asc(tables.sourceRecipeOperationsTable.recipeRevisionId),
51 asc(tables.sourceRecipeOperationsTable.toolId),
52 asc(tables.sourceRecipeOperationsTable.id),
53 );
54
55 return rows.map((row) => decodeStoredSourceRecipeOperationRecord(row));
56 }),
57
58 replaceForRevision: (input: {
59 recipeRevisionId: StoredSourceRecipeOperationRecord["recipeRevisionId"];
60 operations: readonly StoredSourceRecipeOperationRecord[];
61 }) =>
62 client.useTx("rows.source_recipe_operations.replace_for_revision", async (tx) => {
63 await tx
64 .delete(tables.sourceRecipeOperationsTable)
65 .where(eq(tables.sourceRecipeOperationsTable.recipeRevisionId, input.recipeRevisionId));
66
67 if (input.operations.length > 0) {
68 for (const batch of chunkArray(
69 input.operations,
70 RECIPE_OPERATION_INSERT_BATCH_SIZE,
71 )) {
72 await tx.insert(tables.sourceRecipeOperationsTable).values([...batch]);
73 }
74 }
75 }),
76
77 removeByRevisionId: (recipeRevisionId: StoredSourceRecipeOperationRecord["recipeRevisionId"]) =>
78 client.use("rows.source_recipe_operations.remove_by_revision", async (db) => {
79 const deleted = await db
80 .delete(tables.sourceRecipeOperationsTable)
81 .where(eq(tables.sourceRecipeOperationsTable.recipeRevisionId, recipeRevisionId))
82 .returning();
83
84 return deleted.length;
85 }),
86});
packages/control-plane/src/persistence/repos/source-recipe-revisions-repo.ts+0−157
@@ -1,157 +0,0 @@
1import {
2 type StoredSourceRecipeRevisionRecord,
3 StoredSourceRecipeRevisionRecordSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { asc, desc, eq, inArray, and } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeStoredSourceRecipeRevisionRecord = Schema.decodeUnknownSync(
14 StoredSourceRecipeRevisionRecordSchema,
15);
16
17type Mutable<T> = { -readonly [K in keyof T]: T[K] };
18type RecipeRevisionPatch = Partial<
19 Omit<Mutable<StoredSourceRecipeRevisionRecord>, "id" | "recipeId" | "revisionNumber" | "createdAt">
20>;
21
22export const createSourceRecipeRevisionsRepo = (
23 client: DrizzleClient,
24 tables: DrizzleTables,
25) => ({
26 listByRecipeId: (recipeId: StoredSourceRecipeRevisionRecord["recipeId"]) =>
27 client.use("rows.source_recipe_revisions.list_by_recipe", async (db) => {
28 const rows = await db
29 .select()
30 .from(tables.sourceRecipeRevisionsTable)
31 .where(eq(tables.sourceRecipeRevisionsTable.recipeId, recipeId))
32 .orderBy(
33 asc(tables.sourceRecipeRevisionsTable.revisionNumber),
34 asc(tables.sourceRecipeRevisionsTable.createdAt),
35 );
36
37 return rows.map((row) => decodeStoredSourceRecipeRevisionRecord(row));
38 }),
39
40 listByIds: (ids: readonly StoredSourceRecipeRevisionRecord["id"][]) =>
41 client.use("rows.source_recipe_revisions.list_by_ids", async (db) => {
42 if (ids.length === 0) {
43 return [] as StoredSourceRecipeRevisionRecord[];
44 }
45
46 const rows = await db
47 .select()
48 .from(tables.sourceRecipeRevisionsTable)
49 .where(inArray(tables.sourceRecipeRevisionsTable.id, [...ids]))
50 .orderBy(
51 asc(tables.sourceRecipeRevisionsTable.recipeId),
52 asc(tables.sourceRecipeRevisionsTable.revisionNumber),
53 );
54
55 return rows.map((row) => decodeStoredSourceRecipeRevisionRecord(row));
56 }),
57
58 getById: (id: StoredSourceRecipeRevisionRecord["id"]) =>
59 client.use("rows.source_recipe_revisions.get_by_id", async (db) => {
60 const rows = await db
61 .select()
62 .from(tables.sourceRecipeRevisionsTable)
63 .where(eq(tables.sourceRecipeRevisionsTable.id, id))
64 .limit(1);
65
66 const row = firstOption(rows);
67 return Option.isSome(row)
68 ? Option.some(decodeStoredSourceRecipeRevisionRecord(row.value))
69 : Option.none<StoredSourceRecipeRevisionRecord>();
70 }),
71
72 getByRecipeAndMaterializationHash: (input: {
73 recipeId: StoredSourceRecipeRevisionRecord["recipeId"];
74 materializationHash: string;
75 }) =>
76 client.use("rows.source_recipe_revisions.get_by_recipe_materialization", async (db) => {
77 const rows = await db
78 .select()
79 .from(tables.sourceRecipeRevisionsTable)
80 .where(
81 and(
82 eq(tables.sourceRecipeRevisionsTable.recipeId, input.recipeId),
83 eq(tables.sourceRecipeRevisionsTable.materializationHash, input.materializationHash),
84 ),
85 )
86 .limit(1);
87
88 const row = firstOption(rows);
89 return Option.isSome(row)
90 ? Option.some(decodeStoredSourceRecipeRevisionRecord(row.value))
91 : Option.none<StoredSourceRecipeRevisionRecord>();
92 }),
93
94 nextRevisionNumber: (recipeId: StoredSourceRecipeRevisionRecord["recipeId"]) =>
95 client.use("rows.source_recipe_revisions.next_revision_number", async (db) => {
96 const rows = await db
97 .select({
98 revisionNumber: tables.sourceRecipeRevisionsTable.revisionNumber,
99 })
100 .from(tables.sourceRecipeRevisionsTable)
101 .where(eq(tables.sourceRecipeRevisionsTable.recipeId, recipeId))
102 .orderBy(desc(tables.sourceRecipeRevisionsTable.revisionNumber))
103 .limit(1);
104
105 return Number(rows[0]?.revisionNumber ?? 0) + 1;
106 }),
107
108 upsert: (revision: StoredSourceRecipeRevisionRecord) =>
109 client.use("rows.source_recipe_revisions.upsert", async (db) => {
110 await db
111 .insert(tables.sourceRecipeRevisionsTable)
112 .values(revision)
113 .onConflictDoUpdate({
114 target: [tables.sourceRecipeRevisionsTable.id],
115 set: {
116 ...withoutCreatedAt(revision),
117 },
118 });
119 }),
120
121 update: (
122 id: StoredSourceRecipeRevisionRecord["id"],
123 patch: RecipeRevisionPatch,
124 ) =>
125 client.use("rows.source_recipe_revisions.update", async (db) => {
126 const rows = await db
127 .update(tables.sourceRecipeRevisionsTable)
128 .set(patch)
129 .where(eq(tables.sourceRecipeRevisionsTable.id, id))
130 .returning();
131
132 const row = firstOption(rows);
133 return Option.isSome(row)
134 ? Option.some(decodeStoredSourceRecipeRevisionRecord(row.value))
135 : Option.none<StoredSourceRecipeRevisionRecord>();
136 }),
137
138 removeByRecipeId: (recipeId: StoredSourceRecipeRevisionRecord["recipeId"]) =>
139 client.use("rows.source_recipe_revisions.remove_by_recipe", async (db) => {
140 const deleted = await db
141 .delete(tables.sourceRecipeRevisionsTable)
142 .where(eq(tables.sourceRecipeRevisionsTable.recipeId, recipeId))
143 .returning();
144
145 return deleted.length;
146 }),
147
148 removeById: (id: StoredSourceRecipeRevisionRecord["id"]) =>
149 client.use("rows.source_recipe_revisions.remove_by_id", async (db) => {
150 const deleted = await db
151 .delete(tables.sourceRecipeRevisionsTable)
152 .where(eq(tables.sourceRecipeRevisionsTable.id, id))
153 .returning();
154
155 return deleted.length > 0;
156 }),
157});
packages/control-plane/src/persistence/repos/source-recipe-schema-bundles-repo.ts+0−106
@@ -1,106 +0,0 @@
1import {
2 type StoredSourceRecipeSchemaBundleRecord,
3 StoredSourceRecipeSchemaBundleRecordSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { asc, eq, inArray } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeStoredSourceRecipeSchemaBundleRecord = Schema.decodeUnknownSync(
14 StoredSourceRecipeSchemaBundleRecordSchema,
15);
16
17export const createSourceRecipeSchemaBundlesRepo = (
18 client: DrizzleClient,
19 tables: DrizzleTables,
20) => ({
21 getById: (id: StoredSourceRecipeSchemaBundleRecord["id"]) =>
22 client.use("rows.source_recipe_schema_bundles.get_by_id", async (db) => {
23 const rows = await db
24 .select()
25 .from(tables.sourceRecipeSchemaBundlesTable)
26 .where(eq(tables.sourceRecipeSchemaBundlesTable.id, id))
27 .limit(1);
28
29 const row = firstOption(rows);
30 return Option.isSome(row)
31 ? Option.some(decodeStoredSourceRecipeSchemaBundleRecord(row.value))
32 : Option.none<StoredSourceRecipeSchemaBundleRecord>();
33 }),
34
35 listByRevisionId: (recipeRevisionId: StoredSourceRecipeSchemaBundleRecord["recipeRevisionId"]) =>
36 client.use("rows.source_recipe_schema_bundles.list_by_revision", async (db) => {
37 const rows = await db
38 .select()
39 .from(tables.sourceRecipeSchemaBundlesTable)
40 .where(eq(tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, recipeRevisionId))
41 .orderBy(
42 asc(tables.sourceRecipeSchemaBundlesTable.bundleKind),
43 asc(tables.sourceRecipeSchemaBundlesTable.createdAt),
44 );
45
46 return rows.map((row) => decodeStoredSourceRecipeSchemaBundleRecord(row));
47 }),
48
49 listByRevisionIds: (
50 recipeRevisionIds: readonly StoredSourceRecipeSchemaBundleRecord["recipeRevisionId"][],
51 ) =>
52 client.use("rows.source_recipe_schema_bundles.list_by_revisions", async (db) => {
53 if (recipeRevisionIds.length === 0) {
54 return [] as StoredSourceRecipeSchemaBundleRecord[];
55 }
56
57 const rows = await db
58 .select()
59 .from(tables.sourceRecipeSchemaBundlesTable)
60 .where(inArray(tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, [...recipeRevisionIds]))
61 .orderBy(
62 asc(tables.sourceRecipeSchemaBundlesTable.recipeRevisionId),
63 asc(tables.sourceRecipeSchemaBundlesTable.bundleKind),
64 asc(tables.sourceRecipeSchemaBundlesTable.createdAt),
65 );
66
67 return rows.map((row) => decodeStoredSourceRecipeSchemaBundleRecord(row));
68 }),
69
70 replaceForRevision: (input: {
71 recipeRevisionId: StoredSourceRecipeSchemaBundleRecord["recipeRevisionId"];
72 bundles: readonly StoredSourceRecipeSchemaBundleRecord[];
73 }) =>
74 client.useTx("rows.source_recipe_schema_bundles.replace_for_revision", async (tx) => {
75 await tx
76 .delete(tables.sourceRecipeSchemaBundlesTable)
77 .where(eq(tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, input.recipeRevisionId));
78
79 if (input.bundles.length > 0) {
80 await tx.insert(tables.sourceRecipeSchemaBundlesTable).values([...input.bundles]);
81 }
82 }),
83
84 upsert: (bundle: StoredSourceRecipeSchemaBundleRecord) =>
85 client.use("rows.source_recipe_schema_bundles.upsert", async (db) => {
86 await db
87 .insert(tables.sourceRecipeSchemaBundlesTable)
88 .values(bundle)
89 .onConflictDoUpdate({
90 target: [tables.sourceRecipeSchemaBundlesTable.id],
91 set: {
92 ...withoutCreatedAt(bundle),
93 },
94 });
95 }),
96
97 removeByRevisionId: (recipeRevisionId: StoredSourceRecipeSchemaBundleRecord["recipeRevisionId"]) =>
98 client.use("rows.source_recipe_schema_bundles.remove_by_revision", async (db) => {
99 const deleted = await db
100 .delete(tables.sourceRecipeSchemaBundlesTable)
101 .where(eq(tables.sourceRecipeSchemaBundlesTable.recipeRevisionId, recipeRevisionId))
102 .returning();
103
104 return deleted.length;
105 }),
106});
packages/control-plane/src/persistence/repos/source-recipes-repo.ts+0−67
@@ -1,67 +0,0 @@
1import {
2 type StoredSourceRecipeRecord,
3 StoredSourceRecipeRecordSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { asc, eq } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption, withoutCreatedAt } from "./shared";
12
13const decodeStoredSourceRecipeRecord = Schema.decodeUnknownSync(
14 StoredSourceRecipeRecordSchema,
15);
16
17export const createSourceRecipesRepo = (
18 client: DrizzleClient,
19 tables: DrizzleTables,
20) => ({
21 listByUpdatedAt: () =>
22 client.use("rows.source_recipes.list", async (db) => {
23 const rows = await db
24 .select()
25 .from(tables.sourceRecipesTable)
26 .orderBy(asc(tables.sourceRecipesTable.updatedAt), asc(tables.sourceRecipesTable.id));
27
28 return rows.map((row) => decodeStoredSourceRecipeRecord(row));
29 }),
30
31 getById: (id: StoredSourceRecipeRecord["id"]) =>
32 client.use("rows.source_recipes.get_by_id", async (db) => {
33 const rows = await db
34 .select()
35 .from(tables.sourceRecipesTable)
36 .where(eq(tables.sourceRecipesTable.id, id))
37 .limit(1);
38
39 const row = firstOption(rows);
40 return Option.isSome(row)
41 ? Option.some(decodeStoredSourceRecipeRecord(row.value))
42 : Option.none<StoredSourceRecipeRecord>();
43 }),
44
45 upsert: (recipe: StoredSourceRecipeRecord) =>
46 client.use("rows.source_recipes.upsert", async (db) => {
47 await db
48 .insert(tables.sourceRecipesTable)
49 .values(recipe)
50 .onConflictDoUpdate({
51 target: [tables.sourceRecipesTable.id],
52 set: {
53 ...withoutCreatedAt(recipe),
54 },
55 });
56 }),
57
58 removeById: (id: StoredSourceRecipeRecord["id"]) =>
59 client.use("rows.source_recipes.remove", async (db) => {
60 const deleted = await db
61 .delete(tables.sourceRecipesTable)
62 .where(eq(tables.sourceRecipesTable.id, id))
63 .returning();
64
65 return deleted.length > 0;
66 }),
67});
packages/control-plane/src/persistence/repos/sources-repo.ts+0−154
@@ -1,154 +0,0 @@
1import {
2 type StoredSourceRecord,
3 StoredSourceRecordSchema,
4} from "#schema";
5import * as Option from "effect/Option";
6import { Schema } from "effect";
7import { and, asc, count, eq } from "drizzle-orm";
8
9import type { DrizzleClient } from "../client";
10import type { DrizzleTables } from "../schema";
11import { firstOption } from "./shared";
12
13const decodeStoredSourceRecord = Schema.decodeUnknownSync(StoredSourceRecordSchema);
14const encodeStoredSourceRecord = Schema.encodeSync(StoredSourceRecordSchema);
15
16const toSourceUpdateSet = (
17 patch: Partial<Omit<StoredSourceRecord, "id" | "workspaceId" | "createdAt">>,
18): Partial<DrizzleTables["sourcesTable"]["$inferInsert"]> => {
19 const set: Partial<DrizzleTables["sourcesTable"]["$inferInsert"]> = {};
20
21 if (patch.recipeId !== undefined) set.recipeId = patch.recipeId;
22 if (patch.recipeRevisionId !== undefined) set.recipeRevisionId = patch.recipeRevisionId;
23 if (patch.name !== undefined) set.name = patch.name;
24 if (patch.kind !== undefined) set.kind = patch.kind;
25 if (patch.endpoint !== undefined) set.endpoint = patch.endpoint;
26 if (patch.status !== undefined) set.status = patch.status;
27 if (patch.enabled !== undefined) set.enabled = patch.enabled;
28 if (patch.namespace !== undefined) set.namespace = patch.namespace;
29 if (patch.importAuthPolicy !== undefined) set.importAuthPolicy = patch.importAuthPolicy;
30 if (patch.bindingConfigJson !== undefined) set.bindingConfigJson = patch.bindingConfigJson;
31 if (patch.sourceHash !== undefined) set.sourceHash = patch.sourceHash;
32 if (patch.lastError !== undefined) set.lastError = patch.lastError;
33 if (patch.updatedAt !== undefined) set.updatedAt = patch.updatedAt;
34
35 return set;
36};
37
38export const createSourcesRepo = (
39 client: DrizzleClient,
40 tables: DrizzleTables,
41) => ({
42 listAll: () =>
43 client.use("rows.sources.list_all", async (db) => {
44 const rows = await db
45 .select()
46 .from(tables.sourcesTable)
47 .orderBy(
48 asc(tables.sourcesTable.workspaceId),
49 asc(tables.sourcesTable.updatedAt),
50 asc(tables.sourcesTable.sourceId),
51 );
52
53 return rows.map((row) => decodeStoredSourceRecord(row));
54 }),
55
56 listByWorkspaceId: (workspaceId: StoredSourceRecord["workspaceId"]) =>
57 client.use("rows.sources.list_by_workspace", async (db) => {
58 const rows = await db
59 .select()
60 .from(tables.sourcesTable)
61 .where(eq(tables.sourcesTable.workspaceId, workspaceId))
62 .orderBy(asc(tables.sourcesTable.updatedAt), asc(tables.sourcesTable.sourceId));
63
64 return rows.map((row) => decodeStoredSourceRecord(row));
65 }),
66
67 getByWorkspaceAndId: (
68 workspaceId: StoredSourceRecord["workspaceId"],
69 sourceId: StoredSourceRecord["id"],
70 ) =>
71 client.use("rows.sources.get_by_workspace_and_id", async (db) => {
72 const rows = await db
73 .select()
74 .from(tables.sourcesTable)
75 .where(
76 and(
77 eq(tables.sourcesTable.workspaceId, workspaceId),
78 eq(tables.sourcesTable.sourceId, sourceId),
79 ),
80 )
81 .limit(1);
82
83 const row = firstOption(rows);
84 return Option.isSome(row)
85 ? Option.some(decodeStoredSourceRecord(row.value))
86 : Option.none<StoredSourceRecord>();
87 }),
88
89 countByRecipeId: (recipeId: StoredSourceRecord["recipeId"]) =>
90 client.use("rows.sources.count_by_recipe", async (db) => {
91 const rows = await db
92 .select({ value: count() })
93 .from(tables.sourcesTable)
94 .where(eq(tables.sourcesTable.recipeId, recipeId));
95
96 return Number(rows[0]?.value ?? 0);
97 }),
98
99 countByRecipeRevisionId: (recipeRevisionId: StoredSourceRecord["recipeRevisionId"]) =>
100 client.use("rows.sources.count_by_recipe_revision", async (db) => {
101 const rows = await db
102 .select({ value: count() })
103 .from(tables.sourcesTable)
104 .where(eq(tables.sourcesTable.recipeRevisionId, recipeRevisionId));
105
106 return Number(rows[0]?.value ?? 0);
107 }),
108
109 insert: (source: StoredSourceRecord) =>
110 client.use("rows.sources.insert", async (db) => {
111 await db.insert(tables.sourcesTable).values(encodeStoredSourceRecord(source));
112 }),
113
114 update: (
115 workspaceId: StoredSourceRecord["workspaceId"],
116 sourceId: StoredSourceRecord["id"],
117 patch: Partial<Omit<StoredSourceRecord, "id" | "workspaceId" | "createdAt">>,
118 ) =>
119 client.use("rows.sources.update", async (db) => {
120 const rows = await db
121 .update(tables.sourcesTable)
122 .set(toSourceUpdateSet(patch))
123 .where(
124 and(
125 eq(tables.sourcesTable.workspaceId, workspaceId),
126 eq(tables.sourcesTable.sourceId, sourceId),
127 ),
128 )
129 .returning();
130
131 const row = firstOption(rows);
132 return Option.isSome(row)
133 ? Option.some(decodeStoredSourceRecord(row.value))
134 : Option.none<StoredSourceRecord>();
135 }),
136
137 removeByWorkspaceAndId: (
138 workspaceId: StoredSourceRecord["workspaceId"],
139 sourceId: StoredSourceRecord["id"],
140 ) =>
141 client.useTx("rows.sources.remove", async (tx) => {
142 const deleted = await tx
143 .delete(tables.sourcesTable)
144 .where(
145 and(
146 eq(tables.sourcesTable.workspaceId, workspaceId),
147 eq(tables.sourcesTable.sourceId, sourceId),
148 ),
149 )
150 .returning();
151
152 return deleted.length > 0;
153 }),
154});
packages/control-plane/src/persistence/schema.ts+0−550
@@ -1,550 +0,0 @@
1import {
2 bigint,
3 boolean,
4 check,
5 index,
6 pgTable,
7 primaryKey,
8 text,
9 uniqueIndex,
10} from "drizzle-orm/pg-core";
11import { sql } from "drizzle-orm";
12
13export const tableNames = {
14 accounts: "accounts",
15 sources: "sources",
16 sourceRecipes: "source_recipes",
17 sourceRecipeRevisions: "source_recipe_revisions",
18 sourceRecipeDocuments: "source_recipe_documents",
19 sourceRecipeSchemaBundles: "source_recipe_schema_bundles",
20 sourceRecipeOperations: "source_recipe_operations",
21 codeMigrations: "control_plane_code_migrations",
22 authArtifacts: "workspace_source_auth_artifacts",
23 authLeases: "workspace_source_auth_leases",
24 workspaceSourceOauthClients: "workspace_source_oauth_clients",
25 secretMaterials: "secret_materials",
26 sourceAuthSessions: "source_auth_sessions",
27 executions: "executions",
28 executionInteractions: "execution_interactions",
29 executionSteps: "execution_steps",
30} as const;
31
32export const accountsTable = pgTable(
33 tableNames.accounts,
34 {
35 id: text("id").notNull().primaryKey(),
36 provider: text("provider").notNull(),
37 subject: text("subject").notNull(),
38 email: text("email"),
39 displayName: text("display_name"),
40 createdAt: bigint("created_at", { mode: "number" }).notNull(),
41 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
42 },
43 (table) => [
44 uniqueIndex("accounts_provider_subject_idx").on(table.provider, table.subject),
45 index("accounts_updated_idx").on(table.updatedAt, table.id),
46 check(
47 "accounts_provider_check",
48 sql`${table.provider} in ('local', 'workos', 'service')`,
49 ),
50 ],
51);
52
53export const sourcesTable = pgTable(
54 tableNames.sources,
55 {
56 workspaceId: text("workspace_id").notNull(),
57 sourceId: text("source_id").notNull(),
58 recipeId: text("recipe_id").notNull(),
59 recipeRevisionId: text("recipe_revision_id").notNull(),
60 name: text("name").notNull(),
61 kind: text("kind").notNull(),
62 endpoint: text("endpoint").notNull(),
63 status: text("status").notNull(),
64 enabled: boolean("enabled").notNull(),
65 namespace: text("namespace"),
66 importAuthPolicy: text("import_auth_policy").notNull(),
67 bindingConfigJson: text("binding_config_json").notNull(),
68 sourceHash: text("source_hash"),
69 lastError: text("last_error"),
70 createdAt: bigint("created_at", { mode: "number" }).notNull(),
71 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
72 },
73 (table) => [
74 primaryKey({
75 columns: [table.workspaceId, table.sourceId],
76 }),
77 index("sources_workspace_updated_idx").on(
78 table.workspaceId,
79 table.updatedAt,
80 table.sourceId,
81 ),
82 index("sources_workspace_recipe_idx").on(
83 table.workspaceId,
84 table.recipeId,
85 table.updatedAt,
86 table.sourceId,
87 ),
88 uniqueIndex("sources_workspace_name_idx").on(table.workspaceId, table.name),
89 check(
90 "sources_status_check",
91 sql`${table.status} in ('draft', 'probing', 'auth_required', 'connected', 'error')`,
92 ),
93 check(
94 "sources_import_auth_policy_check",
95 sql`${table.importAuthPolicy} in ('none', 'reuse_runtime', 'separate')`,
96 ),
97 ],
98);
99
100export const sourceRecipesTable = pgTable(
101 tableNames.sourceRecipes,
102 {
103 id: text("id").notNull().primaryKey(),
104 kind: text("kind").notNull(),
105 adapterKey: text("adapter_key").notNull(),
106 providerKey: text("provider_key").notNull(),
107 name: text("name").notNull(),
108 summary: text("summary"),
109 visibility: text("visibility").notNull(),
110 latestRevisionId: text("latest_revision_id").notNull(),
111 createdAt: bigint("created_at", { mode: "number" }).notNull(),
112 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
113 },
114 (table) => [
115 index("source_recipes_provider_updated_idx").on(
116 table.providerKey,
117 table.updatedAt,
118 table.id,
119 ),
120 index("source_recipes_visibility_updated_idx").on(
121 table.visibility,
122 table.updatedAt,
123 table.id,
124 ),
125 check(
126 "source_recipes_kind_check",
127 sql`${table.kind} in ('http_api', 'mcp', 'internal')`,
128 ),
129 check(
130 "source_recipes_visibility_check",
131 sql`${table.visibility} in ('private', 'workspace', 'organization', 'public')`,
132 ),
133 ],
134);
135
136export const sourceRecipeRevisionsTable = pgTable(
137 tableNames.sourceRecipeRevisions,
138 {
139 id: text("id").notNull().primaryKey(),
140 recipeId: text("recipe_id").notNull(),
141 revisionNumber: bigint("revision_number", { mode: "number" }).notNull(),
142 sourceConfigJson: text("source_config_json").notNull(),
143 manifestJson: text("manifest_json"),
144 manifestHash: text("manifest_hash"),
145 materializationHash: text("materialization_hash"),
146 createdAt: bigint("created_at", { mode: "number" }).notNull(),
147 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
148 },
149 (table) => [
150 uniqueIndex("source_recipe_revisions_recipe_revision_idx").on(
151 table.recipeId,
152 table.revisionNumber,
153 ),
154 uniqueIndex("source_recipe_revisions_recipe_materialization_idx").on(
155 table.recipeId,
156 table.materializationHash,
157 ),
158 index("source_recipe_revisions_recipe_manifest_idx").on(
159 table.recipeId,
160 table.manifestHash,
161 ),
162 index("source_recipe_revisions_recipe_created_idx").on(
163 table.recipeId,
164 table.createdAt,
165 table.id,
166 ),
167 ],
168);
169
170export const sourceRecipeDocumentsTable = pgTable(
171 tableNames.sourceRecipeDocuments,
172 {
173 id: text("id").notNull().primaryKey(),
174 recipeRevisionId: text("recipe_revision_id").notNull(),
175 documentKind: text("document_kind").notNull(),
176 documentKey: text("document_key").notNull(),
177 contentText: text("content_text").notNull(),
178 contentHash: text("content_hash").notNull(),
179 fetchedAt: bigint("fetched_at", { mode: "number" }),
180 createdAt: bigint("created_at", { mode: "number" }).notNull(),
181 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
182 },
183 (table) => [
184 uniqueIndex("source_recipe_documents_revision_kind_key_idx").on(
185 table.recipeRevisionId,
186 table.documentKind,
187 table.documentKey,
188 ),
189 index("source_recipe_documents_revision_created_idx").on(
190 table.recipeRevisionId,
191 table.createdAt,
192 table.id,
193 ),
194 ],
195);
196
197export const sourceRecipeSchemaBundlesTable = pgTable(
198 tableNames.sourceRecipeSchemaBundles,
199 {
200 id: text("id").notNull().primaryKey(),
201 recipeRevisionId: text("recipe_revision_id").notNull(),
202 bundleKind: text("bundle_kind").notNull(),
203 refsJson: text("refs_json").notNull(),
204 contentHash: text("content_hash").notNull(),
205 createdAt: bigint("created_at", { mode: "number" }).notNull(),
206 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
207 },
208 (table) => [
209 uniqueIndex("source_recipe_schema_bundles_revision_kind_idx").on(
210 table.recipeRevisionId,
211 table.bundleKind,
212 ),
213 index("source_recipe_schema_bundles_revision_created_idx").on(
214 table.recipeRevisionId,
215 table.createdAt,
216 table.id,
217 ),
218 ],
219);
220
221export const sourceRecipeOperationsTable = pgTable(
222 tableNames.sourceRecipeOperations,
223 {
224 id: text("id").notNull().primaryKey(),
225 recipeRevisionId: text("recipe_revision_id").notNull(),
226 operationKey: text("operation_key").notNull(),
227 transportKind: text("transport_kind").notNull(),
228 toolId: text("tool_id").notNull(),
229 title: text("title"),
230 description: text("description"),
231 operationKind: text("operation_kind").notNull(),
232 searchText: text("search_text").notNull(),
233 inputSchemaJson: text("input_schema_json"),
234 outputSchemaJson: text("output_schema_json"),
235 providerKind: text("provider_kind").notNull(),
236 providerDataJson: text("provider_data_json"),
237 createdAt: bigint("created_at", { mode: "number" }).notNull(),
238 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
239 },
240 (table) => [
241 uniqueIndex("source_recipe_operations_revision_operation_key_idx").on(
242 table.recipeRevisionId,
243 table.operationKey,
244 ),
245 index("source_recipe_operations_revision_tool_idx").on(
246 table.recipeRevisionId,
247 table.toolId,
248 table.updatedAt,
249 table.id,
250 ),
251 index("source_recipe_operations_search_text_idx").using(
252 "gin",
253 sql`to_tsvector('simple', ${table.searchText})`,
254 ),
255 check(
256 "source_recipe_operations_transport_kind_check",
257 sql`${table.transportKind} in ('http', 'graphql', 'mcp', 'internal')`,
258 ),
259 check(
260 "source_recipe_operations_kind_check",
261 sql`${table.operationKind} in ('read', 'write', 'delete', 'unknown')`,
262 ),
263 ],
264);
265
266export const codeMigrationsTable = pgTable(
267 tableNames.codeMigrations,
268 {
269 id: text("id").notNull().primaryKey(),
270 appliedAt: bigint("applied_at", { mode: "number" }).notNull(),
271 },
272);
273
274export const authArtifactsTable = pgTable(
275 tableNames.authArtifacts,
276 {
277 id: text("id").notNull().primaryKey(),
278 workspaceId: text("workspace_id").notNull(),
279 sourceId: text("source_id").notNull(),
280 actorAccountId: text("actor_account_id"),
281 slot: text("slot").notNull(),
282 artifactKind: text("artifact_kind").notNull(),
283 configJson: text("config_json").notNull(),
284 grantSetJson: text("grant_set_json"),
285 createdAt: bigint("created_at", { mode: "number" }).notNull(),
286 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
287 },
288 (table) => [
289 index("auth_artifacts_workspace_idx").on(
290 table.workspaceId,
291 table.updatedAt,
292 table.id,
293 ),
294 uniqueIndex("auth_artifacts_workspace_source_actor_idx").on(
295 table.workspaceId,
296 table.sourceId,
297 table.actorAccountId,
298 table.slot,
299 ),
300 index("auth_artifacts_workspace_source_idx").on(
301 table.workspaceId,
302 table.sourceId,
303 table.updatedAt,
304 table.id,
305 ),
306 check(
307 "auth_artifacts_slot_check",
308 sql`${table.slot} in ('runtime', 'import')`,
309 ),
310 ],
311);
312
313export const authLeasesTable = pgTable(
314 tableNames.authLeases,
315 {
316 id: text("id").notNull().primaryKey(),
317 authArtifactId: text("auth_artifact_id").notNull(),
318 workspaceId: text("workspace_id").notNull(),
319 sourceId: text("source_id").notNull(),
320 actorAccountId: text("actor_account_id"),
321 slot: text("slot").notNull(),
322 placementsTemplateJson: text("placements_template_json").notNull(),
323 expiresAt: bigint("expires_at", { mode: "number" }),
324 refreshAfter: bigint("refresh_after", { mode: "number" }),
325 createdAt: bigint("created_at", { mode: "number" }).notNull(),
326 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
327 },
328 (table) => [
329 uniqueIndex("auth_leases_auth_artifact_idx").on(table.authArtifactId),
330 index("auth_leases_workspace_source_idx").on(
331 table.workspaceId,
332 table.sourceId,
333 table.actorAccountId,
334 table.slot,
335 table.updatedAt,
336 table.id,
337 ),
338 check(
339 "auth_leases_slot_check",
340 sql`${table.slot} in ('runtime', 'import')`,
341 ),
342 ],
343);
344
345export const workspaceSourceOauthClientsTable = pgTable(
346 tableNames.workspaceSourceOauthClients,
347 {
348 id: text("id").notNull().primaryKey(),
349 workspaceId: text("workspace_id").notNull(),
350 sourceId: text("source_id").notNull(),
351 providerKey: text("provider_key").notNull(),
352 clientId: text("client_id").notNull(),
353 clientSecretProviderId: text("client_secret_provider_id"),
354 clientSecretHandle: text("client_secret_handle"),
355 clientMetadataJson: text("client_metadata_json"),
356 createdAt: bigint("created_at", { mode: "number" }).notNull(),
357 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
358 },
359 (table) => [
360 uniqueIndex("workspace_source_oauth_clients_workspace_source_provider_idx").on(
361 table.workspaceId,
362 table.sourceId,
363 table.providerKey,
364 ),
365 index("workspace_source_oauth_clients_workspace_idx").on(
366 table.workspaceId,
367 table.updatedAt,
368 table.sourceId,
369 ),
370 ],
371);
372
373export const secretMaterialsTable = pgTable(
374 tableNames.secretMaterials,
375 {
376 id: text("id").notNull().primaryKey(),
377 name: text("name"),
378 purpose: text("purpose").notNull(),
379 value: text("value").notNull(),
380 createdAt: bigint("created_at", { mode: "number" }).notNull(),
381 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
382 },
383 (table) => [
384 index("secret_materials_updated_idx").on(table.updatedAt, table.id),
385 check(
386 "secret_materials_purpose_check",
387 sql`${table.purpose} in ('auth_material', 'oauth_access_token', 'oauth_refresh_token', 'oauth_client_info')`,
388 ),
389 ],
390);
391
392export const sourceAuthSessionsTable = pgTable(
393 tableNames.sourceAuthSessions,
394 {
395 id: text("id").notNull().primaryKey(),
396 workspaceId: text("workspace_id").notNull(),
397 sourceId: text("source_id").notNull(),
398 actorAccountId: text("actor_account_id"),
399 credentialSlot: text("credential_slot").notNull(),
400 executionId: text("execution_id"),
401 interactionId: text("interaction_id"),
402 providerKind: text("provider_kind").notNull(),
403 status: text("status").notNull(),
404 state: text("state").notNull(),
405 sessionDataJson: text("session_data_json").notNull(),
406 errorText: text("error_text"),
407 completedAt: bigint("completed_at", { mode: "number" }),
408 createdAt: bigint("created_at", { mode: "number" }).notNull(),
409 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
410 },
411 (table) => [
412 index("source_auth_sessions_workspace_idx").on(
413 table.workspaceId,
414 table.updatedAt,
415 table.id,
416 ),
417 index("source_auth_sessions_pending_idx").on(
418 table.workspaceId,
419 table.sourceId,
420 table.actorAccountId,
421 table.credentialSlot,
422 table.status,
423 table.updatedAt,
424 table.id,
425 ),
426 uniqueIndex("source_auth_sessions_state_idx").on(table.state),
427 check(
428 "source_auth_sessions_provider_kind_check",
429 sql`${table.providerKind} in ('mcp_oauth', 'oauth2_pkce')`,
430 ),
431 check(
432 "source_auth_sessions_status_check",
433 sql`${table.status} in ('pending', 'completed', 'failed', 'cancelled')`,
434 ),
435 check(
436 "source_auth_sessions_credential_slot_check",
437 sql`${table.credentialSlot} in ('runtime', 'import')`,
438 ),
439 ],
440);
441
442export const executionsTable = pgTable(
443 tableNames.executions,
444 {
445 id: text("id").notNull().primaryKey(),
446 workspaceId: text("workspace_id").notNull(),
447 createdByAccountId: text("created_by_account_id").notNull(),
448 status: text("status").notNull(),
449 code: text("code").notNull(),
450 resultJson: text("result_json"),
451 errorText: text("error_text"),
452 logsJson: text("logs_json"),
453 startedAt: bigint("started_at", { mode: "number" }),
454 completedAt: bigint("completed_at", { mode: "number" }),
455 createdAt: bigint("created_at", { mode: "number" }).notNull(),
456 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
457 },
458 (table) => [
459 index("executions_workspace_idx").on(table.workspaceId, table.updatedAt, table.id),
460 check(
461 "executions_status_check",
462 sql`${table.status} in ('pending', 'running', 'waiting_for_interaction', 'completed', 'failed', 'cancelled')`,
463 ),
464 ],
465);
466
467export const executionInteractionsTable = pgTable(
468 tableNames.executionInteractions,
469 {
470 id: text("id").notNull().primaryKey(),
471 executionId: text("execution_id").notNull(),
472 status: text("status").notNull(),
473 kind: text("kind").notNull(),
474 purpose: text("purpose").notNull(),
475 payloadJson: text("payload_json").notNull(),
476 responseJson: text("response_json"),
477 responsePrivateJson: text("response_private_json"),
478 createdAt: bigint("created_at", { mode: "number" }).notNull(),
479 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
480 },
481 (table) => [
482 index("execution_interactions_execution_idx").on(
483 table.executionId,
484 table.updatedAt,
485 table.id,
486 ),
487 check(
488 "execution_interactions_status_check",
489 sql`${table.status} in ('pending', 'resolved', 'cancelled')`,
490 ),
491 ],
492);
493
494export const executionStepsTable = pgTable(
495 tableNames.executionSteps,
496 {
497 id: text("id").notNull().primaryKey(),
498 executionId: text("execution_id").notNull(),
499 sequence: bigint("sequence", { mode: "number" }).notNull(),
500 kind: text("kind").notNull(),
501 status: text("status").notNull(),
502 path: text("path").notNull(),
503 argsJson: text("args_json").notNull(),
504 resultJson: text("result_json"),
505 errorText: text("error_text"),
506 interactionId: text("interaction_id"),
507 createdAt: bigint("created_at", { mode: "number" }).notNull(),
508 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
509 },
510 (table) => [
511 uniqueIndex("execution_steps_execution_sequence_idx").on(
512 table.executionId,
513 table.sequence,
514 ),
515 index("execution_steps_execution_updated_idx").on(
516 table.executionId,
517 table.updatedAt,
518 table.id,
519 ),
520 check(
521 "execution_steps_kind_check",
522 sql`${table.kind} in ('tool_call')`,
523 ),
524 check(
525 "execution_steps_status_check",
526 sql`${table.status} in ('pending', 'waiting', 'completed', 'failed')`,
527 ),
528 ],
529);
530
531export const drizzleSchema = {
532 accountsTable,
533 sourcesTable,
534 sourceRecipesTable,
535 sourceRecipeRevisionsTable,
536 sourceRecipeDocumentsTable,
537 sourceRecipeSchemaBundlesTable,
538 sourceRecipeOperationsTable,
539 codeMigrationsTable,
540 authArtifactsTable,
541 authLeasesTable,
542 workspaceSourceOauthClientsTable,
543 secretMaterialsTable,
544 sourceAuthSessionsTable,
545 executionsTable,
546 executionInteractionsTable,
547 executionStepsTable,
548};
549
550export type DrizzleTables = typeof drizzleSchema;
packages/control-plane/src/persistence/sql-runtime.ts+0−206
@@ -1,206 +0,0 @@
1import { PGlite } from "@electric-sql/pglite";
2import { drizzle as drizzlePGlite } from "drizzle-orm/pglite";
3import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";
4import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
5import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
6import { existsSync, readdirSync, readFileSync } from "node:fs";
7import { mkdir, unlink } from "node:fs/promises";
8import * as path from "node:path";
9import { fileURLToPath } from "node:url";
10import postgres from "postgres";
11
12import { drizzleSchema } from "./schema";
13
14export type SqlBackend = "pglite" | "postgres";
15
16export type CreateSqlRuntimeOptions = {
17 databaseUrl?: string;
18 localDataDir?: string;
19 postgresApplicationName?: string;
20 migrationsFolder?: string;
21};
22
23const createPGliteDb = (client: PGlite) =>
24 drizzlePGlite({ client, schema: drizzleSchema });
25
26const createPostgresDb = (client: postgres.Sql) =>
27 drizzlePostgres({ client, schema: drizzleSchema });
28
29export type PGliteDb = ReturnType<typeof createPGliteDb>;
30export type PostgresDb = ReturnType<typeof createPostgresDb>;
31export type DrizzleDb = PGliteDb | PostgresDb;
32
33export type SqlRuntime = {
34 backend: SqlBackend;
35 db: DrizzleDb;
36 close: () => Promise<void>;
37};
38
39const trim = (value: string | undefined): string | undefined => {
40 const candidate = value?.trim();
41 return candidate && candidate.length > 0 ? candidate : undefined;
42};
43
44const isPostgresUrl = (value: string): boolean =>
45 value.startsWith("postgres://") || value.startsWith("postgresql://");
46
47/**
48 * Remove a stale PGlite `postmaster.pid` lock file if present.
49 *
50 * PGlite writes this file on open and removes it on close. When a process
51 * exits without calling `PGlite.close()` (e.g. the Vite dev server is
52 * killed with SIGKILL, or the terminal is closed), the lock file is left
53 * behind and prevents subsequent PGlite instances from opening the
54 * database. PGlite uses a synthetic PID (e.g. `-42`) that is never a real
55 * OS process, so we cannot use it to detect a live owner. Instead we rely
56 * on the fact that in the local single-user context only one process
57 * should ever own this database at a time.
58 */
59const cleanupStalePGliteLock = async (dataDir: string): Promise<void> => {
60 const lockPath = path.join(dataDir, "postmaster.pid");
61 if (!existsSync(lockPath)) {
62 return;
63 }
64
65 // PGlite's synthetic PID is always negative (e.g. -42). A real Postgres
66 // postmaster would write a positive PID. If we ever see a positive PID
67 // we leave the file alone.
68 try {
69 const content = readFileSync(lockPath, "utf-8");
70 const firstLine = content.split("\n")[0]?.trim();
71 const pid = Number(firstLine);
72 if (!Number.isNaN(pid) && pid > 0) {
73 // Looks like a real Postgres PID — don't touch it.
74 return;
75 }
76 } catch {
77 // If we can't read the file, try to remove it anyway.
78 }
79
80 try {
81 await unlink(lockPath);
82 } catch {
83 // Best-effort cleanup; PGlite will report the real error if this fails.
84 }
85};
86
87const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {
88 const normalized = trim(localDataDir) ?? ".executor/control-plane-pgdata";
89
90 let client: PGlite;
91 if (normalized === ":memory:") {
92 client = new PGlite();
93 } else {
94 const resolvedDataDir = path.resolve(normalized);
95 if (!existsSync(resolvedDataDir)) {
96 await mkdir(resolvedDataDir, { recursive: true });
97 }
98 await cleanupStalePGliteLock(resolvedDataDir);
99 client = new PGlite(resolvedDataDir);
100 }
101
102 const db = createPGliteDb(client);
103
104 return {
105 backend: "pglite",
106 db,
107 close: async () => {
108 await client.close();
109 },
110 };
111};
112
113const createPostgresRuntime = async (
114 databaseUrl: string,
115 applicationName: string | undefined,
116): Promise<SqlRuntime> => {
117 const client = postgres(databaseUrl, {
118 prepare: false,
119 max: 10,
120 ...(applicationName
121 ? { connection: { application_name: applicationName } }
122 : {}),
123 });
124 const db = createPostgresDb(client);
125
126 return {
127 backend: "postgres",
128 db,
129 close: async () => {
130 await client.end({ timeout: 5 });
131 },
132 };
133};
134
135const resolveMigrationsFolder = (explicit: string | undefined): string => {
136 const explicitTrimmed = trim(explicit);
137 if (explicitTrimmed) {
138 return path.resolve(explicitTrimmed);
139 }
140
141 const moduleDir = path.dirname(fileURLToPath(import.meta.url));
142 const packageRoot = path.resolve(moduleDir, "../..");
143 const cwd = process.cwd();
144 const candidates = [
145 path.resolve(packageRoot, "drizzle"),
146 path.resolve(cwd, "packages/control-plane/drizzle"),
147 path.resolve(cwd, "drizzle"),
148 ];
149
150 const hasModernMigrations = (candidate: string): boolean => {
151 if (!existsSync(candidate)) {
152 return false;
153 }
154
155 const entries = readdirSync(candidate, { withFileTypes: true });
156 return entries.some(
157 (entry) =>
158 entry.isDirectory()
159 && existsSync(path.join(candidate, entry.name, "migration.sql")),
160 );
161 };
162
163 for (const candidate of candidates) {
164 if (
165 existsSync(path.join(candidate, "meta", "_journal.json"))
166 || hasModernMigrations(candidate)
167 ) {
168 return candidate;
169 }
170 }
171
172 throw new Error(
173 "Unable to resolve Drizzle migrations folder for control-plane persistence",
174 );
175};
176
177export const runMigrations = async (
178 runtime: SqlRuntime,
179 options?: { migrationsFolder?: string },
180): Promise<void> => {
181 const migrationsFolder = resolveMigrationsFolder(options?.migrationsFolder);
182
183 if (runtime.backend === "pglite") {
184 await migratePGlite(runtime.db as PGliteDb, { migrationsFolder });
185 return;
186 }
187
188 await migratePostgres(runtime.db as PostgresDb, { migrationsFolder });
189};
190
191export const createSqlRuntime = async (
192 options: CreateSqlRuntimeOptions,
193): Promise<SqlRuntime> => {
194 const databaseUrl = trim(options.databaseUrl);
195 const runtime =
196 databaseUrl && isPostgresUrl(databaseUrl)
197 ? await createPostgresRuntime(
198 databaseUrl,
199 trim(options.postgresApplicationName),
200 )
201 : await createPGliteRuntime(
202 options.localDataDir ?? ".executor/control-plane-pgdata",
203 );
204
205 return runtime;
206};
packages/control-plane/src/runtime/auth-leases.ts+6−6
@@ -1,4 +1,3 @@
1import type { SqlControlPlaneRows } from "#persistence";
2import type {1import type {
3 AuthArtifact,2 AuthArtifact,
4 AuthLease,3 AuthLease,
@@ -14,6 +13,7 @@ import { · 9 unmodified lines
14import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
15import * as Option from "effect/Option";14import * as Option from "effect/Option";
16 15
16import type { ControlPlaneStoreShape } from "./store";
17import { resolveAuthArtifactMaterial, type ResolvedSourceAuthMaterial } from "./auth-artifacts";17import { resolveAuthArtifactMaterial, type ResolvedSourceAuthMaterial } from "./auth-artifacts";
18import {18import {
19 createDefaultSecretMaterialDeleter,19 createDefaultSecretMaterialDeleter,
@@ -33,7 +33,7 @@ const secretRefKey = (ref: SecretRef): string => · 13 unmodified lines
33 `${ref.providerId}:${ref.handle}`;33 `${ref.providerId}:${ref.handle}`;
34 34
35const cleanupAuthLeaseSecretRefs = (35const cleanupAuthLeaseSecretRefs = (
36 rows: SqlControlPlaneRows,36 rows: ControlPlaneStoreShape,
37 input: {37 input: {
38 previous: Pick<AuthLease, "placementsTemplateJson"> | null;38 previous: Pick<AuthLease, "placementsTemplateJson"> | null;
39 next: Pick<AuthLease, "placementsTemplateJson"> | null;39 next: Pick<AuthLease, "placementsTemplateJson"> | null;
@@ -76,7 +76,7 @@ const leaseIsFresh = (lease: AuthLease | null, now: number): boolean => { · 36 unmodified lines
76};76};
77 77
78const refreshRefreshableOauth2AuthorizedUserArtifact = (input: {78const refreshRefreshableOauth2AuthorizedUserArtifact = (input: {
79 rows: SqlControlPlaneRows;79 rows: ControlPlaneStoreShape;
80 artifact: AuthArtifact;80 artifact: AuthArtifact;
81 lease: AuthLease | null;81 lease: AuthLease | null;
82 resolveSecretMaterial: ResolveSecretMaterial;82 resolveSecretMaterial: ResolveSecretMaterial;
@@ -168,7 +168,7 @@ const refreshRefreshableOauth2AuthorizedUserArtifact = (input: { · 85 unmodified lines
168 return nextLease;168 return nextLease;
169 });169 });
170 170
171export const removeAuthLeaseAndSecrets = (rows: SqlControlPlaneRows, input: {171export const removeAuthLeaseAndSecrets = (rows: ControlPlaneStoreShape, input: {
172 authArtifactId: AuthArtifact["id"];172 authArtifactId: AuthArtifact["id"];
173}): Effect.Effect<void, Error, never> =>173}): Effect.Effect<void, Error, never> =>
174 Effect.gen(function* () {174 Effect.gen(function* () {
@@ -185,7 +185,7 @@ export const removeAuthLeaseAndSecrets = (rows: SqlControlPlaneRows, input: { · 10 unmodified lines
185 });185 });
186 186
187export const upsertOauth2AuthorizedUserLeaseFromTokenResponse = (input: {187export const upsertOauth2AuthorizedUserLeaseFromTokenResponse = (input: {
188 rows: SqlControlPlaneRows;188 rows: ControlPlaneStoreShape;
189 artifact: AuthArtifact;189 artifact: AuthArtifact;
190 tokenResponse: OAuth2TokenResponse;190 tokenResponse: OAuth2TokenResponse;
191}): Effect.Effect<void, Error, never> =>191}): Effect.Effect<void, Error, never> =>
@@ -256,7 +256,7 @@ export const upsertOauth2AuthorizedUserLeaseFromTokenResponse = (input: { · 64 unmodified lines
256 });256 });
257 257
258export const resolveAuthArtifactMaterialWithLeases = (input: {258export const resolveAuthArtifactMaterialWithLeases = (input: {
259 rows: SqlControlPlaneRows;259 rows: ControlPlaneStoreShape;
260 artifact: AuthArtifact | null;260 artifact: AuthArtifact | null;
261 resolveSecretMaterial: ResolveSecretMaterial;261 resolveSecretMaterial: ResolveSecretMaterial;
262 context?: SecretMaterialResolveContext;262 context?: SecretMaterialResolveContext;
packages/control-plane/src/runtime/control-plane-runtime.test.ts+1−1
@@ -319,7 +319,7 @@ describe("control-plane-runtime", () => { · 318 unmodified lines
319 decodedCredentialSelection.authKind === "bearer"319 decodedCredentialSelection.authKind === "bearer"
320 ? decodedCredentialSelection.tokenRef.providerId320 ? decodedCredentialSelection.tokenRef.providerId
321 : null,321 : null,
322 ).toBe("postgres");322 ).toBe("local");
323 323
324 const tokenSecretMaterialId = SecretMaterialIdSchema.make(324 const tokenSecretMaterialId = SecretMaterialIdSchema.make(
325 decodedCredentialSelection.authKind === "bearer"325 decodedCredentialSelection.authKind === "bearer"
packages/control-plane/src/runtime/execution-replay.test.ts+0−271
@@ -1,271 +0,0 @@
1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4
5import { makeToolInvokerFromTools } from "@executor/codemode-core";
6import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess";
7
8import {
9 createSqlControlPlaneRuntime,
10 type ResolveExecutionEnvironment,
11} from "./index";
12import { withControlPlaneClient } from "./test-http-client";
13
14const gatedEchoElicitation = {
15 message: "Approve gated echo?",
16 requestedSchema: {
17 type: "object",
18 properties: {
19 approve: {
20 type: "boolean",
21 title: "Approve",
22 },
23 },
24 required: ["approve"],
25 },
26} as const;
27
28const makeReplayResolver = (countedCalls: { value: number }): ResolveExecutionEnvironment =>
29 ({ onElicitation }) =>
30 Effect.succeed({
31 executor: makeDenoSubprocessExecutor(),
32 toolInvoker: makeToolInvokerFromTools({
33 onElicitation,
34 tools: {
35 "counted.add": {
36 description: "Adds numbers and counts calls",
37 inputSchema: Schema.standardSchemaV1(
38 Schema.Struct({
39 a: Schema.optional(Schema.Number),
40 b: Schema.optional(Schema.Number),
41 }),
42 ),
43 execute: ({ a, b }: { a?: number; b?: number }) => {
44 countedCalls.value += 1;
45 return { sum: (a ?? 0) + (b ?? 0) };
46 },
47 },
48 "gated.echo": {
49 tool: {
50 description: "Echoes only after approval",
51 inputSchema: Schema.standardSchemaV1(
52 Schema.Struct({
53 value: Schema.String,
54 approve: Schema.optional(Schema.Boolean),
55 }),
56 ),
57 execute: ({ value, approve }: { value: string; approve?: boolean }) => ({
58 text: approve === true ? `approved:${value}` : `denied:${value}`,
59 }),
60 },
61 metadata: {
62 interaction: "required",
63 elicitation: gatedEchoElicitation,
64 },
65 },
66 },
67 }),
68 });
69
70const createRuntime = (countedCalls: { value: number }) =>
71 Effect.acquireRelease(
72 createSqlControlPlaneRuntime({
73 localDataDir: ":memory:",
74 executionResolver: makeReplayResolver(countedCalls),
75 }),
76 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
77 );
78
79describe("execution replay", () => {
80 it.scoped("replays completed proxy calls instead of re-executing them", () =>
81 Effect.gen(function* () {
82 const countedCalls = { value: 0 };
83 const runtime = yield* createRuntime(countedCalls);
84 const installation = runtime.localInstallation;
85
86 const created = yield* withControlPlaneClient(
87 {
88 runtime,
89 accountId: installation.accountId,
90 },
91 (client) =>
92 client.executions.create({
93 path: {
94 workspaceId: installation.workspaceId,
95 },
96 payload: {
97 code: [
98 "const sum = await tools.counted.add({ a: 20, b: 22 });",
99 'const echo = await tools.gated.echo({ value: "from-replay" });',
100 "return { sum, echo };",
101 ].join("\n"),
102 },
103 }),
104 );
105
106 expect(created.execution.status).toBe("waiting_for_interaction");
107 expect(countedCalls.value).toBe(1);
108
109 const resumed = yield* withControlPlaneClient(
110 {
111 runtime,
112 accountId: installation.accountId,
113 },
114 (client) =>
115 client.executions.resume({
116 path: {
117 workspaceId: installation.workspaceId,
118 executionId: created.execution.id,
119 },
120 payload: {
121 responseJson: JSON.stringify({
122 action: "accept",
123 content: {
124 approve: true,
125 },
126 }),
127 },
128 }),
129 );
130
131 expect(resumed.execution.status).toBe("completed");
132 expect(countedCalls.value).toBe(1);
133 expect(resumed.execution.resultJson).toBe(
134 JSON.stringify({
135 sum: { sum: 42 },
136 echo: { text: "approved:from-replay" },
137 }),
138 );
139
140 const completedSteps = yield* runtime.persistence.rows.executionSteps.listByExecutionId(
141 created.execution.id,
142 );
143 expect(completedSteps).toHaveLength(0);
144 }),
145 );
146
147 it.scoped("keeps live form interactions in the same run when requested", () =>
148 Effect.gen(function* () {
149 const countedCalls = { value: 0 };
150 const runtime = yield* createRuntime(countedCalls);
151 const installation = runtime.localInstallation;
152
153 const created = yield* withControlPlaneClient(
154 {
155 runtime,
156 accountId: installation.accountId,
157 },
158 (client) =>
159 client.executions.create({
160 path: {
161 workspaceId: installation.workspaceId,
162 },
163 payload: {
164 interactionMode: "live_form",
165 code: [
166 "const nonce = String(Math.random());",
167 "return await tools.gated.echo({ value: nonce });",
168 ].join("\n"),
169 },
170 }),
171 );
172
173 expect(created.execution.status).toBe("waiting_for_interaction");
174
175 const resumed = yield* withControlPlaneClient(
176 {
177 runtime,
178 accountId: installation.accountId,
179 },
180 (client) =>
181 client.executions.resume({
182 path: {
183 workspaceId: installation.workspaceId,
184 executionId: created.execution.id,
185 },
186 payload: {
187 interactionMode: "live_form",
188 responseJson: JSON.stringify({
189 action: "accept",
190 content: {
191 approve: true,
192 },
193 }),
194 },
195 }),
196 );
197
198 expect(resumed.execution.status).toBe("completed");
199 expect(resumed.execution.resultJson).toContain("approved:");
200 }),
201 );
202
203 it.scoped("fails loudly when replayed code reaches a different proxy call", () =>
204 Effect.gen(function* () {
205 const countedCalls = { value: 0 };
206 const runtime = yield* createRuntime(countedCalls);
207 const installation = runtime.localInstallation;
208
209 const created = yield* withControlPlaneClient(
210 {
211 runtime,
212 accountId: installation.accountId,
213 },
214 (client) =>
215 client.executions.create({
216 path: {
217 workspaceId: installation.workspaceId,
218 },
219 payload: {
220 code: [
221 "await tools.counted.add({ a: 1, b: 2 });",
222 'return await tools.gated.echo({ value: "mismatch" });',
223 ].join("\n"),
224 },
225 }),
226 );
227
228 expect(created.execution.status).toBe("waiting_for_interaction");
229 expect(countedCalls.value).toBe(1);
230
231 yield* runtime.persistence.rows.executions.update(created.execution.id, {
232 code: [
233 "await tools.counted.add({ a: 1, b: 999 });",
234 'return await tools.gated.echo({ value: "mismatch" });',
235 ].join("\n"),
236 updatedAt: Date.now(),
237 });
238
239 const resumed = yield* withControlPlaneClient(
240 {
241 runtime,
242 accountId: installation.accountId,
243 },
244 (client) =>
245 client.executions.resume({
246 path: {
247 workspaceId: installation.workspaceId,
248 executionId: created.execution.id,
249 },
250 payload: {
251 responseJson: JSON.stringify({
252 action: "accept",
253 content: {
254 approve: true,
255 },
256 }),
257 },
258 }),
259 );
260
261 expect(resumed.execution.status).toBe("failed");
262 expect(resumed.execution.errorText).toContain("Durable execution mismatch");
263 expect(countedCalls.value).toBe(1);
264
265 const failedSteps = yield* runtime.persistence.rows.executionSteps.listByExecutionId(
266 created.execution.id,
267 );
268 expect(failedSteps).toHaveLength(0);
269 }),
270 );
271});
packages/control-plane/src/runtime/executor-tools.ts+21−13
@@ -18,25 +18,33 @@ import { · 17 unmodified lines
18import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
19import * as Cause from "effect/Cause";19import * as Cause from "effect/Cause";
20import * as Exit from "effect/Exit";20import * as Exit from "effect/Exit";
21import * as Layer from "effect/Layer";
21import * as Schema from "effect/Schema";22import * as Schema from "effect/Schema";
22import type { RuntimeLocalWorkspaceState } from "./local-runtime-context";23import type { RuntimeLocalWorkspaceState } from "./local-runtime-context";
23import { RuntimeLocalWorkspaceService } from "./local-runtime-context";24import {
25 InstallationStore,
26 type LocalStorageServices,
27 LocalInstallationStore,
28 LocalSourceArtifactStore,
29 LocalWorkspaceConfigStore,
30 LocalWorkspaceStateStore,
31 SourceArtifactStore,
32 WorkspaceConfigStore,
33 WorkspaceStateStore,
34} from "./local-storage";
24 35
25/** Run an Effect as a Promise, preserving the original error (not FiberFailure). */36/** Run an Effect as a Promise, preserving the original error (not FiberFailure). */
26const runEffect = async <A>(37const runEffect = async <A>(
27 effect: Effect.Effect<A, unknown, never>,38 effect: Effect.Effect<A, unknown, LocalStorageServices>,
28 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null = null,39 _runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null = null,
29): Promise<A> => {40): Promise<A> => {
30 const provided =41 const baseLayer = Layer.mergeAll(
31 runtimeLocalWorkspace === null42 Layer.succeed(InstallationStore, LocalInstallationStore),
32 ? effect43 Layer.succeed(WorkspaceConfigStore, LocalWorkspaceConfigStore),
33 : effect.pipe(44 Layer.succeed(WorkspaceStateStore, LocalWorkspaceStateStore),
34 Effect.provideService(45 Layer.succeed(SourceArtifactStore, LocalSourceArtifactStore),
35 RuntimeLocalWorkspaceService,46 );
36 runtimeLocalWorkspace,47 const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(baseLayer)));
37 ),
38 );
39 const exit = await Effect.runPromiseExit(provided);
40 if (Exit.isSuccess(exit)) return exit.value;48 if (Exit.isSuccess(exit)) return exit.value;
41 throw Cause.squash(exit.cause);49 throw Cause.squash(exit.cause);
42};50};
packages/control-plane/src/runtime/index.ts+90−102
@@ -1,19 +1,9 @@
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";2import * as Exit from "effect/Exit";
4import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
5import * as Scope from "effect/Scope";4import * as Scope from "effect/Scope";
6 5
7import { type ControlPlaneApiRuntimeContext } from "#api";6import { type ControlPlaneApiRuntimeContext } from "#api";
8import {
9 SqlControlPlanePersistenceLive,
10 SqlControlPlanePersistenceService,
11 SqlControlPlaneRowsLive,
12 SqlControlPlaneRowsService,
13 SqlPersistenceBootstrapError,
14 type CreateSqlRuntimeOptions,
15 type SqlControlPlanePersistence,
16} from "#persistence";
17import type { LocalInstallation } from "#schema";7import type { LocalInstallation } from "#schema";
18 8
19import { type ResolveExecutionEnvironment } from "./execution-state";9import { type ResolveExecutionEnvironment } from "./execution-state";
@@ -22,14 +12,33 @@ import { · 2 unmodified lines
22 LiveExecutionManagerLive,12 LiveExecutionManagerLive,
23 LiveExecutionManagerService,13 LiveExecutionManagerService,
24} from "./live-execution";14} from "./live-execution";
25import { getOrProvisionLocalInstallation } from "./local-installation";
26import {15import {
27 loadLocalExecutorConfig,16 createLocalControlPlanePersistence,
17 type LocalControlPlanePersistence,
18} from "./local-control-plane-store";
19import {
28 resolveLocalWorkspaceContext,20 resolveLocalWorkspaceContext,
29} from "./local-config";21} from "./local-config";
30import { RuntimeLocalWorkspaceService } from "./local-runtime-context";22import {
23 InstallationStore,
24 LocalInstallationStore,
25 LocalInstallationStoreLive,
26 LocalSourceArtifactStore,
27 LocalSourceArtifactStoreLive,
28 SourceArtifactStore,
29 LocalWorkspaceConfigStore,
30 LocalWorkspaceConfigStoreLive,
31 LocalWorkspaceStateStore,
32 LocalWorkspaceStateStoreLive,
33 WorkspaceConfigStore,
34 WorkspaceStateStore,
35} from "./local-storage";
36import {
37 type RuntimeLocalWorkspaceState,
38 RuntimeLocalWorkspaceService,
39} from "./local-runtime-context";
31import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";40import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";
32import { ControlPlaneStore, ControlPlaneStoreLive } from "./store";41import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
33import {42import {
34 createRuntimeSourceAuthService,43 createRuntimeSourceAuthService,
35 RuntimeSourceAuthServiceLive,44 RuntimeSourceAuthServiceLive,
@@ -48,6 +57,7 @@ export * from "./executor-tools"; · 12 unmodified lines
48export * from "./live-execution";57export * from "./live-execution";
49export * from "./local-config";58export * from "./local-config";
50export * from "./local-installation";59export * from "./local-installation";
60export * from "./local-storage";
51export * from "./local-source-artifacts";61export * from "./local-source-artifacts";
52export * from "./local-tools";62export * from "./local-tools";
53export * from "./schema-type-signature";63export * from "./schema-type-signature";
@@ -73,12 +83,9 @@ const detailsFromCause = (cause: unknown): string => · 19 unmodified lines
73 83
74const toLocalRuntimeBootstrapError = (84const toLocalRuntimeBootstrapError = (
75 cause: unknown,85 cause: unknown,
76): SqlPersistenceBootstrapError => {86): Error => {
77 const details = detailsFromCause(cause);87 const details = detailsFromCause(cause);
78 return new SqlPersistenceBootstrapError({88 return new Error(`Failed initializing local runtime: ${details}`);
79 message: `Failed initializing local runtime: ${details}`,
80 details,
81 });
82};89};
83 90
84const closeScope = (scope: Scope.CloseableScope) =>91const closeScope = (scope: Scope.CloseableScope) =>
@@ -90,88 +97,57 @@ export type RuntimeControlPlaneLayer = Layer.Layer< · 5 unmodified lines
90 never97 never
91>;98>;
92 99
93const runtimeContextTags = [
94 ControlPlaneStore,
95 LiveExecutionManagerService,
96 RuntimeLocalWorkspaceService,
97 RuntimeSourceAuthServiceTag,
98 RuntimeExecutionResolverService,
99] as const;
100
101const createRuntimeLayerFromContext = (
102 context: Context.Context<
103 | ControlPlaneStore
104 | LiveExecutionManagerService
105 | RuntimeLocalWorkspaceService
106 | RuntimeSourceAuthServiceTag
107 | RuntimeExecutionResolverService
108 >,
109): RuntimeControlPlaneLayer => {
110 const runtimeContext = context.pipe(
111 Context.pick(...runtimeContextTags),
112 ) as Context.Context<
113 | ControlPlaneStore
114 | LiveExecutionManagerService
115 | RuntimeLocalWorkspaceService
116 | RuntimeSourceAuthServiceTag
117 | RuntimeExecutionResolverService
118 >;
119
120 return Layer.succeedContext(runtimeContext) as RuntimeControlPlaneLayer;
121};
122
123export const createRuntimeControlPlaneLayer = (100export const createRuntimeControlPlaneLayer = (
124 options: RuntimeControlPlaneOptions = {},101 input: RuntimeControlPlaneOptions & {
102 store: ControlPlaneStoreShape;
103 localWorkspaceState: RuntimeLocalWorkspaceState;
104 },
125) => {105) => {
126 const liveExecutionManagerLayer = LiveExecutionManagerLive;106 const baseLayer = Layer.mergeAll(
107 Layer.succeed(ControlPlaneStore, input.store),
108 Layer.succeed(RuntimeLocalWorkspaceService, input.localWorkspaceState),
109 LocalInstallationStoreLive,
110 LocalWorkspaceConfigStoreLive,
111 LocalWorkspaceStateStoreLive,
112 LocalSourceArtifactStoreLive,
113 LiveExecutionManagerLive,
114 );
115
127 const sourceAuthLayer = RuntimeSourceAuthServiceLive({116 const sourceAuthLayer = RuntimeSourceAuthServiceLive({
128 getLocalServerBaseUrl: options.getLocalServerBaseUrl,117 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
129 }).pipe(Layer.provide(liveExecutionManagerLayer));118 localConfig: input.localWorkspaceState.loadedConfig.config,
119 workspaceRoot: input.localWorkspaceState.context.workspaceRoot,
120 localWorkspaceState: input.localWorkspaceState,
121 }).pipe(Layer.provide(baseLayer));
122
130 const executionResolverLayer = RuntimeExecutionResolverLive({123 const executionResolverLayer = RuntimeExecutionResolverLive({
131 executionResolver: options.executionResolver,124 executionResolver: input.executionResolver,
132 resolveSecretMaterial: options.resolveSecretMaterial,125 resolveSecretMaterial: input.resolveSecretMaterial,
133 }).pipe(Layer.provide(sourceAuthLayer));126 }).pipe(
127 Layer.provide(Layer.mergeAll(baseLayer, sourceAuthLayer)),
128 );
134 129
135 return Layer.mergeAll(130 return Layer.mergeAll(
136 ControlPlaneStoreLive,131 baseLayer,
137 liveExecutionManagerLayer,
138 sourceAuthLayer,132 sourceAuthLayer,
139 executionResolverLayer,133 executionResolverLayer,
140 );134 ) as RuntimeControlPlaneLayer;
141};135};
142 136
143export type ControlPlaneRuntime = {137export type ControlPlaneRuntime = {
144 persistence: SqlControlPlanePersistence;138 persistence: LocalControlPlanePersistence;
145 localInstallation: LocalInstallation;139 localInstallation: LocalInstallation;
146 runtimeLayer: RuntimeControlPlaneLayer;140 runtimeLayer: RuntimeControlPlaneLayer;
147 close: () => Promise<void>;141 close: () => Promise<void>;
148};142};
149 143
150export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions &144export type CreateControlPlaneRuntimeOptions = RuntimeControlPlaneOptions;
151 RuntimeControlPlaneOptions;
152 145
153export const createControlPlaneRuntime = (146export const createControlPlaneRuntime = (
154 options: CreateControlPlaneRuntimeOptions,147 options: CreateControlPlaneRuntimeOptions,
155): Effect.Effect<ControlPlaneRuntime, SqlPersistenceBootstrapError> =>148): Effect.Effect<ControlPlaneRuntime, Error> =>
156 Effect.gen(function* () {149 Effect.gen(function* () {
157 const scope = yield* Scope.make();150 const scope = yield* Scope.make();
158 const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe(
159 Layer.provideMerge(SqlControlPlanePersistenceLive(options)),
160 );
161 const baseContext = yield* Layer.buildWithScope(
162 persistenceAndRowsLayer,
163 scope,
164 ).pipe(
165 Effect.catchAll((error) =>
166 closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))),
167 ),
168 );
169
170 const persistence = Context.get(
171 baseContext,
172 SqlControlPlanePersistenceService,
173 );
174 const rows = Context.get(baseContext, SqlControlPlaneRowsService);
175 151
176 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({152 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({
177 workspaceRoot: options.workspaceRoot,153 workspaceRoot: options.workspaceRoot,
@@ -182,7 +158,12 @@ export const createControlPlaneRuntime = ( · 4 unmodified lines
182 ),158 ),
183 );159 );
184 160
185 const localInstallation = yield* getOrProvisionLocalInstallation({161 const installationStore = LocalInstallationStore;
162 const workspaceConfigStore = LocalWorkspaceConfigStore;
163 const workspaceStateStore = LocalWorkspaceStateStore;
164 const sourceArtifactStore = LocalSourceArtifactStore;
165
166 const localInstallation = yield* installationStore.getOrProvision({
186 context: localWorkspaceContext,167 context: localWorkspaceContext,
187 }).pipe(168 }).pipe(
188 Effect.mapError(toLocalRuntimeBootstrapError),169 Effect.mapError(toLocalRuntimeBootstrapError),
@@ -191,7 +172,10 @@ export const createControlPlaneRuntime = ( · 2 unmodified lines
191 ),172 ),
192 );173 );
193 174
194 const loadedLocalConfig = yield* loadLocalExecutorConfig(175 const persistence = createLocalControlPlanePersistence(localWorkspaceContext);
176 const rows = persistence.rows;
177
178 const loadedLocalConfig = yield* workspaceConfigStore.load(
195 localWorkspaceContext,179 localWorkspaceContext,
196 ).pipe(180 ).pipe(
197 Effect.mapError(toLocalRuntimeBootstrapError),181 Effect.mapError(toLocalRuntimeBootstrapError),
@@ -237,38 +221,42 @@ export const createControlPlaneRuntime = ( · 39 unmodified lines
237 },221 },
238 },222 },
239 });223 });
224
225 const runtimeLocalWorkspaceState: RuntimeLocalWorkspaceState = {
226 context: localWorkspaceContext,
227 installation: {
228 workspaceId: localInstallation.workspaceId,
229 accountId: localInstallation.accountId,
230 },
231 loadedConfig: {
232 ...loadedLocalConfig,
233 config: effectiveLocalConfig,
234 },
235 };
236
240 const executionResolver =237 const executionResolver =
241 options.executionResolver ??238 options.executionResolver ??
242 createWorkspaceExecutionEnvironmentResolver({239 createWorkspaceExecutionEnvironmentResolver({
243 rows,240 rows,
244 sourceAuthService,241 sourceAuthService,
245 resolveSecretMaterial,242 resolveSecretMaterial,
243 workspaceConfigStore,
244 workspaceStateStore,
245 sourceArtifactStore,
246 });246 });
247 247
248 const runtimeContext = Context.empty().pipe(248 const concreteRuntimeLayer = createRuntimeControlPlaneLayer({
249 Context.add(ControlPlaneStore, rows),249 ...options,
250 Context.add(LiveExecutionManagerService, liveExecutionManager),250 store: rows,
251 Context.add(RuntimeLocalWorkspaceService, {251 localWorkspaceState: runtimeLocalWorkspaceState,
252 context: localWorkspaceContext,252 resolveSecretMaterial,
253 installation: {253 executionResolver,
254 workspaceId: localInstallation.workspaceId,254 });
255 accountId: localInstallation.accountId,
256 },
257 loadedConfig: {
258 ...loadedLocalConfig,
259 config: effectiveLocalConfig,
260 },
261 }),
262 Context.add(RuntimeSourceAuthServiceTag, sourceAuthService),
263 Context.add(RuntimeExecutionResolverService, executionResolver),
264 );
265 const concreteRuntimeLayer = createRuntimeLayerFromContext(runtimeContext);
266 255
267 return {256 return {
268 persistence,257 persistence,
269 localInstallation,258 localInstallation,
270 runtimeLayer: concreteRuntimeLayer,259 runtimeLayer: concreteRuntimeLayer,
271 close: () =>260 close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}),
272 Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}),
273 };261 };
274 });262 });
packages/control-plane/src/runtime/live-execution.test.ts+1−1
@@ -9,7 +9,7 @@ describe("live-execution", () => { · 8 unmodified lines
9 content: {9 content: {
10 authKind: "bearer",10 authKind: "bearer",
11 tokenRef: {11 tokenRef: {
12 providerId: "postgres",12 providerId: "local",
13 handle: "sec_123",13 handle: "sec_123",
14 },14 },
15 },15 },
packages/control-plane/src/runtime/live-execution.ts+3−2
@@ -2,7 +2,6 @@ import type { · 1 unmodified line
2 ElicitationResponse,2 ElicitationResponse,
3 OnElicitation,3 OnElicitation,
4} from "@executor/codemode-core";4} from "@executor/codemode-core";
5import { type SqlControlPlaneRows } from "#persistence";
6import {5import {
7 ExecutionInteractionIdSchema,6 ExecutionInteractionIdSchema,
8 type Execution,7 type Execution,
@@ -13,6 +12,8 @@ import * as Deferred from "effect/Deferred"; · 4 unmodified lines
13import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";13import * as Layer from "effect/Layer";
15 14
15import type { ControlPlaneStoreShape } from "./store";
16
16type VisibleExecutionState =17type VisibleExecutionState =
17 | "waiting_for_interaction"18 | "waiting_for_interaction"
18 | "completed"19 | "completed"
@@ -35,7 +36,7 @@ type LiveExecutionManagerShape = { · 16 unmodified lines
35 executionId: Execution["id"],36 executionId: Execution["id"],
36 ) => Effect.Effect<Deferred.Deferred<VisibleExecutionState>>;37 ) => Effect.Effect<Deferred.Deferred<VisibleExecutionState>>;
37 createOnElicitation: (input: {38 createOnElicitation: (input: {
38 rows: SqlControlPlaneRows;39 rows: ControlPlaneStoreShape;
39 executionId: Execution["id"];40 executionId: Execution["id"];
40 }) => OnElicitation;41 }) => OnElicitation;
41 resolveInteraction: (input: {42 resolveInteraction: (input: {
packages/control-plane/src/runtime/local-control-plane-store.ts+1054−0
@@ -0,0 +1,1054 @@
1import { randomUUID } from "node:crypto";
2import { existsSync } from "node:fs";
3import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4import { dirname, join, resolve } from "node:path";
5
6import {
7 AuthArtifactSchema,
8 AuthLeaseSchema,
9 type AuthArtifact,
10 type AuthLease,
11 type Execution,
12 type ExecutionInteraction,
13 type ExecutionStep,
14 ExecutionInteractionSchema,
15 ExecutionSchema,
16 ExecutionStepSchema,
17 SecretMaterialSchema,
18 type SecretMaterial,
19 type SourceAuthSession,
20 SourceAuthSessionSchema,
21 type WorkspaceSourceOauthClient,
22 WorkspaceSourceOauthClientSchema,
23} from "#schema";
24import * as Effect from "effect/Effect";
25import * as Option from "effect/Option";
26import * as Schema from "effect/Schema";
27
28import { authArtifactSecretMaterialRefs } from "./auth-artifacts";
29import type { ResolvedLocalWorkspaceContext } from "./local-config";
30import { loadLocalExecutorConfig } from "./local-config";
31import {
32 LocalFileSystemError,
33 unknownLocalErrorDetails,
34} from "./local-errors";
35
36const LOCAL_CONTROL_PLANE_STATE_VERSION = 1 as const;
37const LOCAL_CONTROL_PLANE_STATE_BASENAME = "control-plane-state.json";
38
39const LocalControlPlaneStateSchema = Schema.Struct({
40 version: Schema.Literal(LOCAL_CONTROL_PLANE_STATE_VERSION),
41 authArtifacts: Schema.Array(AuthArtifactSchema),
42 authLeases: Schema.Array(AuthLeaseSchema),
43 sourceOauthClients: Schema.Array(WorkspaceSourceOauthClientSchema),
44 sourceAuthSessions: Schema.Array(SourceAuthSessionSchema),
45 secretMaterials: Schema.Array(SecretMaterialSchema),
46 executions: Schema.Array(ExecutionSchema),
47 executionInteractions: Schema.Array(ExecutionInteractionSchema),
48 executionSteps: Schema.Array(ExecutionStepSchema),
49});
50
51export type LocalControlPlaneState = typeof LocalControlPlaneStateSchema.Type;
52
53export type LocalControlPlanePersistence = {
54 rows: LocalControlPlaneStore;
55 close: () => Promise<void>;
56};
57
58const decodeLocalControlPlaneState = Schema.decodeUnknownSync(
59 LocalControlPlaneStateSchema,
60);
61
62const defaultLocalControlPlaneState = (): LocalControlPlaneState => ({
63 version: LOCAL_CONTROL_PLANE_STATE_VERSION,
64 authArtifacts: [],
65 authLeases: [],
66 sourceOauthClients: [],
67 sourceAuthSessions: [],
68 secretMaterials: [],
69 executions: [],
70 executionInteractions: [],
71 executionSteps: [],
72});
73
74const cloneValue = <T>(value: T): T =>
75 JSON.parse(JSON.stringify(value)) as T;
76
77const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
78 new LocalFileSystemError({
79 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
80 action,
81 path,
82 details: unknownLocalErrorDetails(cause),
83 });
84
85const actorEquals = (
86 left: string | null | undefined,
87 right: string | null | undefined,
88): boolean => (left ?? null) === (right ?? null);
89
90const sortByUpdatedAtAndIdAsc = <T extends { updatedAt: number; id: string }>(
91 values: readonly T[],
92): T[] =>
93 [...values].sort((left, right) =>
94 left.updatedAt - right.updatedAt || left.id.localeCompare(right.id),
95 );
96
97const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>(
98 values: readonly T[],
99): T[] =>
100 [...values].sort((left, right) =>
101 right.updatedAt - left.updatedAt || right.id.localeCompare(left.id),
102 );
103
104const localControlPlaneStatePath = (
105 context: ResolvedLocalWorkspaceContext,
106): string => join(context.stateDirectory, LOCAL_CONTROL_PLANE_STATE_BASENAME);
107
108const readStateFromDisk = async (
109 context: ResolvedLocalWorkspaceContext,
110): Promise<LocalControlPlaneState> => {
111 const path = localControlPlaneStatePath(context);
112 if (!existsSync(path)) {
113 return defaultLocalControlPlaneState();
114 }
115
116 const content = await readFile(path, "utf8");
117 return decodeLocalControlPlaneState(JSON.parse(content) as unknown);
118};
119
120const writeStateToDisk = async (
121 context: ResolvedLocalWorkspaceContext,
122 state: LocalControlPlaneState,
123): Promise<void> => {
124 const path = localControlPlaneStatePath(context);
125 await mkdir(dirname(path), { recursive: true });
126 const tempPath = `${path}.${randomUUID()}.tmp`;
127 await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
128 await rename(tempPath, path);
129};
130
131export const loadLocalControlPlaneState = (
132 context: ResolvedLocalWorkspaceContext,
133): Effect.Effect<LocalControlPlaneState, LocalFileSystemError> =>
134 Effect.tryPromise({
135 try: () => readStateFromDisk(context),
136 catch: mapFileSystemError(
137 localControlPlaneStatePath(context),
138 "read control plane state",
139 ),
140 });
141
142export const writeLocalControlPlaneState = (input: {
143 context: ResolvedLocalWorkspaceContext;
144 state: LocalControlPlaneState;
145}): Effect.Effect<void, LocalFileSystemError> =>
146 Effect.tryPromise({
147 try: () => writeStateToDisk(input.context, input.state),
148 catch: mapFileSystemError(
149 localControlPlaneStatePath(input.context),
150 "write control plane state",
151 ),
152 });
153
154const mergeById = <T extends { id: string }>(
155 current: readonly T[],
156 imported: readonly T[],
157): T[] => {
158 const merged = new Map<string, T>();
159
160 for (const item of imported) {
161 merged.set(item.id, cloneValue(item));
162 }
163
164 for (const item of current) {
165 merged.set(item.id, cloneValue(item));
166 }
167
168 return [...merged.values()];
169};
170
171const mergeAuthLeases = (
172 current: readonly AuthLease[],
173 imported: readonly AuthLease[],
174): AuthLease[] => {
175 const merged = new Map<string, AuthLease>();
176
177 for (const lease of imported) {
178 merged.set(lease.authArtifactId, cloneValue(lease));
179 }
180
181 for (const lease of current) {
182 merged.set(lease.authArtifactId, cloneValue(lease));
183 }
184
185 return [...merged.values()];
186};
187
188const mergeAuthArtifacts = (
189 current: readonly AuthArtifact[],
190 imported: readonly AuthArtifact[],
191): AuthArtifact[] => {
192 const merged = new Map<string, AuthArtifact>();
193
194 for (const artifact of imported) {
195 merged.set(
196 [
197 artifact.workspaceId,
198 artifact.sourceId,
199 artifact.actorAccountId ?? "",
200 artifact.slot,
201 ].join("::"),
202 cloneValue(artifact),
203 );
204 }
205
206 for (const artifact of current) {
207 merged.set(
208 [
209 artifact.workspaceId,
210 artifact.sourceId,
211 artifact.actorAccountId ?? "",
212 artifact.slot,
213 ].join("::"),
214 cloneValue(artifact),
215 );
216 }
217
218 return [...merged.values()];
219};
220
221const mergeSourceOauthClients = (
222 current: readonly WorkspaceSourceOauthClient[],
223 imported: readonly WorkspaceSourceOauthClient[],
224): WorkspaceSourceOauthClient[] => {
225 const merged = new Map<string, WorkspaceSourceOauthClient>();
226
227 for (const oauthClient of imported) {
228 merged.set(
229 [oauthClient.workspaceId, oauthClient.sourceId, oauthClient.providerKey].join(
230 "::",
231 ),
232 cloneValue(oauthClient),
233 );
234 }
235
236 for (const oauthClient of current) {
237 merged.set(
238 [oauthClient.workspaceId, oauthClient.sourceId, oauthClient.providerKey].join(
239 "::",
240 ),
241 cloneValue(oauthClient),
242 );
243 }
244
245 return [...merged.values()];
246};
247
248export const mergeImportedLocalControlPlaneState = (input: {
249 current: LocalControlPlaneState;
250 imported: Partial<Omit<LocalControlPlaneState, "version">>;
251}): LocalControlPlaneState => ({
252 version: LOCAL_CONTROL_PLANE_STATE_VERSION,
253 authArtifacts: mergeAuthArtifacts(
254 input.current.authArtifacts,
255 input.imported.authArtifacts ?? [],
256 ),
257 authLeases: mergeAuthLeases(
258 input.current.authLeases,
259 input.imported.authLeases ?? [],
260 ),
261 sourceOauthClients: mergeSourceOauthClients(
262 input.current.sourceOauthClients,
263 input.imported.sourceOauthClients ?? [],
264 ),
265 sourceAuthSessions: mergeById(
266 input.current.sourceAuthSessions,
267 input.imported.sourceAuthSessions ?? [],
268 ),
269 secretMaterials: mergeById(
270 input.current.secretMaterials,
271 input.imported.secretMaterials ?? [],
272 ),
273 executions: mergeById(input.current.executions, input.imported.executions ?? []),
274 executionInteractions: mergeById(
275 input.current.executionInteractions,
276 input.imported.executionInteractions ?? [],
277 ),
278 executionSteps: mergeById(
279 input.current.executionSteps,
280 input.imported.executionSteps ?? [],
281 ),
282});
283
284type StateMutationResult<A> = {
285 state: LocalControlPlaneState;
286 value: A;
287};
288
289const createStateManager = (context: ResolvedLocalWorkspaceContext) => {
290 let cache: LocalControlPlaneState | null = null;
291 let mutationQueue: Promise<void> = Promise.resolve();
292
293 const ensureLoaded = async (): Promise<LocalControlPlaneState> => {
294 if (cache !== null) {
295 return cache;
296 }
297
298 cache = await readStateFromDisk(context);
299 return cache;
300 };
301
302 const read = <A>(
303 operation: (state: LocalControlPlaneState) => A | Promise<A>,
304 ): Effect.Effect<A, LocalFileSystemError> =>
305 Effect.tryPromise({
306 try: async () => operation(cloneValue(await ensureLoaded())),
307 catch: mapFileSystemError(
308 localControlPlaneStatePath(context),
309 "read control plane state",
310 ),
311 });
312
313 const mutate = <A>(
314 operation: (
315 state: LocalControlPlaneState,
316 ) => StateMutationResult<A> | Promise<StateMutationResult<A>>,
317 ): Effect.Effect<A, LocalFileSystemError> =>
318 Effect.tryPromise({
319 try: async () => {
320 let value!: A;
321 let failure: unknown = null;
322
323 mutationQueue = mutationQueue.then(async () => {
324 try {
325 const current = cloneValue(await ensureLoaded());
326 const result = await operation(current);
327 cache = result.state;
328 value = result.value;
329 await writeStateToDisk(context, cache);
330 } catch (cause) {
331 failure = cause;
332 }
333 });
334
335 await mutationQueue;
336
337 if (failure !== null) {
338 throw failure;
339 }
340
341 return value;
342 },
343 catch: mapFileSystemError(
344 localControlPlaneStatePath(context),
345 "write control plane state",
346 ),
347 });
348
349 return {
350 read,
351 mutate,
352 };
353};
354
355export const createLocalControlPlaneStore = (
356 context: ResolvedLocalWorkspaceContext,
357) => {
358 const stateManager = createStateManager(context);
359
360 return {
361 authArtifacts: {
362 listByWorkspaceId: (workspaceId: AuthArtifact["workspaceId"]) =>
363 stateManager.read((state) =>
364 sortByUpdatedAtAndIdAsc(
365 state.authArtifacts.filter((artifact) => artifact.workspaceId === workspaceId),
366 ),
367 ),
368
369 listByWorkspaceAndSourceId: (input: {
370 workspaceId: AuthArtifact["workspaceId"];
371 sourceId: AuthArtifact["sourceId"];
372 }) =>
373 stateManager.read((state) =>
374 sortByUpdatedAtAndIdAsc(
375 state.authArtifacts.filter(
376 (artifact) =>
377 artifact.workspaceId === input.workspaceId
378 && artifact.sourceId === input.sourceId,
379 ),
380 ),
381 ),
382
383 getByWorkspaceSourceAndActor: (input: {
384 workspaceId: AuthArtifact["workspaceId"];
385 sourceId: AuthArtifact["sourceId"];
386 actorAccountId: AuthArtifact["actorAccountId"];
387 slot: AuthArtifact["slot"];
388 }) =>
389 stateManager.read((state) => {
390 const artifact = state.authArtifacts.find(
391 (candidate) =>
392 candidate.workspaceId === input.workspaceId
393 && candidate.sourceId === input.sourceId
394 && candidate.slot === input.slot
395 && actorEquals(candidate.actorAccountId, input.actorAccountId),
396 );
397
398 return artifact ? Option.some(cloneValue(artifact)) : Option.none<AuthArtifact>();
399 }),
400
401 upsert: (artifact: AuthArtifact) =>
402 stateManager.mutate((state) => {
403 const nextArtifacts = state.authArtifacts.filter(
404 (candidate) =>
405 !(
406 candidate.workspaceId === artifact.workspaceId
407 && candidate.sourceId === artifact.sourceId
408 && candidate.slot === artifact.slot
409 && actorEquals(candidate.actorAccountId, artifact.actorAccountId)
410 ),
411 );
412 nextArtifacts.push(cloneValue(artifact));
413
414 return {
415 state: {
416 ...state,
417 authArtifacts: nextArtifacts,
418 },
419 value: undefined,
420 } satisfies StateMutationResult<void>;
421 }),
422
423 removeByWorkspaceSourceAndActor: (input: {
424 workspaceId: AuthArtifact["workspaceId"];
425 sourceId: AuthArtifact["sourceId"];
426 actorAccountId: AuthArtifact["actorAccountId"];
427 slot?: AuthArtifact["slot"];
428 }) =>
429 stateManager.mutate((state) => {
430 const nextArtifacts = state.authArtifacts.filter(
431 (candidate) =>
432 candidate.workspaceId !== input.workspaceId
433 || candidate.sourceId !== input.sourceId
434 || !actorEquals(candidate.actorAccountId, input.actorAccountId)
435 || (input.slot !== undefined && candidate.slot !== input.slot),
436 );
437
438 return {
439 state: {
440 ...state,
441 authArtifacts: nextArtifacts,
442 },
443 value: nextArtifacts.length !== state.authArtifacts.length,
444 } satisfies StateMutationResult<boolean>;
445 }),
446
447 removeByWorkspaceAndSourceId: (input: {
448 workspaceId: AuthArtifact["workspaceId"];
449 sourceId: AuthArtifact["sourceId"];
450 }) =>
451 stateManager.mutate((state) => {
452 const nextArtifacts = state.authArtifacts.filter(
453 (candidate) =>
454 candidate.workspaceId !== input.workspaceId
455 || candidate.sourceId !== input.sourceId,
456 );
457
458 return {
459 state: {
460 ...state,
461 authArtifacts: nextArtifacts,
462 },
463 value: state.authArtifacts.length - nextArtifacts.length,
464 } satisfies StateMutationResult<number>;
465 }),
466 },
467
468 authLeases: {
469 listAll: () =>
470 stateManager.read((state) => sortByUpdatedAtAndIdAsc(state.authLeases)),
471
472 getByAuthArtifactId: (authArtifactId: AuthLease["authArtifactId"]) =>
473 stateManager.read((state) => {
474 const lease = state.authLeases.find(
475 (candidate) => candidate.authArtifactId === authArtifactId,
476 );
477 return lease ? Option.some(cloneValue(lease)) : Option.none<AuthLease>();
478 }),
479
480 upsert: (lease: AuthLease) =>
481 stateManager.mutate((state) => {
482 const nextLeases = state.authLeases.filter(
483 (candidate) => candidate.authArtifactId !== lease.authArtifactId,
484 );
485 nextLeases.push(cloneValue(lease));
486
487 return {
488 state: {
489 ...state,
490 authLeases: nextLeases,
491 },
492 value: undefined,
493 } satisfies StateMutationResult<void>;
494 }),
495
496 removeByAuthArtifactId: (authArtifactId: AuthLease["authArtifactId"]) =>
497 stateManager.mutate((state) => {
498 const nextLeases = state.authLeases.filter(
499 (candidate) => candidate.authArtifactId !== authArtifactId,
500 );
501
502 return {
503 state: {
504 ...state,
505 authLeases: nextLeases,
506 },
507 value: nextLeases.length !== state.authLeases.length,
508 } satisfies StateMutationResult<boolean>;
509 }),
510 },
511
512 sourceOauthClients: {
513 getByWorkspaceSourceAndProvider: (input: {
514 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
515 sourceId: WorkspaceSourceOauthClient["sourceId"];
516 providerKey: string;
517 }) =>
518 stateManager.read((state) => {
519 const oauthClient = state.sourceOauthClients.find(
520 (candidate) =>
521 candidate.workspaceId === input.workspaceId
522 && candidate.sourceId === input.sourceId
523 && candidate.providerKey === input.providerKey,
524 );
525
526 return oauthClient
527 ? Option.some(cloneValue(oauthClient))
528 : Option.none<WorkspaceSourceOauthClient>();
529 }),
530
531 upsert: (oauthClient: WorkspaceSourceOauthClient) =>
532 stateManager.mutate((state) => {
533 const nextOauthClients = state.sourceOauthClients.filter(
534 (candidate) =>
535 !(
536 candidate.workspaceId === oauthClient.workspaceId
537 && candidate.sourceId === oauthClient.sourceId
538 && candidate.providerKey === oauthClient.providerKey
539 ),
540 );
541 nextOauthClients.push(cloneValue(oauthClient));
542
543 return {
544 state: {
545 ...state,
546 sourceOauthClients: nextOauthClients,
547 },
548 value: undefined,
549 } satisfies StateMutationResult<void>;
550 }),
551
552 removeByWorkspaceAndSourceId: (input: {
553 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
554 sourceId: WorkspaceSourceOauthClient["sourceId"];
555 }) =>
556 stateManager.mutate((state) => {
557 const nextOauthClients = state.sourceOauthClients.filter(
558 (candidate) =>
559 candidate.workspaceId !== input.workspaceId
560 || candidate.sourceId !== input.sourceId,
561 );
562
563 return {
564 state: {
565 ...state,
566 sourceOauthClients: nextOauthClients,
567 },
568 value: state.sourceOauthClients.length - nextOauthClients.length,
569 } satisfies StateMutationResult<number>;
570 }),
571 },
572
573 sourceAuthSessions: {
574 listAll: () =>
575 stateManager.read((state) => sortByUpdatedAtAndIdAsc(state.sourceAuthSessions)),
576
577 listByWorkspaceId: (workspaceId: SourceAuthSession["workspaceId"]) =>
578 stateManager.read((state) =>
579 sortByUpdatedAtAndIdAsc(
580 state.sourceAuthSessions.filter(
581 (session) => session.workspaceId === workspaceId,
582 ),
583 ),
584 ),
585
586 getById: (id: SourceAuthSession["id"]) =>
587 stateManager.read((state) => {
588 const session = state.sourceAuthSessions.find(
589 (candidate) => candidate.id === id,
590 );
591 return session
592 ? Option.some(cloneValue(session))
593 : Option.none<SourceAuthSession>();
594 }),
595
596 getByState: (stateValue: SourceAuthSession["state"]) =>
597 stateManager.read((state) => {
598 const session = state.sourceAuthSessions.find(
599 (candidate) => candidate.state === stateValue,
600 );
601 return session
602 ? Option.some(cloneValue(session))
603 : Option.none<SourceAuthSession>();
604 }),
605
606 getPendingByWorkspaceSourceAndActor: (input: {
607 workspaceId: SourceAuthSession["workspaceId"];
608 sourceId: SourceAuthSession["sourceId"];
609 actorAccountId: SourceAuthSession["actorAccountId"];
610 credentialSlot?: SourceAuthSession["credentialSlot"];
611 }) =>
612 stateManager.read((state) => {
613 const session = sortByUpdatedAtAndIdAsc(
614 state.sourceAuthSessions.filter(
615 (candidate) =>
616 candidate.workspaceId === input.workspaceId
617 && candidate.sourceId === input.sourceId
618 && candidate.status === "pending"
619 && actorEquals(candidate.actorAccountId, input.actorAccountId)
620 && (input.credentialSlot === undefined
621 || candidate.credentialSlot === input.credentialSlot),
622 ),
623 )[0] ?? null;
624
625 return session
626 ? Option.some(cloneValue(session))
627 : Option.none<SourceAuthSession>();
628 }),
629
630 insert: (session: SourceAuthSession) =>
631 stateManager.mutate((state) => ({
632 state: {
633 ...state,
634 sourceAuthSessions: [...state.sourceAuthSessions, cloneValue(session)],
635 },
636 value: undefined,
637 } satisfies StateMutationResult<void>)),
638
639 update: (
640 id: SourceAuthSession["id"],
641 patch: Partial<Omit<SourceAuthSession, "id" | "workspaceId" | "sourceId" | "createdAt">>,
642 ) =>
643 stateManager.mutate((state) => {
644 let updated: SourceAuthSession | null = null;
645 const nextSessions = state.sourceAuthSessions.map((session) => {
646 if (session.id !== id) {
647 return session;
648 }
649
650 updated = {
651 ...session,
652 ...cloneValue(patch),
653 } satisfies SourceAuthSession;
654 return updated;
655 });
656
657 return {
658 state: {
659 ...state,
660 sourceAuthSessions: nextSessions,
661 },
662 value: updated ? Option.some(cloneValue(updated)) : Option.none<SourceAuthSession>(),
663 } satisfies StateMutationResult<Option.Option<SourceAuthSession>>;
664 }),
665
666 upsert: (session: SourceAuthSession) =>
667 stateManager.mutate((state) => {
668 const nextSessions = state.sourceAuthSessions.filter(
669 (candidate) => candidate.id !== session.id,
670 );
671 nextSessions.push(cloneValue(session));
672
673 return {
674 state: {
675 ...state,
676 sourceAuthSessions: nextSessions,
677 },
678 value: undefined,
679 } satisfies StateMutationResult<void>;
680 }),
681
682 removeByWorkspaceAndSourceId: (
683 workspaceId: SourceAuthSession["workspaceId"],
684 sourceId: SourceAuthSession["sourceId"],
685 ) =>
686 stateManager.mutate((state) => {
687 const nextSessions = state.sourceAuthSessions.filter(
688 (candidate) =>
689 candidate.workspaceId !== workspaceId || candidate.sourceId !== sourceId,
690 );
691
692 return {
693 state: {
694 ...state,
695 sourceAuthSessions: nextSessions,
696 },
697 value: nextSessions.length !== state.sourceAuthSessions.length,
698 } satisfies StateMutationResult<boolean>;
699 }),
700 },
701
702 secretMaterials: {
703 getById: (id: SecretMaterial["id"]) =>
704 stateManager.read((state) => {
705 const material = state.secretMaterials.find(
706 (candidate) => candidate.id === id,
707 );
708 return material
709 ? Option.some(cloneValue(material))
710 : Option.none<SecretMaterial>();
711 }),
712
713 listAll: () =>
714 stateManager.read((state) => sortByUpdatedAtAndIdDesc(state.secretMaterials)),
715
716 upsert: (material: SecretMaterial) =>
717 stateManager.mutate((state) => {
718 const nextMaterials = state.secretMaterials.filter(
719 (candidate) => candidate.id !== material.id,
720 );
721 nextMaterials.push(cloneValue(material));
722
723 return {
724 state: {
725 ...state,
726 secretMaterials: nextMaterials,
727 },
728 value: undefined,
729 } satisfies StateMutationResult<void>;
730 }),
731
732 updateById: (
733 id: SecretMaterial["id"],
734 update: { name?: string | null; value?: string },
735 ) =>
736 stateManager.mutate((state) => {
737 let updated: SecretMaterial | null = null;
738 const nextMaterials = state.secretMaterials.map((material) => {
739 if (material.id !== id) {
740 return material;
741 }
742
743 updated = {
744 ...material,
745 ...(update.name !== undefined ? { name: update.name } : {}),
746 ...(update.value !== undefined ? { value: update.value } : {}),
747 updatedAt: Date.now(),
748 } satisfies SecretMaterial;
749 return updated;
750 });
751
752 return {
753 state: {
754 ...state,
755 secretMaterials: nextMaterials,
756 },
757 value: updated ? Option.some(cloneValue(updated)) : Option.none<SecretMaterial>(),
758 } satisfies StateMutationResult<Option.Option<SecretMaterial>>;
759 }),
760
761 removeById: (id: SecretMaterial["id"]) =>
762 stateManager.mutate((state) => {
763 const nextMaterials = state.secretMaterials.filter(
764 (candidate) => candidate.id !== id,
765 );
766
767 return {
768 state: {
769 ...state,
770 secretMaterials: nextMaterials,
771 },
772 value: nextMaterials.length !== state.secretMaterials.length,
773 } satisfies StateMutationResult<boolean>;
774 }),
775
776 listLinkedSources: () =>
777 Effect.gen(function* () {
778 const [state, loadedConfig] = yield* Effect.all([
779 stateManager.read((current) => current),
780 loadLocalExecutorConfig(context),
781 ]);
782
783 const sourceNames = new Map<string, string>();
784 for (const [sourceId, source] of Object.entries(
785 loadedConfig.config?.sources ?? {},
786 )) {
787 sourceNames.set(sourceId, source.name ?? sourceId);
788 }
789
790 const materialIds = new Set(
791 state.secretMaterials.map((material) => String(material.id)),
792 );
793 const linkedSources = new Map<string, Array<{ sourceId: string; sourceName: string }>>();
794
795 for (const artifact of state.authArtifacts) {
796 for (const ref of authArtifactSecretMaterialRefs(artifact)) {
797 if (!materialIds.has(ref.handle)) {
798 continue;
799 }
800
801 const existing = linkedSources.get(ref.handle) ?? [];
802 if (!existing.some((link) => link.sourceId === artifact.sourceId)) {
803 existing.push({
804 sourceId: artifact.sourceId,
805 sourceName: sourceNames.get(artifact.sourceId) ?? artifact.sourceId,
806 });
807 linkedSources.set(ref.handle, existing);
808 }
809 }
810 }
811
812 return linkedSources;
813 }),
814 },
815
816 executions: {
817 getById: (executionId: Execution["id"]) =>
818 stateManager.read((state) => {
819 const execution = state.executions.find(
820 (candidate) => candidate.id === executionId,
821 );
822 return execution
823 ? Option.some(cloneValue(execution))
824 : Option.none<Execution>();
825 }),
826
827 getByWorkspaceAndId: (
828 workspaceId: Execution["workspaceId"],
829 executionId: Execution["id"],
830 ) =>
831 stateManager.read((state) => {
832 const execution = state.executions.find(
833 (candidate) =>
834 candidate.workspaceId === workspaceId && candidate.id === executionId,
835 );
836 return execution
837 ? Option.some(cloneValue(execution))
838 : Option.none<Execution>();
839 }),
840
841 insert: (execution: Execution) =>
842 stateManager.mutate((state) => ({
843 state: {
844 ...state,
845 executions: [...state.executions, cloneValue(execution)],
846 },
847 value: undefined,
848 } satisfies StateMutationResult<void>)),
849
850 update: (
851 executionId: Execution["id"],
852 patch: Partial<
853 Omit<Execution, "id" | "workspaceId" | "createdByAccountId" | "createdAt">
854 >,
855 ) =>
856 stateManager.mutate((state) => {
857 let updated: Execution | null = null;
858 const nextExecutions = state.executions.map((execution) => {
859 if (execution.id !== executionId) {
860 return execution;
861 }
862
863 updated = {
864 ...execution,
865 ...cloneValue(patch),
866 } satisfies Execution;
867 return updated;
868 });
869
870 return {
871 state: {
872 ...state,
873 executions: nextExecutions,
874 },
875 value: updated ? Option.some(cloneValue(updated)) : Option.none<Execution>(),
876 } satisfies StateMutationResult<Option.Option<Execution>>;
877 }),
878 },
879
880 executionInteractions: {
881 getById: (interactionId: ExecutionInteraction["id"]) =>
882 stateManager.read((state) => {
883 const interaction = state.executionInteractions.find(
884 (candidate) => candidate.id === interactionId,
885 );
886 return interaction
887 ? Option.some(cloneValue(interaction))
888 : Option.none<ExecutionInteraction>();
889 }),
890
891 listByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
892 stateManager.read((state) =>
893 sortByUpdatedAtAndIdDesc(
894 state.executionInteractions.filter(
895 (interaction) => interaction.executionId === executionId,
896 ),
897 ),
898 ),
899
900 getPendingByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
901 stateManager.read((state) => {
902 const interaction = sortByUpdatedAtAndIdDesc(
903 state.executionInteractions.filter(
904 (candidate) =>
905 candidate.executionId === executionId && candidate.status === "pending",
906 ),
907 )[0] ?? null;
908
909 return interaction
910 ? Option.some(cloneValue(interaction))
911 : Option.none<ExecutionInteraction>();
912 }),
913
914 insert: (interaction: ExecutionInteraction) =>
915 stateManager.mutate((state) => ({
916 state: {
917 ...state,
918 executionInteractions: [
919 ...state.executionInteractions,
920 cloneValue(interaction),
921 ],
922 },
923 value: undefined,
924 } satisfies StateMutationResult<void>)),
925
926 update: (
927 interactionId: ExecutionInteraction["id"],
928 patch: Partial<
929 Omit<
930 ExecutionInteraction,
931 "id" | "executionId" | "createdAt"
932 >
933 >,
934 ) =>
935 stateManager.mutate((state) => {
936 let updated: ExecutionInteraction | null = null;
937 const nextInteractions = state.executionInteractions.map((interaction) => {
938 if (interaction.id !== interactionId) {
939 return interaction;
940 }
941
942 updated = {
943 ...interaction,
944 ...cloneValue(patch),
945 } as ExecutionInteraction;
946 return updated;
947 });
948
949 return {
950 state: {
951 ...state,
952 executionInteractions: nextInteractions,
953 },
954 value: updated
955 ? Option.some(cloneValue(updated))
956 : Option.none<ExecutionInteraction>(),
957 } satisfies StateMutationResult<Option.Option<ExecutionInteraction>>;
958 }),
959 },
960
961 executionSteps: {
962 getByExecutionAndSequence: (
963 executionId: ExecutionStep["executionId"],
964 sequence: ExecutionStep["sequence"],
965 ) =>
966 stateManager.read((state) => {
967 const step = state.executionSteps.find(
968 (candidate) =>
969 candidate.executionId === executionId && candidate.sequence === sequence,
970 );
971 return step
972 ? Option.some(cloneValue(step))
973 : Option.none<ExecutionStep>();
974 }),
975
976 listByExecutionId: (executionId: ExecutionStep["executionId"]) =>
977 stateManager.read((state) =>
978 [...state.executionSteps]
979 .filter((step) => step.executionId === executionId)
980 .sort(
981 (left, right) =>
982 left.sequence - right.sequence
983 || right.updatedAt - left.updatedAt,
984 ),
985 ),
986
987 insert: (step: ExecutionStep) =>
988 stateManager.mutate((state) => ({
989 state: {
990 ...state,
991 executionSteps: [...state.executionSteps, cloneValue(step)],
992 },
993 value: undefined,
994 } satisfies StateMutationResult<void>)),
995
996 deleteByExecutionId: (executionId: ExecutionStep["executionId"]) =>
997 stateManager.mutate((state) => ({
998 state: {
999 ...state,
1000 executionSteps: state.executionSteps.filter(
1001 (step) => step.executionId !== executionId,
1002 ),
1003 },
1004 value: undefined,
1005 } satisfies StateMutationResult<void>)),
1006
1007 updateByExecutionAndSequence: (
1008 executionId: ExecutionStep["executionId"],
1009 sequence: ExecutionStep["sequence"],
1010 patch: Partial<
1011 Omit<
1012 ExecutionStep,
1013 "id" | "executionId" | "sequence" | "createdAt"
1014 >
1015 >,
1016 ) =>
1017 stateManager.mutate((state) => {
1018 let updated: ExecutionStep | null = null;
1019 const nextSteps = state.executionSteps.map((step) => {
1020 if (step.executionId !== executionId || step.sequence !== sequence) {
1021 return step;
1022 }
1023
1024 updated = {
1025 ...step,
1026 ...cloneValue(patch),
1027 } as ExecutionStep;
1028 return updated;
1029 });
1030
1031 return {
1032 state: {
1033 ...state,
1034 executionSteps: nextSteps,
1035 },
1036 value: updated
1037 ? Option.some(cloneValue(updated))
1038 : Option.none<ExecutionStep>(),
1039 } satisfies StateMutationResult<Option.Option<ExecutionStep>>;
1040 }),
1041 },
1042 };
1043};
1044
1045export type LocalControlPlaneStore = ReturnType<typeof createLocalControlPlaneStore>;
1046
1047export const createLocalControlPlanePersistence = (
1048 context: ResolvedLocalWorkspaceContext,
1049): LocalControlPlanePersistence => ({
1050 rows: createLocalControlPlaneStore(context),
1051 close: async () => {},
1052});
1053
1054export { localControlPlaneStatePath };
packages/control-plane/src/runtime/local-operations.ts+16−15
@@ -1,4 +1,3 @@
1import { ControlPlanePersistenceError } from "#persistence";
2import type {1import type {
3 Execution,2 Execution,
4 ExecutionInteraction,3 ExecutionInteraction,
@@ -12,13 +11,13 @@ import { · 7 unmodified lines
12import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
13import * as Option from "effect/Option";12import * as Option from "effect/Option";
14 13
15import { loadLocalInstallation } from "./local-installation";
16import { submitExecutionInteractionResponse } from "./execution-service";14import { submitExecutionInteractionResponse } from "./execution-service";
17import { LiveExecutionManagerService } from "./live-execution";15import { LiveExecutionManagerService } from "./live-execution";
18import {16import {
19 operationErrors,17 operationErrors,
20} from "./operation-errors";18} from "./operation-errors";
21import { resolveLocalWorkspaceContext } from "./local-config";19import { resolveLocalWorkspaceContext } from "./local-config";
20import { InstallationStore, LocalInstallationStore } from "./local-storage";
22import { getRuntimeLocalWorkspaceOption } from "./local-runtime-context";21import { getRuntimeLocalWorkspaceOption } from "./local-runtime-context";
23import { RuntimeSourceAuthServiceTag } from "./source-auth-service";22import { RuntimeSourceAuthServiceTag } from "./source-auth-service";
24import {23import {
@@ -130,12 +129,10 @@ const loadSourceCredentialInteraction = (input: { · 105 unmodified lines
130 129
131 const stored = yield* store.executionInteractions.getById(input.interactionId).pipe(130 const stored = yield* store.executionInteractions.getById(input.interactionId).pipe(
132 Effect.mapError((error) =>131 Effect.mapError((error) =>
133 error instanceof ControlPlanePersistenceError132 input.operation.unknownStorage(
134 ? input.operation.storage(error)133 error,
135 : input.operation.unknownStorage(134 `Failed loading execution interaction ${input.interactionId}`,
136 error,135 ),
137 `Failed loading execution interaction ${input.interactionId}`,
138 ),
139 ),136 ),
140 );137 );
141 138
@@ -194,7 +191,13 @@ export const getLocalInstallation = () => · 52 unmodified lines
194 ),191 ),
195 ));192 ));
196 193
197 return yield* loadLocalInstallation(context);194 const installationStore = runtimeLocalWorkspace !== null
195 ? yield* InstallationStore
196 : LocalInstallationStore;
197
198 return yield* installationStore.load(
199 context,
200 );
198 });201 });
199 202
200export const getSourceCredentialInteraction = (input: {203export const getSourceCredentialInteraction = (input: {
@@ -331,12 +334,10 @@ export const submitSourceCredentialInteraction = (input: { · 130 unmodified lines
331 value: token,334 value: token,
332 }).pipe(335 }).pipe(
333 Effect.mapError((error) =>336 Effect.mapError((error) =>
334 error instanceof ControlPlanePersistenceError337 localOps.sourceCredentialSubmit.unknownStorage(
335 ? localOps.sourceCredentialSubmit.storage(error)338 error,
336 : localOps.sourceCredentialSubmit.unknownStorage(339 `Failed storing credential material for interaction ${interaction.interactionId}`,
337 error,340 ),
338 `Failed storing credential material for interaction ${interaction.interactionId}`,
339 ),
340 ),341 ),
341 );342 );
342 const resumed = yield* liveExecutionManager.resolveInteraction({343 const resumed = yield* liveExecutionManager.resolveInteraction({
packages/control-plane/src/runtime/local-storage.ts+154−0
@@ -0,0 +1,154 @@
1import type { LocalInstallation, LocalExecutorConfig } from "#schema";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5
6import type {
7 LoadedLocalExecutorConfig,
8 ResolvedLocalWorkspaceContext,
9} from "./local-config";
10import {
11 loadLocalExecutorConfig,
12 resolveConfigRelativePath,
13 writeProjectLocalExecutorConfig,
14} from "./local-config";
15import {
16 getOrProvisionLocalInstallation,
17 loadLocalInstallation,
18} from "./local-installation";
19import type {
20 LocalSourceArtifact,
21} from "./local-source-artifacts";
22import {
23 buildLocalSourceArtifact,
24 readLocalSourceArtifact,
25 removeLocalSourceArtifact,
26 writeLocalSourceArtifact,
27} from "./local-source-artifacts";
28import type { LocalWorkspaceState } from "./local-workspace-state";
29import {
30 loadLocalWorkspaceState,
31 writeLocalWorkspaceState,
32} from "./local-workspace-state";
33import type { SourceRecipeMaterialization } from "./source-recipe-support";
34import type { Source } from "#schema";
35
36export type InstallationStoreShape = {
37 load: (
38 context: ResolvedLocalWorkspaceContext,
39 ) => Effect.Effect<LocalInstallation, never, never>;
40 getOrProvision: (input: {
41 context: ResolvedLocalWorkspaceContext;
42 }) => Effect.Effect<LocalInstallation, never, never>;
43};
44
45export class InstallationStore extends Context.Tag(
46 "#runtime/InstallationStore",
47)<InstallationStore, InstallationStoreShape>() {}
48
49export type WorkspaceConfigStoreShape = {
50 load: (
51 context: ResolvedLocalWorkspaceContext,
52 ) => Effect.Effect<LoadedLocalExecutorConfig, Error, never>;
53 writeProject: (input: {
54 context: ResolvedLocalWorkspaceContext;
55 config: LocalExecutorConfig;
56 }) => Effect.Effect<void, Error, never>;
57 resolveRelativePath: (input: { path: string; workspaceRoot: string }) => string;
58};
59
60export class WorkspaceConfigStore extends Context.Tag(
61 "#runtime/WorkspaceConfigStore",
62)<WorkspaceConfigStore, WorkspaceConfigStoreShape>() {}
63
64export type WorkspaceStateStoreShape = {
65 load: (
66 context: ResolvedLocalWorkspaceContext,
67 ) => Effect.Effect<LocalWorkspaceState, Error, never>;
68 write: (input: {
69 context: ResolvedLocalWorkspaceContext;
70 state: LocalWorkspaceState;
71 }) => Effect.Effect<void, Error, never>;
72};
73
74export class WorkspaceStateStore extends Context.Tag(
75 "#runtime/WorkspaceStateStore",
76)<WorkspaceStateStore, WorkspaceStateStoreShape>() {}
77
78export type SourceArtifactStoreShape = {
79 build: (input: {
80 source: Source;
81 materialization: SourceRecipeMaterialization;
82 }) => LocalSourceArtifact;
83 read: (input: {
84 context: ResolvedLocalWorkspaceContext;
85 sourceId: string;
86 }) => Effect.Effect<LocalSourceArtifact | null, Error, never>;
87 write: (input: {
88 context: ResolvedLocalWorkspaceContext;
89 sourceId: string;
90 artifact: LocalSourceArtifact;
91 }) => Effect.Effect<void, Error, never>;
92 remove: (input: {
93 context: ResolvedLocalWorkspaceContext;
94 sourceId: string;
95 }) => Effect.Effect<void, Error, never>;
96};
97
98export class SourceArtifactStore extends Context.Tag(
99 "#runtime/SourceArtifactStore",
100)<SourceArtifactStore, SourceArtifactStoreShape>() {}
101
102export type LocalStorageServices =
103 | InstallationStore
104 | WorkspaceConfigStore
105 | WorkspaceStateStore
106 | SourceArtifactStore;
107
108export type WorkspaceStorageServices =
109 | WorkspaceConfigStore
110 | WorkspaceStateStore
111 | SourceArtifactStore;
112
113export const LocalInstallationStore: InstallationStoreShape = {
114 load: loadLocalInstallation,
115 getOrProvision: getOrProvisionLocalInstallation,
116};
117
118export const LocalInstallationStoreLive = Layer.succeed(
119 InstallationStore,
120 LocalInstallationStore,
121);
122
123export const LocalWorkspaceConfigStore: WorkspaceConfigStoreShape = {
124 load: loadLocalExecutorConfig,
125 writeProject: writeProjectLocalExecutorConfig,
126 resolveRelativePath: resolveConfigRelativePath,
127};
128
129export const LocalWorkspaceConfigStoreLive = Layer.succeed(
130 WorkspaceConfigStore,
131 LocalWorkspaceConfigStore,
132);
133
134export const LocalWorkspaceStateStore: WorkspaceStateStoreShape = {
135 load: loadLocalWorkspaceState,
136 write: writeLocalWorkspaceState,
137};
138
139export const LocalWorkspaceStateStoreLive = Layer.succeed(
140 WorkspaceStateStore,
141 LocalWorkspaceStateStore,
142);
143
144export const LocalSourceArtifactStore: SourceArtifactStoreShape = {
145 build: buildLocalSourceArtifact,
146 read: readLocalSourceArtifact,
147 write: writeLocalSourceArtifact,
148 remove: removeLocalSourceArtifact,
149};
150
151export const LocalSourceArtifactStoreLive = Layer.succeed(
152 SourceArtifactStore,
153 LocalSourceArtifactStore,
154);
packages/control-plane/src/runtime/local-tools.ts+1−1
@@ -540,7 +540,7 @@ const importLocalToolModule = (input: { · 539 unmodified lines
540 toolPath: ToolPath;540 toolPath: ToolPath;
541}): Effect.Effect<ToolInput, LocalToolImportError | LocalToolDefinitionError> =>541}): Effect.Effect<ToolInput, LocalToolImportError | LocalToolDefinitionError> =>
542 Effect.tryPromise({542 Effect.tryPromise({
543 try: () => import(pathToFileURL(resolve(input.artifactPath)).href),543 try: () => import(/* @vite-ignore */ pathToFileURL(resolve(input.artifactPath)).href),
544 catch: (cause) =>544 catch: (cause) =>
545 new LocalToolImportError({545 new LocalToolImportError({
546 message: `Failed to import local tool ${input.sourcePath}`,546 message: `Failed to import local tool ${input.sourcePath}`,
packages/control-plane/src/runtime/operation-errors.ts+10−29
@@ -3,25 +3,8 @@ import { · 2 unmodified lines
3 ControlPlaneNotFoundError,3 ControlPlaneNotFoundError,
4 ControlPlaneStorageError,4 ControlPlaneStorageError,
5} from "../api/errors";5} from "../api/errors";
6import { ControlPlanePersistenceError } from "#persistence";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
8 7
9const unknownPersistenceError = (
10 operation: string,
11 cause: unknown,
12 details: string,
13): ControlPlanePersistenceError =>
14 new ControlPlanePersistenceError({
15 operation,
16 message: cause instanceof Error ? cause.message : String(cause),
17 details,
18 code: null,
19 constraint: null,
20 table: null,
21 kind: "unknown",
22 cause,
23 });
24
25export type OperationErrors<TOperation extends string = string> = {8export type OperationErrors<TOperation extends string = string> = {
26 readonly operation: TOperation;9 readonly operation: TOperation;
27 readonly child: <TSuffix extends string>(10 readonly child: <TSuffix extends string>(
@@ -36,18 +19,14 @@ export type OperationErrors<TOperation extends string = string> = { · 8 unmodified lines
36 details: string,19 details: string,
37 ) => ControlPlaneNotFoundError;20 ) => ControlPlaneNotFoundError;
38 readonly storage: (21 readonly storage: (
39 error: ControlPlanePersistenceError,22 error: Error,
40 ) => ControlPlaneStorageError;23 ) => ControlPlaneStorageError;
41 readonly unknownPersistence: (
42 cause: unknown,
43 details: string,
44 ) => ControlPlanePersistenceError;
45 readonly unknownStorage: (24 readonly unknownStorage: (
46 cause: unknown,25 cause: unknown,
47 details: string,26 details: string,
48 ) => ControlPlaneStorageError;27 ) => ControlPlaneStorageError;
49 readonly mapStorage: <A>(28 readonly mapStorage: <A, E extends Error>(
50 effect: Effect.Effect<A, ControlPlanePersistenceError>,29 effect: Effect.Effect<A, E>,
51 ) => Effect.Effect<A, ControlPlaneStorageError>;30 ) => Effect.Effect<A, ControlPlaneStorageError>;
52};31};
53 32
@@ -76,15 +55,17 @@ export const operationErrors = <TOperation extends string>( · 22 unmodified lines
76 new ControlPlaneStorageError({55 new ControlPlaneStorageError({
77 operation,56 operation,
78 message: error.message,57 message: error.message,
79 details: error.details ?? "Persistence operation failed",58 details: error.message,
80 }),59 }),
81 unknownPersistence: (cause, details) =>
82 unknownPersistenceError(operation, cause, details),
83 unknownStorage: (cause, details) =>60 unknownStorage: (cause, details) =>
84 self.storage(self.unknownPersistence(cause, details)),61 self.storage(
62 cause instanceof Error
63 ? new Error(`${cause.message}: ${details}`)
64 : new Error(details),
65 ),
85 mapStorage: (effect) =>66 mapStorage: (effect) =>
86 effect.pipe(67 effect.pipe(
87 Effect.mapError(self.storage),68 Effect.mapError((error) => self.storage(error)),
88 ),69 ),
89 };70 };
90 71
packages/control-plane/src/runtime/operations-shared.ts+4−17
@@ -2,7 +2,6 @@ import { · 1 unmodified line
2 ControlPlaneBadRequestError,2 ControlPlaneBadRequestError,
3 ControlPlaneStorageError,3 ControlPlaneStorageError,
4} from "../api/errors";4} from "../api/errors";
5import { ControlPlanePersistenceError } from "#persistence";
6import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
7 6
8import {7import {
@@ -11,26 +10,14 @@ import { · 2 unmodified lines
11} from "./operation-errors";10} from "./operation-errors";
12export type Mutable<T> = { -readonly [K in keyof T]: T[K] };11export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
13 12
14const isUniqueViolation = (error: ControlPlanePersistenceError): boolean =>13export const mapPersistenceError = <A, R>(
15 error.kind === "unique_violation";
16
17export const mapPersistenceError = <A>(
18 operation: OperationErrorsLike,14 operation: OperationErrorsLike,
19 effect: Effect.Effect<A, ControlPlanePersistenceError | Error>,15 effect: Effect.Effect<A, Error, R>,
20): Effect.Effect<A, ControlPlaneBadRequestError | ControlPlaneStorageError> =>16): Effect.Effect<A, ControlPlaneBadRequestError | ControlPlaneStorageError, R> =>
21 effect.pipe(17 effect.pipe(
22 Effect.mapError((error) => {18 Effect.mapError((error) => {
23 const errors = asOperationErrors(operation);19 const errors = asOperationErrors(operation);
24 if (error instanceof ControlPlanePersistenceError) {20 return errors.storage(error);
25 return isUniqueViolation(error)
26 ? errors.badRequest("Unique constraint violation", error.details ?? "duplicate key")
27 : errors.storage(error);
28 }
29
30 return errors.unknownStorage(
31 error,
32 error.message,
33 );
34 }),21 }),
35 );22 );
36 23
packages/control-plane/src/runtime/policies-operations.ts+35−11
@@ -13,17 +13,21 @@ import { · 12 unmodified lines
13} from "#schema";13} from "#schema";
14import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
15 15
16import {
17 loadLocalExecutorConfig,
18 writeProjectLocalExecutorConfig,
19} from "./local-config";
20import {16import {
21 requireRuntimeLocalWorkspace,17 requireRuntimeLocalWorkspace,
22} from "./local-runtime-context";18} from "./local-runtime-context";
19import type {
20 WorkspaceStorageServices,
21 WorkspaceConfigStoreShape,
22 WorkspaceStateStoreShape,
23} from "./local-storage";
24import {
25 WorkspaceConfigStore,
26 WorkspaceStateStore,
27} from "./local-storage";
28import type { ResolvedLocalWorkspaceContext } from "./local-config";
23import {29import {
24 loadLocalWorkspaceState,
25 type LocalWorkspaceState,30 type LocalWorkspaceState,
26 writeLocalWorkspaceState,
27} from "./local-workspace-state";31} from "./local-workspace-state";
28import {32import {
29 derivePolicyConfigKey,33 derivePolicyConfigKey,
@@ -77,8 +81,14 @@ const toLocalWorkspacePolicy = (input: { · 47 unmodified lines
77export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) =>81export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) =>
78 Effect.gen(function* () {82 Effect.gen(function* () {
79 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);83 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
80 const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);84 const workspaceConfigStore = yield* WorkspaceConfigStore;
81 const workspaceState = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context);85 const workspaceStateStore = yield* WorkspaceStateStore;
86 const loadedConfig = yield* workspaceConfigStore.load(
87 runtimeLocalWorkspace.context,
88 );
89 const workspaceState = yield* workspaceStateStore.load(
90 runtimeLocalWorkspace.context,
91 );
82 92
83 const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(([key, policyConfig]) =>93 const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(([key, policyConfig]) =>
84 toLocalWorkspacePolicy({94 toLocalWorkspacePolicy({
@@ -99,16 +109,18 @@ export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => · 14 unmodified lines
99 109
100const writeLocalPolicyFiles = (input: {110const writeLocalPolicyFiles = (input: {
101 operation: OperationErrors;111 operation: OperationErrors;
102 context: Parameters<typeof writeProjectLocalExecutorConfig>[0]["context"];112 context: ResolvedLocalWorkspaceContext;
113 workspaceConfigStore: WorkspaceConfigStoreShape;
114 workspaceStateStore: WorkspaceStateStoreShape;
103 projectConfig: LocalExecutorConfig;115 projectConfig: LocalExecutorConfig;
104 workspaceState: LocalWorkspaceState;116 workspaceState: LocalWorkspaceState;
105}) =>117}) =>
106 Effect.all([118 Effect.all([
107 writeProjectLocalExecutorConfig({119 input.workspaceConfigStore.writeProject({
108 context: input.context,120 context: input.context,
109 config: input.projectConfig,121 config: input.projectConfig,
110 }),122 }),
111 writeLocalWorkspaceState({123 input.workspaceStateStore.write({
112 context: input.context,124 context: input.context,
113 state: input.workspaceState,125 state: input.workspaceState,
114 }),126 }),
@@ -154,6 +166,8 @@ export const createPolicy = (input: { · 39 unmodified lines
154}) =>166}) =>
155 Effect.gen(function* () {167 Effect.gen(function* () {
156 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId);168 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId);
169 const workspaceConfigStore = yield* WorkspaceConfigStore;
170 const workspaceStateStore = yield* WorkspaceStateStore;
157 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(171 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
158 Effect.mapError((cause) =>172 Effect.mapError((cause) =>
159 policyOps.create.unknownStorage(173 policyOps.create.unknownStorage(
@@ -201,6 +215,8 @@ export const createPolicy = (input: { · 41 unmodified lines
201 yield* writeLocalPolicyFiles({215 yield* writeLocalPolicyFiles({
202 operation: policyOps.create,216 operation: policyOps.create,
203 context: runtimeLocalWorkspace.context,217 context: runtimeLocalWorkspace.context,
218 workspaceConfigStore,
219 workspaceStateStore,
204 projectConfig: {220 projectConfig: {
205 ...projectConfig,221 ...projectConfig,
206 policies,222 policies,
@@ -250,6 +266,8 @@ export const updatePolicy = (input: { · 43 unmodified lines
250}) =>266}) =>
251 Effect.gen(function* () {267 Effect.gen(function* () {
252 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId);268 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId);
269 const workspaceConfigStore = yield* WorkspaceConfigStore;
270 const workspaceStateStore = yield* WorkspaceStateStore;
253 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(271 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
254 Effect.mapError((cause) =>272 Effect.mapError((cause) =>
255 policyOps.update.unknownStorage(273 policyOps.update.unknownStorage(
@@ -300,6 +318,8 @@ export const updatePolicy = (input: { · 44 unmodified lines
300 yield* writeLocalPolicyFiles({318 yield* writeLocalPolicyFiles({
301 operation: policyOps.update,319 operation: policyOps.update,
302 context: runtimeLocalWorkspace.context,320 context: runtimeLocalWorkspace.context,
321 workspaceConfigStore,
322 workspaceStateStore,
303 projectConfig: {323 projectConfig: {
304 ...projectConfig,324 ...projectConfig,
305 policies,325 policies,
@@ -322,6 +342,8 @@ export const removePolicy = (input: { · 16 unmodified lines
322}) =>342}) =>
323 Effect.gen(function* () {343 Effect.gen(function* () {
324 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId);344 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId);
345 const workspaceConfigStore = yield* WorkspaceConfigStore;
346 const workspaceStateStore = yield* WorkspaceStateStore;
325 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(347 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(
326 Effect.mapError((cause) =>348 Effect.mapError((cause) =>
327 policyOps.remove.unknownStorage(349 policyOps.remove.unknownStorage(
@@ -343,6 +365,8 @@ export const removePolicy = (input: { · 15 unmodified lines
343 yield* writeLocalPolicyFiles({365 yield* writeLocalPolicyFiles({
344 operation: policyOps.remove,366 operation: policyOps.remove,
345 context: runtimeLocalWorkspace.context,367 context: runtimeLocalWorkspace.context,
368 workspaceConfigStore,
369 workspaceStateStore,
346 projectConfig: {370 projectConfig: {
347 ...projectConfig,371 ...projectConfig,
348 policies,372 policies,
packages/control-plane/src/runtime/secret-material-providers.ts+16−14
@@ -4,7 +4,6 @@ import { promises as fs } from "node:fs"; · 3 unmodified lines
4import { lstatSync, realpathSync } from "node:fs";4import { lstatSync, realpathSync } from "node:fs";
5import { isAbsolute } from "node:path";5import { isAbsolute } from "node:path";
6 6
7import type { SqlControlPlaneRows } from "#persistence";
8import {7import {
9 type LocalConfigSecretProvider,8 type LocalConfigSecretProvider,
10 type LocalExecutorConfig,9 type LocalExecutorConfig,
@@ -17,15 +16,16 @@ import * as Option from "effect/Option"; · 6 unmodified lines
17 16
18import { resolveConfigRelativePath } from "./local-config";17import { resolveConfigRelativePath } from "./local-config";
19import { fromConfigSecretProviderId } from "./local-config-secrets";18import { fromConfigSecretProviderId } from "./local-config-secrets";
19import type { ControlPlaneStoreShape } from "./store";
20 20
21export const ENV_SECRET_PROVIDER_ID = "env";21export const ENV_SECRET_PROVIDER_ID = "env";
22export const PARAMS_SECRET_PROVIDER_ID = "params";22export const PARAMS_SECRET_PROVIDER_ID = "params";
23export const KEYCHAIN_SECRET_PROVIDER_ID = "keychain";23export const KEYCHAIN_SECRET_PROVIDER_ID = "keychain";
24export const POSTGRES_SECRET_PROVIDER_ID = "postgres";24export const LOCAL_SECRET_PROVIDER_ID = "local";
25 25
26export type SecretStoreProviderId =26export type SecretStoreProviderId =
27 | typeof KEYCHAIN_SECRET_PROVIDER_ID27 | typeof KEYCHAIN_SECRET_PROVIDER_ID
28 | typeof POSTGRES_SECRET_PROVIDER_ID;28 | typeof LOCAL_SECRET_PROVIDER_ID;
29 29
30export type SecretMaterialResolveContext = {30export type SecretMaterialResolveContext = {
31 params?: Readonly<Record<string, string | undefined>>;31 params?: Readonly<Record<string, string | undefined>>;
@@ -47,7 +47,7 @@ export type DeleteSecretMaterial = ( · 15 unmodified lines
47) => Effect.Effect<boolean, Error, never>;47) => Effect.Effect<boolean, Error, never>;
48 48
49type SecretMaterialProviderRuntime = {49type SecretMaterialProviderRuntime = {
50 rows: SqlControlPlaneRows;50 rows: ControlPlaneStoreShape;
51 env: NodeJS.ProcessEnv;51 env: NodeJS.ProcessEnv;
52 dangerouslyAllowEnvSecrets: boolean;52 dangerouslyAllowEnvSecrets: boolean;
53 keychainServiceName: string;53 keychainServiceName: string;
@@ -106,8 +106,10 @@ export const parseSecretStoreProviderId = (value: string | undefined): SecretSto · 52 unmodified lines
106 return KEYCHAIN_SECRET_PROVIDER_ID;106 return KEYCHAIN_SECRET_PROVIDER_ID;
107 }107 }
108 108
109 if (normalized === POSTGRES_SECRET_PROVIDER_ID) {109 if (
110 return POSTGRES_SECRET_PROVIDER_ID;110 normalized === LOCAL_SECRET_PROVIDER_ID
111 ) {
112 return LOCAL_SECRET_PROVIDER_ID;
111 }113 }
112 114
113 return null;115 return null;
@@ -119,7 +121,7 @@ const resolveDangerouslyAllowEnvSecrets = (value: boolean | undefined): boolean · 5 unmodified lines
119const resolveSecretStoreProviderId = (value: SecretStoreProviderId | undefined): SecretStoreProviderId =>121const resolveSecretStoreProviderId = (value: SecretStoreProviderId | undefined): SecretStoreProviderId =>
120 value122 value
121 ?? parseSecretStoreProviderId(process.env[SECRET_STORE_PROVIDER_ENV])123 ?? parseSecretStoreProviderId(process.env[SECRET_STORE_PROVIDER_ENV])
122 ?? POSTGRES_SECRET_PROVIDER_ID;124 ?? LOCAL_SECRET_PROVIDER_ID;
123 125
124const resolveKeychainServiceName = (value: string | undefined): string =>126const resolveKeychainServiceName = (value: string | undefined): string =>
125 trimOrNull(value)127 trimOrNull(value)
@@ -239,7 +241,7 @@ const createEnvSecretMaterialProvider = (): SecretMaterialProvider => ({ · 113 unmodified lines
239 remove: () => Effect.succeed(false),241 remove: () => Effect.succeed(false),
240});242});
241 243
242const createPostgresSecretMaterialProvider = (): SecretMaterialProvider => ({244const createLocalSecretMaterialProvider = (): SecretMaterialProvider => ({
243 resolve: ({ ref, runtime }) =>245 resolve: ({ ref, runtime }) =>
244 Effect.gen(function* () {246 Effect.gen(function* () {
245 const materialId = SecretMaterialIdSchema.make(ref.handle);247 const materialId = SecretMaterialIdSchema.make(ref.handle);
@@ -265,7 +267,7 @@ const createPostgresSecretMaterialProvider = (): SecretMaterialProvider => ({ · 19 unmodified lines
265 });267 });
266 268
267 return {269 return {
268 providerId: POSTGRES_SECRET_PROVIDER_ID,270 providerId: LOCAL_SECRET_PROVIDER_ID,
269 handle: id,271 handle: id,
270 } satisfies SecretRef;272 } satisfies SecretRef;
271 }),273 }),
@@ -467,7 +469,7 @@ const createSecretMaterialProviderRegistry = (): SecretMaterialProviderRegistry · 195 unmodified lines
467 [PARAMS_SECRET_PROVIDER_ID, createParamsSecretMaterialProvider()],469 [PARAMS_SECRET_PROVIDER_ID, createParamsSecretMaterialProvider()],
468 [ENV_SECRET_PROVIDER_ID, createEnvSecretMaterialProvider()],470 [ENV_SECRET_PROVIDER_ID, createEnvSecretMaterialProvider()],
469 [KEYCHAIN_SECRET_PROVIDER_ID, createKeychainSecretMaterialProvider()],471 [KEYCHAIN_SECRET_PROVIDER_ID, createKeychainSecretMaterialProvider()],
470 [POSTGRES_SECRET_PROVIDER_ID, createPostgresSecretMaterialProvider()],472 [LOCAL_SECRET_PROVIDER_ID, createLocalSecretMaterialProvider()],
471 ]);473 ]);
472 474
473const getSecretMaterialProvider = (input: {475const getSecretMaterialProvider = (input: {
@@ -483,7 +485,7 @@ const getSecretMaterialProvider = (input: { · 9 unmodified lines
483};485};
484 486
485const createSecretMaterialProviderRuntime = (input: {487const createSecretMaterialProviderRuntime = (input: {
486 rows: SqlControlPlaneRows;488 rows: ControlPlaneStoreShape;
487 dangerouslyAllowEnvSecrets?: boolean;489 dangerouslyAllowEnvSecrets?: boolean;
488 keychainServiceName?: string;490 keychainServiceName?: string;
489 localConfig?: LocalExecutorConfig | null;491 localConfig?: LocalExecutorConfig | null;
@@ -644,7 +646,7 @@ const resolveConfiguredSecretProvider = (input: { · 154 unmodified lines
644};646};
645 647
646export const createDefaultSecretMaterialResolver = (input: {648export const createDefaultSecretMaterialResolver = (input: {
647 rows: SqlControlPlaneRows;649 rows: ControlPlaneStoreShape;
648 dangerouslyAllowEnvSecrets?: boolean;650 dangerouslyAllowEnvSecrets?: boolean;
649 keychainServiceName?: string;651 keychainServiceName?: string;
650 localConfig?: LocalExecutorConfig | null;652 localConfig?: LocalExecutorConfig | null;
@@ -682,7 +684,7 @@ export const createDefaultSecretMaterialResolver = (input: { · 31 unmodified lines
682};684};
683 685
684export const createDefaultSecretMaterialStorer = (input: {686export const createDefaultSecretMaterialStorer = (input: {
685 rows: SqlControlPlaneRows;687 rows: ControlPlaneStoreShape;
686 storeProviderId?: SecretStoreProviderId;688 storeProviderId?: SecretStoreProviderId;
687 dangerouslyAllowEnvSecrets?: boolean;689 dangerouslyAllowEnvSecrets?: boolean;
688 keychainServiceName?: string;690 keychainServiceName?: string;
@@ -714,7 +716,7 @@ export const createDefaultSecretMaterialStorer = (input: { · 25 unmodified lines
714};716};
715 717
716export const createDefaultSecretMaterialDeleter = (input: {718export const createDefaultSecretMaterialDeleter = (input: {
717 rows: SqlControlPlaneRows;719 rows: ControlPlaneStoreShape;
718 dangerouslyAllowEnvSecrets?: boolean;720 dangerouslyAllowEnvSecrets?: boolean;
719 keychainServiceName?: string;721 keychainServiceName?: string;
720}): DeleteSecretMaterial => {722}): DeleteSecretMaterial => {
packages/control-plane/src/runtime/source-adapters/graphql.ts+0−71
@@ -5,7 +5,6 @@ import { · 4 unmodified lines
5 makeToolInvokerFromTools,5 makeToolInvokerFromTools,
6 typeSignatureFromSchemaJson,6 typeSignatureFromSchemaJson,
7} from "@executor/codemode-core";7} from "@executor/codemode-core";
8import type { SqlControlPlaneRows } from "#persistence";
9import type {8import type {
10 GraphqlToolManifest,9 GraphqlToolManifest,
11} from "../graphql-tools";10} from "../graphql-tools";
@@ -41,7 +40,6 @@ import { · 29 unmodified lines
41import type {40import type {
42 SourceAdapter,41 SourceAdapter,
43 SourceAdapterMaterialization,42 SourceAdapterMaterialization,
44 SourceAdapterRepairRevisionInput,
45} from "./types";43} from "./types";
46import {44import {
47 ConnectHttpAuthSchema,45 ConnectHttpAuthSchema,
@@ -285,75 +283,6 @@ const primaryDocument = ( · 237 unmodified lines
285 documents.find((document) => document.documentKind === "graphql_introspection")283 documents.find((document) => document.documentKind === "graphql_introspection")
286 ?? null;284 ?? null;
287 285
288export const rebuildGraphqlRecipeRevisionContent = (input: {
289 rows: SqlControlPlaneRows;
290 sourceRecord: SourceAdapterRepairRevisionInput["sourceRecord"];
291 revision: SourceAdapterRepairRevisionInput["revision"];
292 documents: readonly StoredSourceRecipeDocumentRecord[];
293}): Effect.Effect<void, Error, never> =>
294 Effect.gen(function* () {
295 const manifest =
296 input.revision.manifestJson !== null
297 ? yield* parseJsonValue<GraphqlToolManifest>({
298 label: `GraphQL manifest for ${input.sourceRecord.id}`,
299 value: input.revision.manifestJson,
300 })
301 : null;
302 const document = primaryDocument(input.documents);
303 const resolvedManifest =
304 manifest
305 ?? (document !== null
306 ? yield* extractGraphqlManifest(
307 input.sourceRecord.name,
308 document.contentText,
309 )
310 : null);
311
312 if (resolvedManifest === null) {
313 return yield* Effect.fail(
314 new Error(
315 `Missing GraphQL manifest and document for ${input.sourceRecord.id}`,
316 ),
317 );
318 }
319
320 const now = Math.max(input.revision.updatedAt, input.sourceRecord.updatedAt);
321 const definitions = compileGraphqlToolDefinitions(resolvedManifest);
322 const schemaBundle = toGraphqlSchemaBundleRecord({
323 recipeRevisionId: input.revision.id,
324 manifest: resolvedManifest,
325 now,
326 });
327
328 yield* input.rows.sourceRecipeRevisions.update(input.revision.id, {
329 manifestJson: JSON.stringify(resolvedManifest),
330 manifestHash: resolvedManifest.sourceHash,
331 updatedAt: now,
332 });
333 yield* input.rows.sourceRecipeOperations.replaceForRevision({
334 recipeRevisionId: input.revision.id,
335 operations: definitions.map((definition) =>
336 toGraphqlRecipeOperationRecord({
337 recipeRevisionId: input.revision.id,
338 definition,
339 manifest: resolvedManifest,
340 now,
341 }),
342 ),
343 });
344 yield* input.rows.sourceRecipeSchemaBundles.replaceForRevision({
345 recipeRevisionId: input.revision.id,
346 bundles: schemaBundle ? [schemaBundle] : [],
347 });
348
349 if (input.sourceRecord.sourceHash !== resolvedManifest.sourceHash) {
350 yield* input.rows.sources.update(input.sourceRecord.workspaceId, input.sourceRecord.id, {
351 sourceHash: resolvedManifest.sourceHash,
352 updatedAt: now,
353 });
354 }
355 });
356
357export const graphqlSourceAdapter: SourceAdapter = {286export const graphqlSourceAdapter: SourceAdapter = {
358 key: "graphql",287 key: "graphql",
359 displayName: "GraphQL",288 displayName: "GraphQL",
packages/control-plane/src/runtime/source-adapters/mcp.ts+0−19
@@ -9,7 +9,6 @@ import { · 8 unmodified lines
9 type McpToolManifest,9 type McpToolManifest,
10 type McpToolManifestEntry,10 type McpToolManifestEntry,
11} from "@executor/codemode-mcp";11} from "@executor/codemode-mcp";
12import type { SqlControlPlaneRows } from "#persistence";
13import type {12import type {
14 Source,13 Source,
15 SourceRecipeRevisionId,14 SourceRecipeRevisionId,
@@ -25,7 +24,6 @@ import * as Schema from "effect/Schema"; · 9 unmodified lines
25import {24import {
26 contentHash,25 contentHash,
27 normalizeSearchText,26 normalizeSearchText,
28 persistRecipeMaterialization,
29 type SourceRecipeMaterialization,27 type SourceRecipeMaterialization,
30} from "../source-recipe-support";28} from "../source-recipe-support";
31import { namespaceFromSourceName } from "../source-names";29import { namespaceFromSourceName } from "../source-names";
@@ -170,23 +168,6 @@ const toMcpRecipeOperationRecord = (input: { · 138 unmodified lines
170 updatedAt: input.now,168 updatedAt: input.now,
171});169});
172 170
173export const persistMcpRecipeRevisionFromManifestEntries = (input: {
174 rows: SqlControlPlaneRows;
175 source: Source;
176 manifestEntries: readonly McpToolManifestEntry[];
177}): Effect.Effect<void, Error, never> =>
178 Effect.gen(function* () {
179 yield* persistRecipeMaterialization({
180 rows: input.rows,
181 source: input.source,
182 materialization: materializationFromMcpManifestEntries({
183 recipeRevisionId: "src_recipe_rev_materialization" as SourceRecipeRevisionId,
184 endpoint: input.source.endpoint,
185 manifestEntries: input.manifestEntries,
186 }),
187 });
188 });
189
190export const materializationFromMcpManifestEntries = (input: {171export const materializationFromMcpManifestEntries = (input: {
191 recipeRevisionId: SourceRecipeRevisionId;172 recipeRevisionId: SourceRecipeRevisionId;
192 endpoint: string;173 endpoint: string;
packages/control-plane/src/runtime/source-adapters/openapi.ts+0−70
@@ -3,7 +3,6 @@ import { · 2 unmodified lines
3 HttpClient,3 HttpClient,
4 HttpClientRequest,4 HttpClientRequest,
5} from "@effect/platform";5} from "@effect/platform";
6import type { SqlControlPlaneRows } from "#persistence";
7import {6import {
8 applyCookiePlacementsToHeaders,7 applyCookiePlacementsToHeaders,
9 applyHttpQueryPlacementsToUrl,8 applyHttpQueryPlacementsToUrl,
@@ -41,7 +40,6 @@ import { · 31 unmodified lines
41import type {40import type {
42 SourceAdapter,41 SourceAdapter,
43 SourceAdapterMaterialization,42 SourceAdapterMaterialization,
44 SourceAdapterRepairRevisionInput,
45} from "./types";43} from "./types";
46import {44import {
47 ConnectHttpAuthSchema,45 ConnectHttpAuthSchema,
@@ -330,74 +328,6 @@ const primaryDocument = ( · 282 unmodified lines
330): StoredSourceRecipeDocumentRecord | null =>328): StoredSourceRecipeDocumentRecord | null =>
331 documents.find((document) => document.documentKind === "openapi") ?? null;329 documents.find((document) => document.documentKind === "openapi") ?? null;
332 330
333export const rebuildOpenApiRecipeRevisionContent = (input: {
334 rows: SqlControlPlaneRows;
335 sourceRecord: SourceAdapterRepairRevisionInput["sourceRecord"];
336 revision: SourceAdapterRepairRevisionInput["revision"];
337 documents: readonly StoredSourceRecipeDocumentRecord[];
338}): Effect.Effect<void, Error, never> =>
339 Effect.gen(function* () {
340 const manifest =
341 input.revision.manifestJson !== null
342 ? ((yield* parseJsonValue<OpenApiToolManifest>({
343 label: `OpenAPI manifest for ${input.sourceRecord.id}`,
344 value: input.revision.manifestJson,
345 })) as OpenApiToolManifest | null)
346 : null;
347 const document = primaryDocument(input.documents);
348 const resolvedManifest =
349 manifest
350 ?? (document !== null
351 ? yield* extractOpenApiManifest(
352 input.sourceRecord.name,
353 document.contentText,
354 )
355 : null);
356
357 if (resolvedManifest === null) {
358 return yield* Effect.fail(
359 new Error(
360 `Missing OpenAPI manifest and document for ${input.sourceRecord.id}`,
361 ),
362 );
363 }
364
365 const now = Math.max(input.revision.updatedAt, input.sourceRecord.updatedAt);
366 const definitions = compileOpenApiToolDefinitions(resolvedManifest);
367 const schemaBundle = toOpenApiSchemaBundleRecord({
368 recipeRevisionId: input.revision.id,
369 refTable: resolvedManifest.refHintTable,
370 now,
371 });
372
373 yield* input.rows.sourceRecipeRevisions.update(input.revision.id, {
374 manifestJson: JSON.stringify(resolvedManifest),
375 manifestHash: resolvedManifest.sourceHash,
376 updatedAt: now,
377 });
378 yield* input.rows.sourceRecipeOperations.replaceForRevision({
379 recipeRevisionId: input.revision.id,
380 operations: definitions.map((definition) =>
381 toOpenApiRecipeOperationRecord({
382 recipeRevisionId: input.revision.id,
383 definition,
384 now,
385 }),
386 ),
387 });
388 yield* input.rows.sourceRecipeSchemaBundles.replaceForRevision({
389 recipeRevisionId: input.revision.id,
390 bundles: schemaBundle ? [schemaBundle] : [],
391 });
392
393 if (input.sourceRecord.sourceHash !== resolvedManifest.sourceHash) {
394 yield* input.rows.sources.update(input.sourceRecord.workspaceId, input.sourceRecord.id, {
395 sourceHash: resolvedManifest.sourceHash,
396 updatedAt: now,
397 });
398 }
399 });
400
401export const openApiSourceAdapter: SourceAdapter = {331export const openApiSourceAdapter: SourceAdapter = {
402 key: "openapi",332 key: "openapi",
403 displayName: "OpenAPI",333 displayName: "OpenAPI",
packages/control-plane/src/runtime/source-adapters/types.ts+2−11
@@ -3,7 +3,6 @@ import type { · 2 unmodified lines
3 ToolDescriptor,3 ToolDescriptor,
4 ToolSchemaBundle,4 ToolSchemaBundle,
5} from "@executor/codemode-core";5} from "@executor/codemode-core";
6import type { SqlControlPlaneRows } from "#persistence";
7import type {6import type {
8 AccountId,7 AccountId,
9 CredentialSlot,8 CredentialSlot,
@@ -25,6 +24,7 @@ import * as Schema from "effect/Schema"; · 15 unmodified lines
25 24
26import type { ResolvedSourceAuthMaterial } from "../source-auth-material";25import type { ResolvedSourceAuthMaterial } from "../source-auth-material";
27import type { ResolveSecretMaterial } from "../secret-material-providers";26import type { ResolveSecretMaterial } from "../secret-material-providers";
27import type { ControlPlaneStoreShape } from "../store";
28 28
29export type SourceAdapterFamily = "http_api" | "mcp" | "internal";29export type SourceAdapterFamily = "http_api" | "mcp" | "internal";
30export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;30export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;
@@ -81,17 +81,8 @@ export type SourceAdapterOauth2SetupConfig = { · 50 unmodified lines
81 authorizationParams?: Readonly<Record<string, string>>;81 authorizationParams?: Readonly<Record<string, string>>;
82};82};
83 83
84export type SourceAdapterRepairRevisionInput = {
85 rows: SqlControlPlaneRows;
86 sourceRecord: StoredSourceRecord;
87 revision: StoredSourceRecipeRevisionRecord;
88 documents: readonly StoredSourceRecipeDocumentRecord[];
89 operations: readonly StoredSourceRecipeOperationRecord[];
90 schemaBundles: readonly ToolSchemaBundle[];
91};
92
93export type SourceAdapterInvokePersistedToolInput = {84export type SourceAdapterInvokePersistedToolInput = {
94 rows: SqlControlPlaneRows;85 rows: ControlPlaneStoreShape;
95 workspaceId: Source["workspaceId"];86 workspaceId: Source["workspaceId"];
96 accountId: AccountId;87 accountId: AccountId;
97 source: Source;88 source: Source;
packages/control-plane/src/runtime/source-auth-material.test.ts+5−5
@@ -64,7 +64,7 @@ describe("source-auth-material", () => { · 63 unmodified lines
64 headerName: "X-Api-Key",64 headerName: "X-Api-Key",
65 prefix: "Token ",65 prefix: "Token ",
66 token: {66 token: {
67 providerId: "postgres",67 providerId: "local",
68 handle: "sec_bearer",68 handle: "sec_bearer",
69 },69 },
70 },70 },
@@ -75,7 +75,7 @@ describe("source-auth-material", () => { · 4 unmodified lines
75 },75 },
76 }));76 }));
77 77
78 expect(calls).toEqual(["postgres:sec_bearer"]);78 expect(calls).toEqual(["local:sec_bearer"]);
79 expect(auth).toEqual({79 expect(auth).toEqual({
80 placements: [80 placements: [
81 {81 {
@@ -110,11 +110,11 @@ describe("source-auth-material", () => { · 28 unmodified lines
110 headerName: "Authorization",110 headerName: "Authorization",
111 prefix: "Bearer ",111 prefix: "Bearer ",
112 accessToken: {112 accessToken: {
113 providerId: "postgres",113 providerId: "local",
114 handle: "sec_access",114 handle: "sec_access",
115 },115 },
116 refreshToken: {116 refreshToken: {
117 providerId: "postgres",117 providerId: "local",
118 handle: "sec_refresh",118 handle: "sec_refresh",
119 },119 },
120 },120 },
@@ -125,7 +125,7 @@ describe("source-auth-material", () => { · 4 unmodified lines
125 },125 },
126 }));126 }));
127 127
128 expect(calls).toEqual(["postgres:sec_access"]);128 expect(calls).toEqual(["local:sec_access"]);
129 expect(auth).toEqual({129 expect(auth).toEqual({
130 placements: [130 placements: [
131 {131 {
packages/control-plane/src/runtime/source-auth-material.ts+2−2
@@ -1,4 +1,3 @@
1import type { SqlControlPlaneRows } from "#persistence";
2import type {1import type {
3 AccountId,2 AccountId,
4 CredentialSlot,3 CredentialSlot,
@@ -8,6 +7,7 @@ import type { · 3 unmodified lines
8import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";8import * as Option from "effect/Option";
10 9
10import type { ControlPlaneStoreShape } from "./store";
11import {11import {
12 authArtifactFromSourceAuth,12 authArtifactFromSourceAuth,
13 resolveAuthArtifactMaterial,13 resolveAuthArtifactMaterial,
@@ -42,7 +42,7 @@ export const resolveSourceAuthMaterial = (input: { · 28 unmodified lines
42 source: Source;42 source: Source;
43 slot?: CredentialSlot;43 slot?: CredentialSlot;
44 actorAccountId?: AccountId | null;44 actorAccountId?: AccountId | null;
45 rows?: SqlControlPlaneRows;45 rows?: ControlPlaneStoreShape;
46 resolveSecretMaterial: ResolveSecretMaterial;46 resolveSecretMaterial: ResolveSecretMaterial;
47 context?: SecretMaterialResolveContext;47 context?: SecretMaterialResolveContext;
48}): Effect.Effect<ResolvedSourceAuthMaterial, Error, never> =>48}): Effect.Effect<ResolvedSourceAuthMaterial, Error, never> =>
packages/control-plane/src/runtime/source-auth-service.test.ts+0−608
@@ -1,608 +0,0 @@
1import { mkdtempSync } from "node:fs";
2import { createServer } from "node:http";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5
6import { describe, expect, it } from "vitest";
7import * as Schema from "effect/Schema";
8import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";
10
11import {
12 AccountIdSchema,
13 McpSourceAuthSessionDataJsonSchema,
14 OAuth2PkceSourceAuthSessionDataJsonSchema,
15 SecretMaterialIdSchema,
16 type Source,
17 type SourceAuthSession,
18 WorkspaceIdSchema,
19 decodeAuthLeasePlacementTemplates,
20 decodeBuiltInAuthArtifactConfig,
21} from "#schema";
22import {
23 createSqlControlPlanePersistence,
24 type SqlControlPlanePersistence,
25} from "#persistence";
26
27import {
28 createRuntimeSourceAuthService,
29 createTerminalSourceAuthSessionPatch,
30} from "./source-auth-service";
31import { createLiveExecutionManager } from "./live-execution";
32import {
33 loadLocalExecutorConfig,
34 resolveLocalWorkspaceContext,
35} from "./local-config";
36import {
37 readLocalSourceArtifact,
38} from "./local-source-artifacts";
39import {
40 RuntimeLocalWorkspaceService,
41 type RuntimeLocalWorkspaceState,
42} from "./local-runtime-context";
43import { createDefaultSecretMaterialResolver } from "./secret-material-providers";
44import { resolveSourceAuthMaterial } from "./source-auth-material";
45
46const makeExistingOpenApiSource = (auth: Source["auth"]): Source => ({
47 id: "src_test" as Source["id"],
48 workspaceId: "ws_test" as Source["workspaceId"],
49 name: "GitHub",
50 kind: "openapi",
51 endpoint: "https://api.github.com",
52 status: "connected",
53 enabled: true,
54 namespace: "github",
55 bindingVersion: 1,
56 binding: {
57 specUrl: "https://example.com/openapi.json",
58 defaultHeaders: null,
59 },
60 importAuthPolicy: "reuse_runtime",
61 importAuth: { kind: "none" },
62 auth,
63 sourceHash: null,
64 lastError: null,
65 createdAt: 1,
66 updatedAt: 1,
67});
68
69const fetchLiveDiscoveryDocument = async (): Promise<string> => {
70 const response = await fetch("https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest", {
71 signal: AbortSignal.timeout(20_000),
72 });
73 if (!response.ok) {
74 throw new Error(`Failed to fetch Sheets discovery doc: ${response.status}`);
75 }
76
77 return response.text();
78};
79
80const withGoogleAuthTestServer = async <T>(input: {
81 discoveryDocument: string;
82 handler: (server: {
83 discoveryUrl: string;
84 tokenUrl: string;
85 tokenRequests: Array<URLSearchParams>;
86 }) => Promise<T>;
87}): Promise<T> => {
88 const tokenRequests: Array<URLSearchParams> = [];
89 const server = createServer(async (request, response) => {
90 const requestUrl = new URL(
91 request.url ?? "/",
92 "http://127.0.0.1",
93 );
94
95 if (request.method === "GET" && requestUrl.pathname === "/discovery") {
96 response.statusCode = 200;
97 response.setHeader("content-type", "application/json");
98 response.end(input.discoveryDocument);
99 return;
100 }
101
102 if (request.method === "POST" && requestUrl.pathname === "/token") {
103 const chunks: Buffer[] = [];
104 for await (const chunk of request) {
105 chunks.push(Buffer.from(chunk));
106 }
107 const form = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
108 tokenRequests.push(form);
109
110 response.statusCode = 200;
111 response.setHeader("content-type", "application/json");
112 if (form.get("grant_type") === "authorization_code") {
113 response.end(
114 JSON.stringify({
115 access_token: "access-token-initial",
116 refresh_token: "refresh-token-live",
117 expires_in: 3600,
118 scope: "https://www.googleapis.com/auth/spreadsheets.readonly",
119 token_type: "Bearer",
120 }),
121 );
122 return;
123 }
124
125 response.end(
126 JSON.stringify({
127 access_token: "access-token-refreshed",
128 expires_in: 3600,
129 token_type: "Bearer",
130 }),
131 );
132 return;
133 }
134
135 response.statusCode = 404;
136 response.end();
137 });
138
139 await new Promise<void>((resolve, reject) => {
140 server.listen(0, "127.0.0.1", (error?: Error) => {
141 if (error) {
142 reject(error);
143 return;
144 }
145 resolve();
146 });
147 });
148
149 try {
150 const address = server.address();
151 if (!address || typeof address === "string") {
152 throw new Error("Failed to resolve Google auth test server address");
153 }
154 const baseUrl = `http://127.0.0.1:${address.port}`;
155 return await input.handler({
156 discoveryUrl: `${baseUrl}/discovery`,
157 tokenUrl: `${baseUrl}/token`,
158 tokenRequests,
159 });
160 } finally {
161 await new Promise<void>((resolve, reject) => {
162 server.close((error) => {
163 if (error) {
164 reject(error);
165 return;
166 }
167 resolve();
168 });
169 });
170 }
171};
172
173const makePersistence = async (): Promise<SqlControlPlanePersistence> =>
174 Effect.runPromise(
175 createSqlControlPlanePersistence({
176 localDataDir: ":memory:",
177 }),
178 );
179
180const 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* resolveLocalWorkspaceContext({ workspaceRoot });
190 const loadedConfig = yield* loadLocalExecutorConfig(context);
191
192 return {
193 context,
194 installation: {
195 workspaceId,
196 accountId,
197 },
198 loadedConfig,
199 } satisfies RuntimeLocalWorkspaceState;
200 }),
201 );
202
203describe("source-auth-service", () => {
204 const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema);
205
206 const baseSessionDataJson = encodeSessionDataJson({
207 kind: "mcp_oauth",
208 endpoint: "https://example.com/resource",
209 redirectUri: "http://127.0.0.1/callback",
210 scope: null,
211 resourceMetadataUrl: "https://example.com/resource",
212 authorizationServerUrl: "https://example.com/as",
213 resourceMetadata: {
214 issuer: "https://example.com",
215 },
216 authorizationServerMetadata: {
217 token_endpoint: "https://example.com/token",
218 },
219 clientInformation: {
220 client_id: "abc",
221 },
222 codeVerifier: "verifier",
223 authorizationUrl: "https://example.com/auth",
224 });
225
226 it("clears ephemeral OAuth session fields when failing a session", () => {
227 const patch = createTerminalSourceAuthSessionPatch({
228 sessionDataJson: baseSessionDataJson,
229 status: "failed",
230 now: 123,
231 errorText: "OAuth authorization failed",
232 });
233
234 expect(patch).toMatchObject({
235 sessionDataJson: baseSessionDataJson,
236 status: "failed",
237 errorText: "OAuth authorization failed",
238 completedAt: 123,
239 updatedAt: 123,
240 });
241 });
242
243 it("clears ephemeral OAuth session fields when completing a session", () => {
244 const patch = createTerminalSourceAuthSessionPatch({
245 sessionDataJson: baseSessionDataJson,
246 status: "completed",
247 now: 456,
248 errorText: null,
249 });
250
251 expect(patch).toMatchObject({
252 sessionDataJson: baseSessionDataJson,
253 status: "completed",
254 errorText: null,
255 completedAt: 456,
256 updatedAt: 456,
257 });
258 });
259
260 it("completes Google Discovery desktop OAuth into a refreshable auth artifact and lease", async () => {
261 const discoveryDocument = await fetchLiveDiscoveryDocument();
262
263 const persistence = await makePersistence();
264 try {
265 const workspaceId = WorkspaceIdSchema.make("ws_google_oauth");
266 const accountId = AccountIdSchema.make("acc_google_oauth");
267 const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
268 workspaceId,
269 accountId,
270 );
271
272 const service = createRuntimeSourceAuthService({
273 rows: persistence.rows,
274 liveExecutionManager: createLiveExecutionManager(),
275 getLocalServerBaseUrl: () => "http://localhost:8788",
276 localConfig: runtimeLocalWorkspace.loadedConfig.config,
277 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
278 localWorkspaceState: runtimeLocalWorkspace,
279 });
280
281 await withGoogleAuthTestServer({
282 discoveryDocument,
283 handler: async ({ discoveryUrl, tokenUrl, tokenRequests }) => {
284 const originalFetch = globalThis.fetch;
285 globalThis.fetch = (async (input, init) => {
286 const url = typeof input === "string"
287 ? input
288 : input instanceof URL
289 ? input.toString()
290 : input.url;
291 if (url === "https://oauth2.googleapis.com/token") {
292 return originalFetch(tokenUrl, init);
293 }
294
295 return originalFetch(input as any, init);
296 }) as typeof fetch;
297
298 try {
299 const addResult = await Effect.runPromise(
300 service.addExecutorSource({
301 kind: "google_discovery",
302 workspaceId,
303 actorAccountId: accountId,
304 executionId: null,
305 interactionId: null,
306 service: "sheets",
307 version: "v4",
308 discoveryUrl,
309 scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
310 oauthClient: {
311 clientId: "google-test-client",
312 clientSecret: "google-test-secret",
313 },
314 }).pipe(
315 Effect.provideService(
316 RuntimeLocalWorkspaceService,
317 runtimeLocalWorkspace,
318 ),
319 ),
320 );
321
322 expect(addResult.kind).toBe("oauth_required");
323 if (addResult.kind !== "oauth_required") {
324 return;
325 }
326
327 const sessionOption = await Effect.runPromise(
328 persistence.rows.sourceAuthSessions.getById(addResult.sessionId),
329 );
330 expect(Option.isSome(sessionOption)).toBe(true);
331 const session = Option.getOrNull(sessionOption) as SourceAuthSession;
332 const oauthClientOption = await Effect.runPromise(
333 persistence.rows.sourceOauthClients.getByWorkspaceSourceAndProvider({
334 workspaceId,
335 sourceId: addResult.source.id,
336 providerKey: "google_workspace",
337 }),
338 );
339 expect(Option.isSome(oauthClientOption)).toBe(true);
340 const oauthClient = Option.getOrNull(oauthClientOption)!;
341 expect(oauthClient.clientId).toBe("google-test-client");
342 expect(oauthClient.clientSecretProviderId).toBe("postgres");
343 expect(oauthClient.clientSecretHandle).toBeTruthy();
344 expect(session.providerKind).toBe("oauth2_pkce");
345 const sessionData = Schema.decodeUnknownSync(
346 OAuth2PkceSourceAuthSessionDataJsonSchema,
347 )(session.sessionDataJson);
348 expect(sessionData.redirectUri).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
349 expect(sessionData.tokenEndpoint).toBe("https://oauth2.googleapis.com/token");
350 expect(sessionData.authorizationUrl).toContain(
351 encodeURIComponent(sessionData.redirectUri),
352 );
353
354 const loopbackResponse = await fetch(
355 `${sessionData.redirectUri}/?state=${encodeURIComponent(session.state)}&code=loopback-code`,
356 {
357 redirect: "manual",
358 },
359 );
360 expect(loopbackResponse.status).toBe(302);
361 const completionLocation = loopbackResponse.headers.get("location");
362 expect(completionLocation).toBeTruthy();
363 if (!completionLocation) {
364 return;
365 }
366 const completionUrl = new URL(completionLocation);
367 expect(completionUrl.origin).toBe("http://localhost:8788");
368 expect(completionUrl.searchParams.get("state")).toBe(session.state);
369 expect(completionUrl.searchParams.get("code")).toBe("loopback-code");
370
371 const connectedSource = await Effect.runPromise(
372 service.completeSourceCredentialSetup({
373 workspaceId,
374 sourceId: addResult.source.id,
375 actorAccountId: accountId,
376 state: session.state,
377 code: "authorization-code",
378 }),
379 );
380
381 expect(connectedSource.status).toBe("connected");
382 expect(connectedSource.auth.kind).toBe("oauth2_authorized_user");
383 if (connectedSource.auth.kind !== "oauth2_authorized_user") {
384 return;
385 }
386 expect(connectedSource.auth.clientId).toBe("google-test-client");
387 expect(connectedSource.auth.tokenEndpoint).toBe("https://oauth2.googleapis.com/token");
388 expect(connectedSource.auth.grantSet).toEqual([
389 "https://www.googleapis.com/auth/spreadsheets.readonly",
390 ]);
391
392 expect(tokenRequests).toHaveLength(1);
393 expect(tokenRequests[0]?.get("grant_type")).toBe("authorization_code");
394 expect(tokenRequests[0]?.get("client_id")).toBe("google-test-client");
395 expect(tokenRequests[0]?.get("client_secret")).toBe("google-test-secret");
396 expect(tokenRequests[0]?.get("code")).toBe("authorization-code");
397 expect(tokenRequests[0]?.get("redirect_uri")).toBe(sessionData.redirectUri);
398 expect(tokenRequests[0]?.get("code_verifier")).toBe(sessionData.codeVerifier);
399
400 const artifactOption = await Effect.runPromise(
401 persistence.rows.authArtifacts.getByWorkspaceSourceAndActor({
402 workspaceId,
403 sourceId: connectedSource.id,
404 actorAccountId: accountId,
405 slot: "runtime",
406 }),
407 );
408 expect(Option.isSome(artifactOption)).toBe(true);
409 const artifact = Option.getOrNull(artifactOption)!;
410 const decodedArtifact = decodeBuiltInAuthArtifactConfig(artifact);
411 expect(decodedArtifact?.artifactKind).toBe("oauth2_authorized_user");
412
413 const leaseOption = await Effect.runPromise(
414 persistence.rows.authLeases.getByAuthArtifactId(artifact.id),
415 );
416 expect(Option.isSome(leaseOption)).toBe(true);
417 const initialLease = Option.getOrNull(leaseOption)!;
418 const initialTemplates = decodeAuthLeasePlacementTemplates(initialLease);
419 expect(initialTemplates).toEqual([
420 {
421 location: "header",
422 name: "Authorization",
423 parts: [
424 {
425 kind: "literal",
426 value: "Bearer ",
427 },
428 {
429 kind: "secret_ref",
430 ref: {
431 providerId: "postgres",
432 handle: expect.any(String),
433 },
434 },
435 ],
436 },
437 ]);
438
439 const resolver = createDefaultSecretMaterialResolver({
440 rows: persistence.rows,
441 });
442 const resolvedInitialAuth = await Effect.runPromise(
443 resolveSourceAuthMaterial({
444 source: connectedSource,
445 slot: "runtime",
446 actorAccountId: accountId,
447 rows: persistence.rows,
448 resolveSecretMaterial: resolver,
449 }),
450 );
451 expect(resolvedInitialAuth.headers.Authorization).toBe(
452 "Bearer access-token-initial",
453 );
454
455 await Effect.runPromise(
456 persistence.rows.authLeases.upsert({
457 ...initialLease,
458 refreshAfter: Date.now() - 1,
459 expiresAt: Date.now() + 10_000,
460 updatedAt: Date.now(),
461 }),
462 );
463
464 const previousAccessTokenHandle = initialTemplates?.[0]?.parts[1];
465 const refreshedAuth = await Effect.runPromise(
466 resolveSourceAuthMaterial({
467 source: connectedSource,
468 slot: "runtime",
469 actorAccountId: accountId,
470 rows: persistence.rows,
471 resolveSecretMaterial: resolver,
472 }),
473 );
474 expect(refreshedAuth.headers.Authorization).toBe(
475 "Bearer access-token-refreshed",
476 );
477 expect(tokenRequests).toHaveLength(2);
478 expect(tokenRequests[1]?.get("grant_type")).toBe("refresh_token");
479 expect(tokenRequests[1]?.get("refresh_token")).toBe("refresh-token-live");
480
481 if (previousAccessTokenHandle?.kind === "secret_ref") {
482 const oldSecret = await Effect.runPromise(
483 persistence.rows.secretMaterials.getById(
484 SecretMaterialIdSchema.make(previousAccessTokenHandle.ref.handle),
485 ),
486 );
487 expect(Option.isNone(oldSecret)).toBe(true);
488 }
489
490 const localArtifact = await Effect.runPromise(readLocalSourceArtifact({
491 context: runtimeLocalWorkspace.context,
492 sourceId: connectedSource.id,
493 }));
494 expect(localArtifact).not.toBeNull();
495 expect(localArtifact?.operations.length).toBeGreaterThan(0);
496 expect(
497 localArtifact?.operations.some(
498 (operation) => operation.toolId === "spreadsheets.values.get",
499 ),
500 ).toBe(true);
501 } finally {
502 globalThis.fetch = originalFetch;
503 }
504 },
505 });
506 } finally {
507 await persistence.close();
508 }
509 }, 60_000);
510
511 it("uses the request origin callback for Google Discovery OAuth started from the web app", async () => {
512 const discoveryDocument = await fetchLiveDiscoveryDocument();
513
514 const persistence = await makePersistence();
515 try {
516 const workspaceId = WorkspaceIdSchema.make("ws_google_oauth_web");
517 const accountId = AccountIdSchema.make("acc_google_oauth_web");
518 const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
519 workspaceId,
520 accountId,
521 );
522
523 const service = createRuntimeSourceAuthService({
524 rows: persistence.rows,
525 liveExecutionManager: createLiveExecutionManager(),
526 getLocalServerBaseUrl: () => "http://127.0.0.1:8788",
527 localConfig: runtimeLocalWorkspace.loadedConfig.config,
528 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
529 localWorkspaceState: runtimeLocalWorkspace,
530 });
531
532 await withGoogleAuthTestServer({
533 discoveryDocument,
534 handler: async ({ discoveryUrl, tokenUrl }) => {
535 const originalFetch = globalThis.fetch;
536 globalThis.fetch = (async (input, init) => {
537 const url = typeof input === "string"
538 ? input
539 : input instanceof URL
540 ? input.toString()
541 : input.url;
542 if (url === "https://oauth2.googleapis.com/token") {
543 return originalFetch(tokenUrl, init);
544 }
545
546 return originalFetch(input as any, init);
547 }) as typeof fetch;
548
549 try {
550 const addResult = await Effect.runPromise(
551 service.addExecutorSource(
552 {
553 kind: "google_discovery",
554 workspaceId,
555 actorAccountId: accountId,
556 executionId: null,
557 interactionId: null,
558 service: "sheets",
559 version: "v4",
560 discoveryUrl,
561 scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
562 oauthClient: {
563 clientId: "google-test-client",
564 clientSecret: "google-test-secret",
565 },
566 },
567 {
568 baseUrl: "https://app.executor.dev",
569 },
570 ).pipe(
571 Effect.provideService(
572 RuntimeLocalWorkspaceService,
573 runtimeLocalWorkspace,
574 ),
575 ),
576 );
577
578 expect(addResult.kind).toBe("oauth_required");
579 if (addResult.kind !== "oauth_required") {
580 return;
581 }
582
583 const sessionOption = await Effect.runPromise(
584 persistence.rows.sourceAuthSessions.getById(addResult.sessionId),
585 );
586 expect(Option.isSome(sessionOption)).toBe(true);
587 const session = Option.getOrNull(sessionOption) as SourceAuthSession;
588 const sessionData = Schema.decodeUnknownSync(
589 OAuth2PkceSourceAuthSessionDataJsonSchema,
590 )(session.sessionDataJson);
591
592 expect(sessionData.redirectUri).toBe(
593 `https://app.executor.dev/v1/workspaces/${encodeURIComponent(workspaceId)}/sources/${encodeURIComponent(addResult.source.id)}/credentials/oauth/complete`,
594 );
595 expect(sessionData.authorizationUrl).toContain(
596 encodeURIComponent(sessionData.redirectUri),
597 );
598 expect(sessionData.redirectUri).not.toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
599 } finally {
600 globalThis.fetch = originalFetch;
601 }
602 },
603 });
604 } finally {
605 await persistence.close();
606 }
607 }, 60_000);
608});
packages/control-plane/src/runtime/source-auth-service.ts+27−25
@@ -3,10 +3,6 @@ import { · 2 unmodified lines
3 discoverMcpToolsFromConnector,3 discoverMcpToolsFromConnector,
4 type McpDiscoveryElicitationContext,4 type McpDiscoveryElicitationContext,
5} from "@executor/codemode-mcp";5} from "@executor/codemode-mcp";
6import {
7 SqlControlPlaneRowsService,
8 type SqlControlPlaneRows,
9} from "#persistence";
10import {6import {
11 AccountId,7 AccountId,
12 type CredentialSlot,8 type CredentialSlot,
@@ -86,6 +82,8 @@ import { · 73 unmodified lines
86 loadSourcesInWorkspace,82 loadSourcesInWorkspace,
87 persistSource,83 persistSource,
88} from "./source-store";84} from "./source-store";
85import type { WorkspaceStorageServices } from "./local-storage";
86import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
89 87
90const trimOrNull = (value: string | null | undefined): string | null => {88const trimOrNull = (value: string | null | undefined): string | null => {
91 if (value === null || value === undefined) {89 if (value === null || value === undefined) {
@@ -303,7 +301,7 @@ const mergeOauth2PkceSourceAuthSessionData = (input: { · 211 unmodified lines
303 301
304 302
305const completeLiveInteraction = (input: {303const completeLiveInteraction = (input: {
306 rows: SqlControlPlaneRows;304 rows: ControlPlaneStoreShape;
307 liveExecutionManager: LiveExecutionManager;305 liveExecutionManager: LiveExecutionManager;
308 session: SourceAuthSession;306 session: SourceAuthSession;
309 response: {307 response: {
@@ -360,7 +358,7 @@ const serializeJson = (value: unknown): string | null => { · 50 unmodified lines
360 return JSON.stringify(value);358 return JSON.stringify(value);
361};359};
362 360
363const updateSourceStatus = (rows: SqlControlPlaneRows, source: Source, input: {361const updateSourceStatus = (rows: ControlPlaneStoreShape, source: Source, input: {
364 actorAccountId?: AccountId | null;362 actorAccountId?: AccountId | null;
365 status: Source["status"];363 status: Source["status"];
366 lastError?: string | null;364 lastError?: string | null;
@@ -743,7 +741,7 @@ const sourceOauthClientSecretRef = (client: { · 376 unmodified lines
743 : null;741 : null;
744 742
745const upsertSourceOauthClient = (input: {743const upsertSourceOauthClient = (input: {
746 rows: SqlControlPlaneRows;744 rows: ControlPlaneStoreShape;
747 source: Source;745 source: Source;
748 oauthClient: SourceOauthClientInput;746 oauthClient: SourceOauthClientInput;
749 storeSecretMaterial: StoreSecretMaterial;747 storeSecretMaterial: StoreSecretMaterial;
@@ -825,7 +823,7 @@ const upsertSourceOauthClient = (input: { · 75 unmodified lines
825 });823 });
826 824
827const resolveExistingSourceOauthClient = (input: {825const resolveExistingSourceOauthClient = (input: {
828 rows: SqlControlPlaneRows;826 rows: ControlPlaneStoreShape;
829 source: Source;827 source: Source;
830}): Effect.Effect<ResolvedSourceOauthClient | null, Error, never> =>828}): Effect.Effect<ResolvedSourceOauthClient | null, Error, never> =>
831 Effect.gen(function* () {829 Effect.gen(function* () {
@@ -858,7 +856,7 @@ const resolveExistingSourceOauthClient = (input: { · 26 unmodified lines
858 });856 });
859 857
860const startOauth2PkceSourceCredentialSetup = (input: {858const startOauth2PkceSourceCredentialSetup = (input: {
861 rows: SqlControlPlaneRows;859 rows: ControlPlaneStoreShape;
862 source: Source;860 source: Source;
863 actorAccountId?: AccountId | null;861 actorAccountId?: AccountId | null;
864 executionId?: SourceAuthSession["executionId"];862 executionId?: SourceAuthSession["executionId"];
@@ -866,7 +864,11 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 1 unmodified line
866 baseUrl: string;864 baseUrl: string;
867 redirectModeOverride?: WorkspaceSourceOauthClientRedirectMode;865 redirectModeOverride?: WorkspaceSourceOauthClientRedirectMode;
868 storeSecretMaterial: StoreSecretMaterial;866 storeSecretMaterial: StoreSecretMaterial;
869}): Effect.Effect<Extract<ExecutorSourceAddResult, { kind: "oauth_required" }> | null, Error, never> =>867}): Effect.Effect<
868 Extract<ExecutorSourceAddResult, { kind: "oauth_required" }> | null,
869 Error,
870 WorkspaceStorageServices
871> =>
870 Effect.gen(function* () {872 Effect.gen(function* () {
871 const adapter = getSourceAdapterForSource(input.source);873 const adapter = getSourceAdapterForSource(input.source);
872 const setupConfig = adapter.getOauth2SetupConfig874 const setupConfig = adapter.getOauth2SetupConfig
@@ -971,7 +973,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 98 unmodified lines
971 });973 });
972 974
973const connectMcpSourceInternal = (input: {975const connectMcpSourceInternal = (input: {
974 rows: SqlControlPlaneRows;976 rows: ControlPlaneStoreShape;
975 getLocalServerBaseUrl?: () => string | undefined;977 getLocalServerBaseUrl?: () => string | undefined;
976 baseUrl?: string | null;978 baseUrl?: string | null;
977 workspaceId: WorkspaceId;979 workspaceId: WorkspaceId;
@@ -988,7 +990,7 @@ const connectMcpSourceInternal = (input: { · 10 unmodified lines
988 headers?: StringMap | null;990 headers?: StringMap | null;
989 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;991 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;
990 resolveSecretMaterial: ResolveSecretMaterial;992 resolveSecretMaterial: ResolveSecretMaterial;
991}): Effect.Effect<McpSourceConnectResult, Error, never> =>993}): Effect.Effect<McpSourceConnectResult, Error, WorkspaceStorageServices> =>
992 Effect.gen(function* () {994 Effect.gen(function* () {
993 const normalizedEndpoint = normalizeEndpoint(input.endpoint);995 const normalizedEndpoint = normalizeEndpoint(input.endpoint);
994 const existing = yield* (996 const existing = yield* (
@@ -1200,13 +1202,13 @@ const connectMcpSourceInternal = (input: { · 205 unmodified lines
1200 });1202 });
1201 1203
1202const addExecutorHttpSource = (input: {1204const addExecutorHttpSource = (input: {
1203 rows: SqlControlPlaneRows;1205 rows: ControlPlaneStoreShape;
1204 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;1206 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;
1205 storeSecretMaterial: StoreSecretMaterial;1207 storeSecretMaterial: StoreSecretMaterial;
1206 resolveSecretMaterial: ResolveSecretMaterial;1208 resolveSecretMaterial: ResolveSecretMaterial;
1207 getLocalServerBaseUrl?: () => string | undefined;1209 getLocalServerBaseUrl?: () => string | undefined;
1208 baseUrl?: string | null;1210 baseUrl?: string | null;
1209}): Effect.Effect<ExecutorSourceAddResult, Error, never> =>1211}): Effect.Effect<ExecutorSourceAddResult, Error, WorkspaceStorageServices> =>
1210 Effect.gen(function* () {1212 Effect.gen(function* () {
1211 const normalizedEndpoint = normalizeEndpoint(input.sourceInput.endpoint);1213 const normalizedEndpoint = normalizeEndpoint(input.sourceInput.endpoint);
1212 const normalizedSpecUrl = input.sourceInput.kind === "openapi"1214 const normalizedSpecUrl = input.sourceInput.kind === "openapi"
@@ -1403,13 +1405,13 @@ const addExecutorHttpSource = (input: { · 190 unmodified lines
1403 });1405 });
1404 1406
1405const addExecutorGoogleDiscoverySource = (input: {1407const addExecutorGoogleDiscoverySource = (input: {
1406 rows: SqlControlPlaneRows;1408 rows: ControlPlaneStoreShape;
1407 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;1409 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;
1408 storeSecretMaterial: StoreSecretMaterial;1410 storeSecretMaterial: StoreSecretMaterial;
1409 resolveSecretMaterial: ResolveSecretMaterial;1411 resolveSecretMaterial: ResolveSecretMaterial;
1410 getLocalServerBaseUrl?: () => string | undefined;1412 getLocalServerBaseUrl?: () => string | undefined;
1411 baseUrl?: string | null;1413 baseUrl?: string | null;
1412}): Effect.Effect<ExecutorSourceAddResult, Error, never> =>1414}): Effect.Effect<ExecutorSourceAddResult, Error, WorkspaceStorageServices> =>
1413 Effect.gen(function* () {1415 Effect.gen(function* () {
1414 const normalizedService = input.sourceInput.service.trim();1416 const normalizedService = input.sourceInput.service.trim();
1415 const normalizedVersion = input.sourceInput.version.trim();1417 const normalizedVersion = input.sourceInput.version.trim();
@@ -1627,7 +1629,7 @@ type RuntimeSourceAuthServiceShape = { · 211 unmodified lines
1627 workspaceId: WorkspaceId;1629 workspaceId: WorkspaceId;
1628 sourceId: Source["id"];1630 sourceId: Source["id"];
1629 actorAccountId?: AccountId | null;1631 actorAccountId?: AccountId | null;
1630 }) => Effect.Effect<Source, Error, never>;1632 }) => Effect.Effect<Source, Error, WorkspaceStorageServices>;
1631 getLocalServerBaseUrl: () => string | null;1633 getLocalServerBaseUrl: () => string | null;
1632 storeSecretMaterial: (input: {1634 storeSecretMaterial: (input: {
1633 purpose: SecretMaterialPurpose;1635 purpose: SecretMaterialPurpose;
@@ -1639,10 +1641,10 @@ type RuntimeSourceAuthServiceShape = { · 5 unmodified lines
1639 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;1641 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;
1640 baseUrl?: string | null;1642 baseUrl?: string | null;
1641 },1643 },
1642 ) => Effect.Effect<ExecutorSourceAddResult, Error, never>;1644 ) => Effect.Effect<ExecutorSourceAddResult, Error, WorkspaceStorageServices>;
1643 connectMcpSource: (1645 connectMcpSource: (
1644 input: ConnectMcpSourceInput,1646 input: ConnectMcpSourceInput,
1645 ) => Effect.Effect<McpSourceConnectResult, Error, never>;1647 ) => Effect.Effect<McpSourceConnectResult, Error, WorkspaceStorageServices>;
1646 startSourceOAuthSession: (1648 startSourceOAuthSession: (
1647 input: StartSourceOAuthSessionInput,1649 input: StartSourceOAuthSessionInput,
1648 ) => Effect.Effect<StartSourceOAuthSessionResult, Error, never>;1650 ) => Effect.Effect<StartSourceOAuthSessionResult, Error, never>;
@@ -1651,7 +1653,7 @@ type RuntimeSourceAuthServiceShape = { · 2 unmodified lines
1651 code?: string | null;1653 code?: string | null;
1652 error?: string | null;1654 error?: string | null;
1653 errorDescription?: string | null;1655 errorDescription?: string | null;
1654 }) => Effect.Effect<CompleteSourceOAuthSessionResult, Error, never>;1656 }) => Effect.Effect<CompleteSourceOAuthSessionResult, Error, WorkspaceStorageServices>;
1655 completeSourceCredentialSetup: (input: {1657 completeSourceCredentialSetup: (input: {
1656 workspaceId: WorkspaceId;1658 workspaceId: WorkspaceId;
1657 sourceId: Source["id"];1659 sourceId: Source["id"];
@@ -1660,11 +1662,11 @@ type RuntimeSourceAuthServiceShape = { · 2 unmodified lines
1660 code?: string | null;1662 code?: string | null;
1661 error?: string | null;1663 error?: string | null;
1662 errorDescription?: string | null;1664 errorDescription?: string | null;
1663 }) => Effect.Effect<Source, Error, never>;1665 }) => Effect.Effect<Source, Error, WorkspaceStorageServices>;
1664};1666};
1665 1667
1666export const createRuntimeSourceAuthService = (input: {1668export const createRuntimeSourceAuthService = (input: {
1667 rows: SqlControlPlaneRows;1669 rows: ControlPlaneStoreShape;
1668 liveExecutionManager: LiveExecutionManager;1670 liveExecutionManager: LiveExecutionManager;
1669 getLocalServerBaseUrl?: () => string | undefined;1671 getLocalServerBaseUrl?: () => string | undefined;
1670 localConfig?: LocalExecutorConfig | null;1672 localConfig?: LocalExecutorConfig | null;
@@ -1681,11 +1683,11 @@ export const createRuntimeSourceAuthService = (input: { · 10 unmodified lines
1681 });1683 });
1682 const mirrorLocalSourceResult = (1684 const mirrorLocalSourceResult = (
1683 result: ExecutorSourceAddResult,1685 result: ExecutorSourceAddResult,
1684 ): Effect.Effect<ExecutorSourceAddResult, Error, never> =>1686 ): Effect.Effect<ExecutorSourceAddResult, Error, WorkspaceStorageServices> =>
1685 Effect.succeed(result);1687 Effect.succeed(result);
1686 const mirrorLocalMcpSourceResult = (1688 const mirrorLocalMcpSourceResult = (
1687 result: McpSourceConnectResult,1689 result: McpSourceConnectResult,
1688 ): Effect.Effect<McpSourceConnectResult, Error, never> =>1690 ): Effect.Effect<McpSourceConnectResult, Error, WorkspaceStorageServices> =>
1689 Effect.succeed(result);1691 Effect.succeed(result);
1690 const provideLocalWorkspace = <A, E, R>(effect: Effect.Effect<A, E, R>) =>1692 const provideLocalWorkspace = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1691 input.localWorkspaceState1693 input.localWorkspaceState
@@ -2250,7 +2252,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 558 unmodified lines
2250 Layer.effect(2252 Layer.effect(
2251 RuntimeSourceAuthServiceTag,2253 RuntimeSourceAuthServiceTag,
2252 Effect.gen(function* () {2254 Effect.gen(function* () {
2253 const rows = yield* SqlControlPlaneRowsService;2255 const rows = yield* ControlPlaneStore;
2254 const liveExecutionManager = yield* LiveExecutionManagerService;2256 const liveExecutionManager = yield* LiveExecutionManagerService;
2255 2257
2256 return createRuntimeSourceAuthService({2258 return createRuntimeSourceAuthService({
packages/control-plane/src/runtime/source-definitions.test.ts+11−11
@@ -129,7 +129,7 @@ describe("source-definitions", () => { · 128 unmodified lines
129 headerName: "Authorization",129 headerName: "Authorization",
130 prefix: "Bearer ",130 prefix: "Bearer ",
131 token: {131 token: {
132 providerId: "postgres",132 providerId: "local",
133 handle: "sec_token",133 handle: "sec_token",
134 },134 },
135 },135 },
@@ -166,7 +166,7 @@ describe("source-definitions", () => { · 30 unmodified lines
166 headerName: " ",166 headerName: " ",
167 prefix: undefined,167 prefix: undefined,
168 accessToken: {168 accessToken: {
169 providerId: " postgres ",169 providerId: " local ",
170 handle: " sec_access ",170 handle: " sec_access ",
171 },171 },
172 refreshToken: null,172 refreshToken: null,
@@ -180,7 +180,7 @@ describe("source-definitions", () => { · 7 unmodified lines
180 headerName: "Authorization",180 headerName: "Authorization",
181 prefix: "Bearer ",181 prefix: "Bearer ",
182 accessToken: {182 accessToken: {
183 providerId: "postgres",183 providerId: "local",
184 handle: "sec_access",184 handle: "sec_access",
185 },185 },
186 refreshToken: null,186 refreshToken: null,
@@ -222,11 +222,11 @@ describe("source-definitions", () => { · 35 unmodified lines
222 headerName: "Authorization",222 headerName: "Authorization",
223 prefix: "Bearer ",223 prefix: "Bearer ",
224 accessToken: {224 accessToken: {
225 providerId: "postgres",225 providerId: "local",
226 handle: "sec_access",226 handle: "sec_access",
227 },227 },
228 refreshToken: {228 refreshToken: {
229 providerId: "postgres",229 providerId: "local",
230 handle: " ",230 handle: " ",
231 },231 },
232 } as never,232 } as never,
@@ -319,7 +319,7 @@ describe("source-definitions", () => { · 86 unmodified lines
319 headerName: " ",319 headerName: " ",
320 prefix: "Token ",320 prefix: "Token ",
321 token: {321 token: {
322 providerId: " postgres ",322 providerId: " local ",
323 handle: " sec_token ",323 handle: " sec_token ",
324 },324 },
325 } as never,325 } as never,
@@ -332,7 +332,7 @@ describe("source-definitions", () => { · 6 unmodified lines
332 headerName: "Authorization",332 headerName: "Authorization",
333 prefix: "Token ",333 prefix: "Token ",
334 token: {334 token: {
335 providerId: "postgres",335 providerId: "local",
336 handle: "sec_token",336 handle: "sec_token",
337 },337 },
338 });338 });
@@ -353,7 +353,7 @@ describe("source-definitions", () => { · 14 unmodified lines
353 headerName: "Authorization",353 headerName: "Authorization",
354 prefix: "Bearer ",354 prefix: "Bearer ",
355 token: {355 token: {
356 providerId: "postgres",356 providerId: "local",
357 handle: "sec_bearer",357 handle: "sec_bearer",
358 },358 },
359 },359 },
@@ -397,11 +397,11 @@ describe("source-definitions", () => { · 37 unmodified lines
397 headerName: "Authorization",397 headerName: "Authorization",
398 prefix: "Bearer ",398 prefix: "Bearer ",
399 accessToken: {399 accessToken: {
400 providerId: "postgres",400 providerId: "local",
401 handle: "sec_access",401 handle: "sec_access",
402 },402 },
403 refreshToken: {403 refreshToken: {
404 providerId: "postgres",404 providerId: "local",
405 handle: "sec_refresh",405 handle: "sec_refresh",
406 },406 },
407 },407 },
@@ -416,7 +416,7 @@ describe("source-definitions", () => { · 8 unmodified lines
416 headerName: "Authorization",416 headerName: "Authorization",
417 prefix: "Bearer ",417 prefix: "Bearer ",
418 accessToken: {418 accessToken: {
419 providerId: "postgres",419 providerId: "local",
420 handle: "sec_access",420 handle: "sec_access",
421 },421 },
422 refreshToken: null,422 refreshToken: null,
packages/control-plane/src/runtime/source-inspection.test.ts+0−344
@@ -1,344 +0,0 @@
1import { mkdtempSync } from "node:fs";
2import { tmpdir } from "node:os";
3import { join } from "node:path";
4
5import { describe, expect, it } from "vitest";
6import * as Effect from "effect/Effect";
7
8import {
9 AccountIdSchema,
10 type LocalConfigSource,
11 SourceIdSchema,
12 SourceRecipeRevisionIdSchema,
13 SourceRecipeSchemaBundleIdSchema,
14 WorkspaceIdSchema,
15 type Source,
16 type StoredSourceRecipeOperationRecord,
17} from "#schema";
18import {
19 createSqlControlPlanePersistence,
20 type SqlControlPlanePersistence,
21} from "#persistence";
22
23import {
24 loadLocalExecutorConfig,
25 resolveLocalWorkspaceContext,
26 writeProjectLocalExecutorConfig,
27} from "./local-config";
28import {
29 buildLocalSourceArtifact,
30 writeLocalSourceArtifact,
31} from "./local-source-artifacts";
32import {
33 getSourceInspection,
34 getSourceInspectionSchemaBundle,
35 getSourceInspectionToolDetail,
36} from "./source-inspection";
37import {
38 RuntimeLocalWorkspaceService,
39 type RuntimeLocalWorkspaceState,
40} from "./local-runtime-context";
41import {
42 writeLocalWorkspaceState,
43} from "./local-workspace-state";
44import { ControlPlaneStore } from "./store";
45
46const makePersistence = () =>
47 Effect.runPromise(
48 createSqlControlPlanePersistence({
49 localDataDir: ":memory:",
50 }),
51 );
52
53const 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* resolveLocalWorkspaceContext({ workspaceRoot });
63 const loadedConfig = yield* loadLocalExecutorConfig(context);
64
65 return {
66 context,
67 installation: {
68 workspaceId,
69 accountId,
70 },
71 loadedConfig,
72 } satisfies RuntimeLocalWorkspaceState;
73 }),
74 );
75
76const makeSource = (input: {
77 workspaceId: Source["workspaceId"];
78 sourceId: Source["id"];
79}): Source => ({
80 id: input.sourceId,
81 workspaceId: input.workspaceId,
82 name: "Cloudflare API",
83 kind: "openapi",
84 endpoint: "https://api.cloudflare.com/client/v4",
85 status: "connected",
86 enabled: true,
87 namespace: "cloudflare.api",
88 bindingVersion: 1,
89 binding: {
90 specUrl: "https://example.com/openapi.json",
91 defaultHeaders: null,
92 },
93 importAuthPolicy: "reuse_runtime",
94 importAuth: { kind: "none" },
95 auth: { kind: "none" },
96 sourceHash: null,
97 lastError: null,
98 createdAt: 1000,
99 updatedAt: 1000,
100});
101
102const makeOperation = (
103 overrides: Partial<StoredSourceRecipeOperationRecord> = {},
104): StoredSourceRecipeOperationRecord => ({
105 id: "src_recipe_op_inspection",
106 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_inspection"),
107 operationKey: "zones.listZones",
108 transportKind: "http",
109 toolId: "zones.listZones",
110 title: "List zones",
111 description: "List Cloudflare zones",
112 operationKind: "read",
113 searchText: "zones list cloudflare",
114 inputSchemaJson: JSON.stringify({
115 type: "object",
116 properties: {
117 page: { type: "number" },
118 },
119 }),
120 outputSchemaJson: JSON.stringify({
121 type: "object",
122 properties: {
123 result: {
124 type: "array",
125 items: { type: "string" },
126 },
127 },
128 }),
129 providerKind: "openapi",
130 providerDataJson: JSON.stringify({
131 kind: "openapi",
132 toolId: "zones.listZones",
133 rawToolId: "zones_listZones",
134 operationId: "zones.listZones",
135 group: "zones",
136 leaf: "listZones",
137 tags: ["zones"],
138 method: "get",
139 path: "/zones",
140 operationHash: "hash",
141 invocation: {
142 method: "get",
143 pathTemplate: "/zones",
144 parameters: [],
145 requestBody: null,
146 },
147 }),
148 createdAt: 1000,
149 updatedAt: 1000,
150 ...overrides,
151});
152
153const configSourceFromSource = (source: Source): LocalConfigSource => ({
154 kind: "openapi",
155 ...(source.name !== source.id ? { name: source.name } : {}),
156 ...(source.namespace && source.namespace !== source.id
157 ? { namespace: source.namespace }
158 : {}),
159 connection: {
160 endpoint: source.endpoint,
161 },
162 binding: {
163 specUrl:
164 typeof source.binding.specUrl === "string"
165 ? source.binding.specUrl
166 : "https://example.com/openapi.json",
167 defaultHeaders:
168 (source.binding.defaultHeaders as Record<string, string> | null | undefined)
169 ?? null,
170 },
171});
172
173describe("source-inspection", () => {
174 it("returns a lightweight inspection bundle and loads rich detail on demand", async () => {
175 const persistence = await makePersistence();
176 try {
177 const workspaceId = WorkspaceIdSchema.make("ws_source_inspection");
178 const accountId = AccountIdSchema.make("acc_source_inspection");
179 const sourceId = SourceIdSchema.make("src_source_inspection");
180 const recipeRevisionId =
181 SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization");
182 const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState(
183 workspaceId,
184 accountId,
185 );
186 const hugeDocument = JSON.stringify({
187 openapi: "3.0.3",
188 info: {
189 title: "Cloudflare API",
190 version: "1.0.0",
191 },
192 paths: Object.fromEntries(
193 Array.from({ length: 256 }, (_, index) => [
194 `/zones/${index}`,
195 {
196 get: {
197 operationId: `zones.listZones${index}`,
198 responses: { 200: { description: "ok" } },
199 },
200 },
201 ]),
202 ),
203 });
204
205 const source = makeSource({
206 workspaceId,
207 sourceId,
208 });
209 await Effect.runPromise(writeProjectLocalExecutorConfig({
210 context: runtimeLocalWorkspace.context,
211 config: {
212 sources: {
213 [sourceId]: configSourceFromSource(source),
214 },
215 },
216 }));
217 await Effect.runPromise(writeLocalSourceArtifact({
218 context: runtimeLocalWorkspace.context,
219 sourceId,
220 artifact: buildLocalSourceArtifact({
221 source: {
222 ...source,
223 sourceHash: "manifest_hash",
224 },
225 materialization: {
226 manifestJson: JSON.stringify({
227 sourceHash: "manifest_hash",
228 }),
229 manifestHash: "manifest_hash",
230 sourceHash: "manifest_hash",
231 documents: [{
232 id: "src_recipe_doc_inspection",
233 recipeRevisionId,
234 documentKind: "openapi",
235 documentKey: "https://example.com/openapi.json",
236 contentText: hugeDocument,
237 contentHash: "doc_hash",
238 fetchedAt: 1000,
239 createdAt: 1000,
240 updatedAt: 1000,
241 }],
242 schemaBundles: [{
243 id: SourceRecipeSchemaBundleIdSchema.make(
244 "src_recipe_bundle_inspection",
245 ),
246 recipeRevisionId,
247 bundleKind: "json_schema_ref_map",
248 refsJson: JSON.stringify({
249 "#/components/schemas/Pagination": {
250 type: "object",
251 properties: {
252 page: { type: "number" },
253 },
254 },
255 }),
256 contentHash: "bundle_hash",
257 createdAt: 1000,
258 updatedAt: 1000,
259 }],
260 operations: [makeOperation({
261 recipeRevisionId,
262 })],
263 },
264 }),
265 }));
266 await Effect.runPromise(writeLocalWorkspaceState({
267 context: runtimeLocalWorkspace.context,
268 state: {
269 version: 1,
270 sources: {
271 [sourceId]: {
272 status: "connected",
273 lastError: null,
274 sourceHash: "manifest_hash",
275 createdAt: 1000,
276 updatedAt: 1000,
277 },
278 },
279 policies: {},
280 },
281 }));
282
283 const inspection = await Effect.runPromise(
284 getSourceInspection({
285 workspaceId,
286 sourceId,
287 }).pipe(
288 Effect.provideService(ControlPlaneStore, persistence.rows),
289 Effect.provideService(
290 RuntimeLocalWorkspaceService,
291 runtimeLocalWorkspace,
292 ),
293 ),
294 );
295
296 expect(inspection.toolCount).toBe(1);
297 expect(inspection.tools[0]?.path).toBe("cloudflare.api.zones.listZones");
298 expect("manifestJson" in inspection).toBe(false);
299 expect("rawDocumentText" in inspection).toBe(false);
300 expect("definitionsJson" in inspection).toBe(false);
301
302 const detail = await Effect.runPromise(
303 getSourceInspectionToolDetail({
304 workspaceId,
305 sourceId,
306 toolPath: "cloudflare.api.zones.listZones",
307 }).pipe(
308 Effect.provideService(ControlPlaneStore, persistence.rows),
309 Effect.provideService(
310 RuntimeLocalWorkspaceService,
311 runtimeLocalWorkspace,
312 ),
313 ),
314 );
315
316 expect(detail.summary.method).toBe("get");
317 expect(detail.summary.inputType).toContain("page");
318 expect(detail.summary.outputType).toContain("result");
319 expect(detail.providerDataJson).toContain("/zones");
320 expect(detail.inputSchemaJson).toContain("\"page\"");
321 expect(detail.outputSchemaJson).toContain("\"result\"");
322 expect(detail.schemaBundleId).toBe("src_recipe_bundle_inspection");
323
324 const schemaBundle = await Effect.runPromise(
325 getSourceInspectionSchemaBundle({
326 workspaceId,
327 sourceId,
328 schemaBundleId: "src_recipe_bundle_inspection",
329 }).pipe(
330 Effect.provideService(ControlPlaneStore, persistence.rows),
331 Effect.provideService(
332 RuntimeLocalWorkspaceService,
333 runtimeLocalWorkspace,
334 ),
335 ),
336 );
337
338 expect(schemaBundle.kind).toBe("json_schema_ref_map");
339 expect(schemaBundle.refsJson).toContain("Pagination");
340 } finally {
341 await persistence.close();
342 }
343 });
344});
packages/control-plane/src/runtime/source-materialization.ts+26−23
@@ -1,4 +1,3 @@
1import type { SqlControlPlaneRows } from "#persistence";
2import type {1import type {
3 AccountId,2 AccountId,
4 Source,3 Source,
@@ -6,16 +5,16 @@ import type { · 1 unmodified line
6} from "#schema";5} from "#schema";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
8 7
9import {8import type { ControlPlaneStoreShape } from "./store";
10 buildLocalSourceArtifact,
11 writeLocalSourceArtifact,
12} from "./local-source-artifacts";
13import {9import {
14 requireRuntimeLocalWorkspace,10 requireRuntimeLocalWorkspace,
15} from "./local-runtime-context";11} from "./local-runtime-context";
16import {12import {
17 loadLocalWorkspaceState,13 type WorkspaceStorageServices,
18 writeLocalWorkspaceState,14 SourceArtifactStore,
15 WorkspaceStateStore,
16} from "./local-storage";
17import {
19 type LocalWorkspaceState,18 type LocalWorkspaceState,
20} from "./local-workspace-state";19} from "./local-workspace-state";
21import { resolveSourceAuthMaterial } from "./source-auth-material";20import { resolveSourceAuthMaterial } from "./source-auth-material";
@@ -24,7 +23,6 @@ import { · 2 unmodified lines
24} from "./source-adapters";23} from "./source-adapters";
25import {24import {
26 materializationFromMcpManifestEntries,25 materializationFromMcpManifestEntries,
27 persistMcpRecipeRevisionFromManifestEntries,
28} from "./source-adapters/mcp";26} from "./source-adapters/mcp";
29import type {27import type {
30 ResolveSecretMaterial as ResolveSourceSecretMaterial,28 ResolveSecretMaterial as ResolveSourceSecretMaterial,
@@ -36,16 +34,20 @@ const shouldIndexSource = (source: Source): boolean => · 5 unmodified lines
36 && getSourceAdapterForSource(source).family !== "internal";34 && getSourceAdapterForSource(source).family !== "internal";
37 35
38export const syncSourceMaterialization = (input: {36export const syncSourceMaterialization = (input: {
39 rows: SqlControlPlaneRows;37 rows: ControlPlaneStoreShape;
40 source: Source;38 source: Source;
41 actorAccountId?: AccountId | null;39 actorAccountId?: AccountId | null;
42 resolveSecretMaterial: ResolveSourceSecretMaterial;40 resolveSecretMaterial: ResolveSourceSecretMaterial;
43}): Effect.Effect<void, Error, never> =>41}): Effect.Effect<void, Error, WorkspaceStorageServices> =>
44 Effect.gen(function* () {42 Effect.gen(function* () {
45 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);43 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
44 const workspaceStateStore = yield* WorkspaceStateStore;
45 const sourceArtifactStore = yield* SourceArtifactStore;
46 46
47 if (!shouldIndexSource(input.source)) {47 if (!shouldIndexSource(input.source)) {
48 const state = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context);48 const state = yield* workspaceStateStore.load(
49 runtimeLocalWorkspace.context,
50 );
49 const existingSourceState = state.sources[input.source.id];51 const existingSourceState = state.sources[input.source.id];
50 const nextState: LocalWorkspaceState = {52 const nextState: LocalWorkspaceState = {
51 ...state,53 ...state,
@@ -60,7 +62,7 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
60 },62 },
61 },63 },
62 };64 };
63 yield* writeLocalWorkspaceState({65 yield* workspaceStateStore.write({
64 context: runtimeLocalWorkspace.context,66 context: runtimeLocalWorkspace.context,
65 state: nextState,67 state: nextState,
66 });68 });
@@ -80,16 +82,18 @@ export const syncSourceMaterialization = (input: { · 13 unmodified lines
80 resolveSecretMaterial: input.resolveSecretMaterial,82 resolveSecretMaterial: input.resolveSecretMaterial,
81 }),83 }),
82 });84 });
83 yield* writeLocalSourceArtifact({85 yield* sourceArtifactStore.write({
84 context: runtimeLocalWorkspace.context,86 context: runtimeLocalWorkspace.context,
85 sourceId: input.source.id,87 sourceId: input.source.id,
86 artifact: buildLocalSourceArtifact({88 artifact: sourceArtifactStore.build({
87 source: input.source,89 source: input.source,
88 materialization,90 materialization,
89 }),91 }),
90 });92 });
91 93
92 const state = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context);94 const state = yield* workspaceStateStore.load(
95 runtimeLocalWorkspace.context,
96 );
93 const existingSourceState = state.sources[input.source.id];97 const existingSourceState = state.sources[input.source.id];
94 const nextState: LocalWorkspaceState = {98 const nextState: LocalWorkspaceState = {
95 ...state,99 ...state,
@@ -104,31 +108,30 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
104 },108 },
105 },109 },
106 };110 };
107 yield* writeLocalWorkspaceState({111 yield* workspaceStateStore.write({
108 context: runtimeLocalWorkspace.context,112 context: runtimeLocalWorkspace.context,
109 state: nextState,113 state: nextState,
110 });114 });
111 });115 });
112 116
113export const persistMcpRecipeMaterializationFromManifest = (input: {117export const persistMcpRecipeMaterializationFromManifest = (input: {
114 rows: SqlControlPlaneRows;118 rows: ControlPlaneStoreShape;
115 source: Source;119 source: Source;
116 manifestEntries: Parameters<120 manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
117 typeof persistMcpRecipeRevisionFromManifestEntries121}): Effect.Effect<void, Error, WorkspaceStorageServices> =>
118 >[0]["manifestEntries"];
119}): Effect.Effect<void, Error, never> =>
120 Effect.gen(function* () {122 Effect.gen(function* () {
121 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);123 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId);
124 const sourceArtifactStore = yield* SourceArtifactStore;
122 const materialization = materializationFromMcpManifestEntries({125 const materialization = materializationFromMcpManifestEntries({
123 recipeRevisionId: "src_recipe_rev_materialization" as never,126 recipeRevisionId: "src_recipe_rev_materialization" as never,
124 endpoint: input.source.endpoint,127 endpoint: input.source.endpoint,
125 manifestEntries: input.manifestEntries,128 manifestEntries: input.manifestEntries,
126 });129 });
127 130
128 yield* writeLocalSourceArtifact({131 yield* sourceArtifactStore.write({
129 context: runtimeLocalWorkspace.context,132 context: runtimeLocalWorkspace.context,
130 sourceId: input.source.id,133 sourceId: input.source.id,
131 artifact: buildLocalSourceArtifact({134 artifact: sourceArtifactStore.build({
132 source: input.source,135 source: input.source,
133 materialization,136 materialization,
134 }),137 }),
packages/control-plane/src/runtime/source-recipe-support.ts+1−348
@@ -1,22 +1,10 @@
1import { sha256Hex } from "@executor/codemode-core";1import { sha256Hex } from "@executor/codemode-core";
2 2
3import type {3import type {
4 Source,
5 StoredSourceRecipeDocumentRecord,4 StoredSourceRecipeDocumentRecord,
6 StoredSourceRecipeOperationRecord,5 StoredSourceRecipeOperationRecord,
7 StoredSourceRecipeSchemaBundleRecord,6 StoredSourceRecipeSchemaBundleRecord,
8 StoredSourceRecipeRevisionRecord,
9} from "#schema";7} from "#schema";
10import type { SqlControlPlaneRows } from "#persistence";
11import * as Effect from "effect/Effect";
12import * as Option from "effect/Option";
13
14type SourceBindingRecord =
15 Awaited<
16 ReturnType<SqlControlPlaneRows["sources"]["getByWorkspaceAndId"]>
17 > extends Effect.Effect<Option.Option<infer T>, unknown, never>
18 ? T
19 : never;
20 8
21export const normalizeSearchText = (9export const normalizeSearchText = (
22 ...parts: ReadonlyArray<string | null | undefined>10 ...parts: ReadonlyArray<string | null | undefined>
@@ -27,32 +15,7 @@ export const normalizeSearchText = ( · 4 unmodified lines
27 .join(" ")15 .join(" ")
28 .toLowerCase();16 .toLowerCase();
29 17
30export const contentHash = (value: string): string =>18export const contentHash = (value: string): string => sha256Hex(value);
31 sha256Hex(value);
32
33export const loadSourceBindingRecord = (input: {
34 rows: SqlControlPlaneRows;
35 source: Source;
36}): Effect.Effect<SourceBindingRecord, Error, never> =>
37 Effect.gen(function* () {
38 const sourceRecord = yield* input.rows.sources
39 .getByWorkspaceAndId(input.source.workspaceId, input.source.id)
40 .pipe(
41 Effect.mapError((cause) =>
42 cause instanceof Error ? cause : new Error(String(cause)),
43 ),
44 );
45
46 if (Option.isNone(sourceRecord)) {
47 return yield* Effect.fail(
48 new Error(
49 `Source disappeared while syncing recipe data for ${input.source.id}`,
50 ),
51 );
52 }
53
54 return sourceRecord.value;
55 });
56 19
57export type SourceRecipeMaterialization = {20export type SourceRecipeMaterialization = {
58 manifestJson: string | null;21 manifestJson: string | null;
@@ -62,313 +25,3 @@ export type SourceRecipeMaterialization = { · 3 unmodified lines
62 schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];25 schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];
63 operations: readonly StoredSourceRecipeOperationRecord[];26 operations: readonly StoredSourceRecipeOperationRecord[];
64};27};
65
66const canonicalMaterializationHash = (input: SourceRecipeMaterialization): string => {
67 const documents = [...input.documents]
68 .map((document) => ({
69 documentKind: document.documentKind,
70 documentKey: document.documentKey,
71 contentHash: document.contentHash,
72 }))
73 .sort((left, right) =>
74 left.documentKind.localeCompare(right.documentKind)
75 || left.documentKey.localeCompare(right.documentKey)
76 || left.contentHash.localeCompare(right.contentHash)
77 );
78 const schemaBundles = [...input.schemaBundles]
79 .map((bundle) => ({
80 bundleKind: bundle.bundleKind,
81 contentHash: bundle.contentHash,
82 }))
83 .sort((left, right) =>
84 left.bundleKind.localeCompare(right.bundleKind)
85 || left.contentHash.localeCompare(right.contentHash)
86 );
87 const operations = [...input.operations]
88 .map((operation) => ({
89 operationKey: operation.operationKey,
90 transportKind: operation.transportKind,
91 toolId: operation.toolId,
92 title: operation.title,
93 description: operation.description,
94 operationKind: operation.operationKind,
95 searchText: operation.searchText,
96 inputSchemaJson: operation.inputSchemaJson,
97 outputSchemaJson: operation.outputSchemaJson,
98 providerKind: operation.providerKind,
99 providerDataJson: operation.providerDataJson,
100 }))
101 .sort((left, right) => left.operationKey.localeCompare(right.operationKey));
102
103 return contentHash(JSON.stringify({
104 schemaVersion: 1,
105 manifestHash: input.manifestHash,
106 manifestJson: input.manifestJson,
107 documents,
108 schemaBundles,
109 operations,
110 }));
111};
112
113const replaceRevisionContent = (input: {
114 rows: SqlControlPlaneRows;
115 recipeRevisionId: StoredSourceRecipeRevisionRecord["id"];
116 materializationHash: string;
117 materialization: SourceRecipeMaterialization;
118 updatedAt: number;
119}) =>
120 Effect.gen(function* () {
121 const boundDocuments = input.materialization.documents.map((document) => ({
122 ...document,
123 recipeRevisionId: input.recipeRevisionId,
124 }));
125 const boundSchemaBundles = input.materialization.schemaBundles.map((bundle) => ({
126 ...bundle,
127 recipeRevisionId: input.recipeRevisionId,
128 }));
129 const boundOperations = input.materialization.operations.map((operation) => ({
130 ...operation,
131 recipeRevisionId: input.recipeRevisionId,
132 }));
133
134 yield* input.rows.sourceRecipeRevisions
135 .update(input.recipeRevisionId, {
136 manifestJson: input.materialization.manifestJson,
137 manifestHash: input.materialization.manifestHash,
138 materializationHash: input.materializationHash,
139 updatedAt: input.updatedAt,
140 })
141 .pipe(
142 Effect.mapError((cause) =>
143 cause instanceof Error ? cause : new Error(String(cause)),
144 ),
145 );
146
147 yield* input.rows.sourceRecipeDocuments
148 .replaceForRevision({
149 recipeRevisionId: input.recipeRevisionId,
150 documents: boundDocuments,
151 })
152 .pipe(
153 Effect.mapError((cause) =>
154 cause instanceof Error ? cause : new Error(String(cause)),
155 ),
156 );
157
158 yield* input.rows.sourceRecipeSchemaBundles
159 .replaceForRevision({
160 recipeRevisionId: input.recipeRevisionId,
161 bundles: boundSchemaBundles,
162 })
163 .pipe(
164 Effect.mapError((cause) =>
165 cause instanceof Error ? cause : new Error(String(cause)),
166 ),
167 );
168
169 yield* input.rows.sourceRecipeOperations
170 .replaceForRevision({
171 recipeRevisionId: input.recipeRevisionId,
172 operations: boundOperations,
173 })
174 .pipe(
175 Effect.mapError((cause) =>
176 cause instanceof Error ? cause : new Error(String(cause)),
177 ),
178 );
179 });
180
181const cleanupOrphanedRevision = (input: {
182 rows: SqlControlPlaneRows;
183 recipeRevisionId: StoredSourceRecipeRevisionRecord["id"];
184}) =>
185 Effect.gen(function* () {
186 const referenceCount = yield* input.rows.sources.countByRecipeRevisionId(input.recipeRevisionId);
187 if (referenceCount > 0) {
188 return;
189 }
190
191 yield* input.rows.sourceRecipeDocuments.removeByRevisionId(input.recipeRevisionId);
192 yield* input.rows.sourceRecipeSchemaBundles.removeByRevisionId(input.recipeRevisionId);
193 yield* input.rows.sourceRecipeOperations.removeByRevisionId(input.recipeRevisionId);
194 yield* input.rows.sourceRecipeRevisions.removeById(input.recipeRevisionId);
195 });
196
197const updateRecipeLatestRevision = (input: {
198 rows: SqlControlPlaneRows;
199 recipeId: SourceBindingRecord["recipeId"];
200 latestRevisionId: StoredSourceRecipeRevisionRecord["id"];
201 updatedAt: number;
202}) =>
203 Effect.gen(function* () {
204 const recipe = yield* input.rows.sourceRecipes.getById(input.recipeId);
205 if (Option.isNone(recipe)) {
206 return yield* Effect.fail(
207 new Error(`Recipe disappeared while syncing recipe data for ${input.recipeId}`),
208 );
209 }
210
211 yield* input.rows.sourceRecipes.upsert({
212 ...recipe.value,
213 latestRevisionId: input.latestRevisionId,
214 updatedAt: input.updatedAt,
215 });
216 });
217
218export const persistRecipeMaterialization = (input: {
219 rows: SqlControlPlaneRows;
220 source: Source;
221 materialization: SourceRecipeMaterialization;
222}): Effect.Effect<void, Error, never> =>
223 Effect.gen(function* () {
224 const sourceRecord = yield* loadSourceBindingRecord({
225 rows: input.rows,
226 source: input.source,
227 });
228 const now = Date.now();
229 const materializationHash = canonicalMaterializationHash(input.materialization);
230 const currentRevision = yield* input.rows.sourceRecipeRevisions.getById(
231 sourceRecord.recipeRevisionId,
232 );
233
234 if (Option.isNone(currentRevision)) {
235 return yield* Effect.fail(
236 new Error(
237 `Recipe revision disappeared while syncing recipe data for ${input.source.id}`,
238 ),
239 );
240 }
241
242 const existingMaterializedRevision = yield* input.rows.sourceRecipeRevisions
243 .getByRecipeAndMaterializationHash({
244 recipeId: sourceRecord.recipeId,
245 materializationHash,
246 });
247
248 if (
249 Option.isSome(existingMaterializedRevision)
250 && existingMaterializedRevision.value.id !== currentRevision.value.id
251 ) {
252 const movedSource = yield* input.rows.sources.update(
253 input.source.workspaceId,
254 input.source.id,
255 {
256 recipeRevisionId: existingMaterializedRevision.value.id,
257 sourceHash: input.materialization.sourceHash,
258 updatedAt: now,
259 },
260 );
261 if (Option.isNone(movedSource)) {
262 return yield* Effect.fail(
263 new Error(`Source disappeared while updating recipe materialization for ${input.source.id}`),
264 );
265 }
266
267 yield* updateRecipeLatestRevision({
268 rows: input.rows,
269 recipeId: sourceRecord.recipeId,
270 latestRevisionId: existingMaterializedRevision.value.id,
271 updatedAt: now,
272 });
273 yield* cleanupOrphanedRevision({
274 rows: input.rows,
275 recipeRevisionId: currentRevision.value.id,
276 });
277 return;
278 }
279
280 const needsFreshRevision =
281 currentRevision.value.materializationHash !== null
282 && currentRevision.value.materializationHash !== materializationHash;
283
284 const nextRevisionNumber = needsFreshRevision
285 ? yield* input.rows.sourceRecipeRevisions.nextRevisionNumber(sourceRecord.recipeId)
286 : null;
287
288 const targetRevision = needsFreshRevision
289 ? createFreshTargetRevision({
290 currentRevision: currentRevision.value,
291 materializationHash,
292 now,
293 nextRevisionNumber: nextRevisionNumber!,
294 })
295 : {
296 revision: currentRevision.value,
297 movedFromRevisionId: null as StoredSourceRecipeRevisionRecord["id"] | null,
298 };
299
300 if (needsFreshRevision) {
301 yield* input.rows.sourceRecipeRevisions.upsert(targetRevision.revision);
302 const movedSource = yield* input.rows.sources.update(
303 input.source.workspaceId,
304 input.source.id,
305 {
306 recipeRevisionId: targetRevision.revision.id,
307 sourceHash: input.materialization.sourceHash,
308 updatedAt: now,
309 },
310 );
311 if (Option.isNone(movedSource)) {
312 return yield* Effect.fail(
313 new Error(`Source disappeared while switching recipe materialization for ${input.source.id}`),
314 );
315 }
316 } else {
317 const updatedSource = yield* input.rows.sources.update(
318 input.source.workspaceId,
319 input.source.id,
320 {
321 sourceHash: input.materialization.sourceHash,
322 updatedAt: now,
323 },
324 );
325 if (Option.isNone(updatedSource)) {
326 return yield* Effect.fail(
327 new Error(`Source disappeared while updating sync metadata for ${input.source.id}`),
328 );
329 }
330 }
331
332 yield* replaceRevisionContent({
333 rows: input.rows,
334 recipeRevisionId: targetRevision.revision.id,
335 materializationHash,
336 materialization: input.materialization,
337 updatedAt: now,
338 });
339 yield* updateRecipeLatestRevision({
340 rows: input.rows,
341 recipeId: sourceRecord.recipeId,
342 latestRevisionId: targetRevision.revision.id,
343 updatedAt: now,
344 });
345
346 if (targetRevision.movedFromRevisionId !== null) {
347 yield* cleanupOrphanedRevision({
348 rows: input.rows,
349 recipeRevisionId: targetRevision.movedFromRevisionId,
350 });
351 }
352 });
353
354const createFreshTargetRevision = (input: {
355 currentRevision: StoredSourceRecipeRevisionRecord;
356 materializationHash: string;
357 now: number;
358 nextRevisionNumber: number;
359}): {
360 revision: StoredSourceRecipeRevisionRecord;
361 movedFromRevisionId: StoredSourceRecipeRevisionRecord["id"] | null;
362} => ({
363 revision: {
364 ...input.currentRevision,
365 id: `src_recipe_rev_${crypto.randomUUID()}` as StoredSourceRecipeRevisionRecord["id"],
366 revisionNumber: input.nextRevisionNumber,
367 manifestJson: null,
368 manifestHash: null,
369 materializationHash: input.materializationHash,
370 createdAt: input.now,
371 updatedAt: input.now,
372 },
373 movedFromRevisionId: input.currentRevision.id,
374});
packages/control-plane/src/runtime/source-recipes-runtime.test.ts+0−858
@@ -1,858 +0,0 @@
1import { mkdtempSync } from "node:fs";
2import { tmpdir } from "node:os";
3import { join } from "node:path";
4
5import { describe, expect, it } from "vitest";
6import * as Effect from "effect/Effect";
7import {
8 buildOpenApiToolPresentation,
9 compileOpenApiToolDefinitions,
10 extractOpenApiManifest,
11} from "@executor/codemode-openapi";
12
13import {
14 AccountIdSchema,
15 SourceIdSchema,
16 type LocalConfigSource,
17 SourceRecipeIdSchema,
18 SourceRecipeRevisionIdSchema,
19 WorkspaceIdSchema,
20 type Source,
21 type StoredSourceRecipeOperationRecord,
22} from "#schema";
23import {
24 createSqlControlPlanePersistence,
25 type SqlControlPlanePersistence,
26} from "#persistence";
27
28import {
29 loadLocalExecutorConfig,
30 resolveLocalWorkspaceContext,
31 writeProjectLocalExecutorConfig,
32} from "./local-config";
33import {
34 buildLocalSourceArtifact,
35 writeLocalSourceArtifact,
36} from "./local-source-artifacts";
37import {
38 RuntimeLocalWorkspaceService,
39 type RuntimeLocalWorkspaceState,
40} from "./local-runtime-context";
41import {
42 writeLocalWorkspaceState,
43} from "./local-workspace-state";
44import {
45 expandRecipeTools,
46 loadSourceWithRecipe,
47 loadWorkspaceSourceRecipes,
48 recipeToolDescriptor,
49 recipeToolPath,
50 recipeToolSearchNamespace,
51 type LoadedSourceRecipe,
52} from "./source-recipes-runtime";
53
54const makePersistence = () =>
55 Effect.runPromise(
56 createSqlControlPlanePersistence({
57 localDataDir: ":memory:",
58 }),
59 );
60
61const 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* resolveLocalWorkspaceContext({ workspaceRoot });
70 const loadedConfig = yield* loadLocalExecutorConfig(context);
71
72 return {
73 context,
74 installation: {
75 workspaceId,
76 accountId: AccountIdSchema.make("acc_local_source_recipes"),
77 },
78 loadedConfig,
79 } satisfies RuntimeLocalWorkspaceState;
80 }),
81 );
82
83const withRuntimeLocalWorkspace = <A, E>(
84 effect: Effect.Effect<A, E, never>,
85 runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
86) =>
87 effect.pipe(
88 Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
89 );
90
91const openApiBindingConfigJson = (specUrl: string): string =>
92 JSON.stringify({
93 adapterKey: "openapi",
94 version: 1,
95 payload: {
96 specUrl,
97 defaultHeaders: null,
98 },
99 });
100
101const graphqlBindingConfigJson = (): string =>
102 JSON.stringify({
103 adapterKey: "graphql",
104 version: 1,
105 payload: {
106 defaultHeaders: null,
107 },
108 });
109
110const makeSource = (overrides: Partial<Source> = {}): Source => {
111 const kind = overrides.kind ?? "openapi";
112 const endpoint = overrides.endpoint
113 ?? (kind === "graphql"
114 ? "https://example.com/graphql"
115 : kind === "mcp"
116 ? "https://example.com/mcp"
117 : "https://api.github.com");
118 const binding =
119 overrides.binding
120 ?? (kind === "openapi"
121 ? {
122 specUrl: "https://api.github.com/openapi.json",
123 defaultHeaders: null,
124 }
125 : kind === "graphql"
126 ? {
127 defaultHeaders: null,
128 }
129 : kind === "mcp"
130 ? {
131 transport: null,
132 queryParams: null,
133 headers: null,
134 }
135 : {});
136
137 return {
138 id: SourceIdSchema.make("src_runtime_recipe"),
139 workspaceId: WorkspaceIdSchema.make("ws_runtime_recipe"),
140 name: "GitHub",
141 kind,
142 endpoint,
143 status: "connected",
144 enabled: true,
145 namespace: "github",
146 bindingVersion: 1,
147 binding,
148 importAuthPolicy: "reuse_runtime",
149 importAuth: { kind: "none" },
150 auth: { kind: "none" },
151 sourceHash: null,
152 lastError: null,
153 createdAt: 1000,
154 updatedAt: 1000,
155 ...overrides,
156 };
157};
158
159const makeOperation = (
160 overrides: Partial<StoredSourceRecipeOperationRecord> = {},
161): StoredSourceRecipeOperationRecord => ({
162 id: "src_recipe_op_runtime",
163 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
164 operationKey: "getRepo",
165 transportKind: "http",
166 toolId: "getRepo",
167 title: "Get Repo",
168 description: "Read a repository",
169 operationKind: "read",
170 searchText: "get repo github",
171 inputSchemaJson: JSON.stringify({
172 type: "object",
173 additionalProperties: false,
174 }),
175 outputSchemaJson: JSON.stringify({
176 type: "object",
177 properties: {
178 full_name: { type: "string" },
179 },
180 }),
181 providerKind: "openapi",
182 providerDataJson: JSON.stringify({
183 kind: "openapi",
184 toolId: "getRepo",
185 rawToolId: "repos_getRepo",
186 operationId: "repos.getRepo",
187 group: "repos",
188 leaf: "getRepo",
189 tags: ["repos"],
190 method: "get",
191 path: "/repos/{owner}/{repo}",
192 operationHash: "hash",
193 invocation: {
194 method: "get",
195 pathTemplate: "/repos/{owner}/{repo}",
196 parameters: [],
197 requestBody: null,
198 },
199 }),
200 createdAt: 1000,
201 updatedAt: 1000,
202 ...overrides,
203});
204
205const makeGraphqlOperation = (
206 overrides: Partial<StoredSourceRecipeOperationRecord> = {},
207): StoredSourceRecipeOperationRecord => ({
208 id: "src_recipe_op_graphql_runtime",
209 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
210 operationKey: "viewer",
211 transportKind: "graphql",
212 toolId: "viewer",
213 title: "Viewer",
214 description: "Query the current viewer",
215 operationKind: "read",
216 searchText: "viewer graphql query",
217 inputSchemaJson: JSON.stringify({
218 type: "object",
219 additionalProperties: false,
220 }),
221 outputSchemaJson: JSON.stringify({
222 type: "object",
223 properties: {
224 login: { type: "string" },
225 },
226 }),
227 providerKind: "graphql",
228 providerDataJson: JSON.stringify({
229 kind: "graphql",
230 toolKind: "field",
231 toolId: "viewer",
232 rawToolId: "viewer",
233 group: "query",
234 leaf: "viewer",
235 fieldName: "viewer",
236 operationType: "query",
237 operationName: "viewer",
238 operationDocument: "query Viewer { viewer { login } }",
239 queryTypeName: "Query",
240 mutationTypeName: null,
241 subscriptionTypeName: null,
242 }),
243 createdAt: 1000,
244 updatedAt: 1000,
245 ...overrides,
246});
247
248const makeMcpOperation = (
249 overrides: Partial<StoredSourceRecipeOperationRecord> = {},
250): StoredSourceRecipeOperationRecord => ({
251 id: "src_recipe_op_mcp_runtime",
252 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
253 operationKey: "echo",
254 transportKind: "mcp",
255 toolId: "echo",
256 title: "Echo",
257 description: "Echo a value",
258 operationKind: "unknown",
259 searchText: "echo mcp",
260 inputSchemaJson: null,
261 outputSchemaJson: null,
262 providerKind: "mcp",
263 providerDataJson: JSON.stringify({
264 kind: "mcp",
265 toolId: "echo",
266 toolName: "echo",
267 description: "Echo a value",
268 }),
269 createdAt: 1000,
270 updatedAt: 1000,
271 ...overrides,
272});
273
274const configSourceFromSource = (source: Source): LocalConfigSource => {
275 switch (source.kind) {
276 case "openapi":
277 return {
278 kind: "openapi" as const,
279 ...(source.name !== source.id ? { name: source.name } : {}),
280 ...(source.namespace && source.namespace !== source.id
281 ? { namespace: source.namespace }
282 : {}),
283 ...(source.enabled ? {} : { enabled: false }),
284 connection: {
285 endpoint: source.endpoint,
286 },
287 binding: {
288 specUrl:
289 typeof source.binding.specUrl === "string"
290 ? source.binding.specUrl
291 : "https://example.com/openapi.json",
292 defaultHeaders:
293 (source.binding.defaultHeaders as Record<string, string> | null | undefined)
294 ?? null,
295 },
296 };
297 case "graphql":
298 return {
299 kind: "graphql" as const,
300 ...(source.name !== source.id ? { name: source.name } : {}),
301 ...(source.namespace && source.namespace !== source.id
302 ? { namespace: source.namespace }
303 : {}),
304 ...(source.enabled ? {} : { enabled: false }),
305 connection: {
306 endpoint: source.endpoint,
307 },
308 binding: {
309 defaultHeaders:
310 (source.binding.defaultHeaders as Record<string, string> | null | undefined)
311 ?? null,
312 },
313 };
314 case "mcp":
315 return {
316 kind: "mcp" as const,
317 ...(source.name !== source.id ? { name: source.name } : {}),
318 ...(source.namespace && source.namespace !== source.id
319 ? { namespace: source.namespace }
320 : {}),
321 ...(source.enabled ? {} : { enabled: false }),
322 connection: {
323 endpoint: source.endpoint,
324 },
325 binding: {
326 transport:
327 (source.binding.transport as "auto" | "streamable-http" | "sse" | null | undefined)
328 ?? null,
329 queryParams:
330 (source.binding.queryParams as Record<string, string> | null | undefined)
331 ?? null,
332 headers:
333 (source.binding.headers as Record<string, string> | null | undefined)
334 ?? null,
335 },
336 };
337 default:
338 throw new Error(`Unsupported source kind in test fixture: ${source.kind}`);
339 }
340};
341
342const seedLocalWorkspaceSources = async (input: {
343 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
344 fixtures: ReadonlyArray<{
345 source: Source;
346 materialization: {
347 manifestJson: string | null;
348 manifestHash: string | null;
349 sourceHash: string | null;
350 documents: ReadonlyArray<any>;
351 schemaBundles: ReadonlyArray<any>;
352 operations: ReadonlyArray<StoredSourceRecipeOperationRecord>;
353 };
354 }>;
355}) => {
356 await Effect.runPromise(writeProjectLocalExecutorConfig({
357 context: input.runtimeLocalWorkspace.context,
358 config: {
359 sources: Object.fromEntries(
360 input.fixtures.map(({ source }) => [source.id, configSourceFromSource(source)]),
361 ),
362 },
363 }));
364
365 await Promise.all(
366 input.fixtures.map(({ source, materialization }) =>
367 Effect.runPromise(writeLocalSourceArtifact({
368 context: input.runtimeLocalWorkspace.context,
369 sourceId: source.id,
370 artifact: buildLocalSourceArtifact({
371 source,
372 materialization,
373 }),
374 })),
375 ),
376 );
377
378 await Effect.runPromise(writeLocalWorkspaceState({
379 context: input.runtimeLocalWorkspace.context,
380 state: {
381 version: 1,
382 sources: Object.fromEntries(
383 input.fixtures.map(({ source }) => [
384 source.id,
385 {
386 status: source.status,
387 lastError: source.lastError,
388 sourceHash: source.sourceHash,
389 createdAt: source.createdAt,
390 updatedAt: source.updatedAt,
391 },
392 ]),
393 ),
394 policies: {},
395 },
396 }));
397};
398
399describe("source-recipes-runtime", () => {
400 describe("recipe tool helpers", () => {
401 it("derives tool paths from explicit and implicit namespaces", () => {
402 const source = makeSource();
403 const operation = makeOperation();
404
405 expect(recipeToolPath({
406 source,
407 operation,
408 })).toBe("github.getRepo");
409 expect(recipeToolPath({
410 source: makeSource({
411 namespace: null,
412 name: "My GitHub",
413 }),
414 operation,
415 })).toBe("my.github.getRepo");
416 expect(recipeToolPath({
417 source: makeSource({
418 namespace: "",
419 }),
420 operation,
421 })).toBe("getRepo");
422 });
423
424 it("derives search namespaces from path segments and graphql namespaces", () => {
425 const openApiSource = makeSource();
426 const graphqlSource = makeSource({
427 kind: "graphql",
428 namespace: "issues",
429 });
430
431 expect(recipeToolSearchNamespace({
432 source: openApiSource,
433 path: "github.getRepo",
434 operation: makeOperation(),
435 })).toBe("github.getRepo");
436 expect(recipeToolSearchNamespace({
437 source: openApiSource,
438 path: "a.b.c",
439 operation: makeOperation(),
440 })).toBe("a.b");
441 expect(recipeToolSearchNamespace({
442 source: graphqlSource,
443 path: "ignored.path",
444 operation: makeGraphqlOperation(),
445 })).toBe("issues");
446 });
447
448 it("computes interaction modes and descriptor fields correctly", () => {
449 expect(recipeToolDescriptor({
450 source: makeSource(),
451 operation: makeOperation(),
452 path: "github.getRepo",
453 includeSchemas: true,
454 }).interaction).toBe("auto");
455
456 expect(recipeToolDescriptor({
457 source: makeSource(),
458 operation: makeOperation({
459 operationKind: "delete",
460 }),
461 path: "github.deleteRepo",
462 includeSchemas: true,
463 }).interaction).toBe("required");
464
465 expect(recipeToolDescriptor({
466 source: makeSource({
467 kind: "graphql",
468 }),
469 operation: makeGraphqlOperation(),
470 path: "graphql.viewer",
471 includeSchemas: true,
472 }).interaction).toBe("auto");
473
474 expect(recipeToolDescriptor({
475 source: makeSource({
476 kind: "graphql",
477 }),
478 operation: makeGraphqlOperation({
479 operationKind: "write",
480 }),
481 path: "graphql.createIssue",
482 includeSchemas: true,
483 }).interaction).toBe("required");
484
485 expect(recipeToolDescriptor({
486 source: makeSource({
487 kind: "mcp",
488 binding: {
489 transport: "streamable-http",
490 queryParams: null,
491 headers: null,
492 },
493 }),
494 operation: makeMcpOperation(),
495 path: "mcp.echo",
496 includeSchemas: true,
497 }).interaction).toBe("auto");
498
499 const descriptor = recipeToolDescriptor({
500 source: makeSource(),
501 operation: makeOperation({
502 description: null,
503 title: "Fallback Title",
504 providerDataJson: null,
505 }),
506 path: "github.getRepo",
507 includeSchemas: false,
508 });
509
510 expect(descriptor.description).toBe("Fallback Title");
511 expect(descriptor.inputSchemaJson).toBeUndefined();
512 expect(descriptor.outputSchemaJson).toBeUndefined();
513 expect(descriptor).not.toHaveProperty("providerDataJson");
514 });
515
516 it("expands recipes into lower-cased searchable tools and handles empty operation sets", () => {
517 const recipe: LoadedSourceRecipe = {
518 source: makeSource({
519 name: "GITHUB API",
520 }),
521 sourceRecord: {
522 id: SourceIdSchema.make("src_runtime_recipe"),
523 workspaceId: WorkspaceIdSchema.make("ws_runtime_recipe"),
524 recipeId: SourceRecipeIdSchema.make("src_recipe_runtime"),
525 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
526 name: "GITHUB API",
527 kind: "openapi",
528 endpoint: "https://api.github.com",
529 status: "connected",
530 enabled: true,
531 namespace: "github",
532 importAuthPolicy: "reuse_runtime",
533 bindingConfigJson: openApiBindingConfigJson("https://api.github.com/openapi.json"),
534 sourceHash: null,
535 lastError: null,
536 createdAt: 1000,
537 updatedAt: 1000,
538 },
539 revision: {
540 id: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
541 recipeId: SourceRecipeIdSchema.make("src_recipe_runtime"),
542 revisionNumber: 1,
543 sourceConfigJson: "{}",
544 manifestJson: null,
545 manifestHash: null,
546 materializationHash: null,
547 createdAt: 1000,
548 updatedAt: 1000,
549 },
550 documents: [],
551 schemaBundles: [],
552 operations: [makeOperation({
553 searchText: "",
554 })],
555 manifest: null,
556 };
557
558 const expanded = Effect.runSync(
559 expandRecipeTools({
560 recipes: [recipe],
561 includeSchemas: false,
562 }),
563 );
564 expect(expanded).toHaveLength(1);
565 expect(expanded[0]?.searchText).toBe(
566 "github.getrepo github.getrepo github api github.getrepo getrepo get repo read a repository repos_getrepo repos.getrepo get /repos/{owner}/{repo} repos getrepo repos",
567 );
568
569 expect(Effect.runSync(expandRecipeTools({
570 recipes: [{
571 ...recipe,
572 operations: [],
573 }],
574 includeSchemas: false,
575 }))).toEqual([]);
576 });
577 });
578
579 describe("recipe loading", () => {
580 it("loads multiple sources sharing the same recipe revision", async () => {
581 const persistence = await makePersistence();
582 try {
583 const workspaceId = WorkspaceIdSchema.make("ws_shared_revision");
584 const runtimeLocalWorkspace =
585 await makeRuntimeLocalWorkspaceState(workspaceId);
586 const openApiDocument = JSON.stringify({
587 openapi: "3.0.3",
588 info: {
589 title: "GitHub",
590 version: "1.0.0",
591 },
592 paths: {
593 "/repos/{owner}/{repo}": {
594 get: {
595 operationId: "repos.getRepo",
596 parameters: [
597 {
598 name: "owner",
599 in: "path",
600 required: true,
601 schema: { type: "string" },
602 },
603 {
604 name: "repo",
605 in: "path",
606 required: true,
607 schema: { type: "string" },
608 },
609 ],
610 responses: {
611 200: {
612 description: "ok",
613 },
614 },
615 },
616 },
617 },
618 });
619 const manifest = await Effect.runPromise(extractOpenApiManifest("GitHub", openApiDocument));
620 const definition = compileOpenApiToolDefinitions(manifest)[0]!;
621 const presentation = buildOpenApiToolPresentation({
622 definition,
623 });
624
625 await seedLocalWorkspaceSources({
626 runtimeLocalWorkspace,
627 fixtures: ["GitHub One", "GitHub Two"].map((name, index) => ({
628 source: makeSource({
629 id: SourceIdSchema.make(`src_shared_revision_${index}`),
630 workspaceId,
631 name,
632 sourceHash: manifest.sourceHash,
633 createdAt: 1000 + index,
634 updatedAt: 1000 + index,
635 }),
636 materialization: {
637 manifestJson: JSON.stringify(manifest),
638 manifestHash: manifest.sourceHash,
639 sourceHash: manifest.sourceHash,
640 documents: [{
641 id: `src_recipe_doc_shared_revision_${index}`,
642 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),
643 documentKind: "openapi",
644 documentKey: "https://api.github.com/openapi.json",
645 contentText: openApiDocument,
646 contentHash: manifest.sourceHash,
647 fetchedAt: 1000,
648 createdAt: 1000,
649 updatedAt: 1000,
650 }],
651 schemaBundles: [],
652 operations: [makeOperation({
653 id: `src_recipe_op_shared_revision_${index}`,
654 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),
655 operationKey: definition.toolId,
656 toolId: definition.toolId,
657 title: definition.name,
658 description: definition.description,
659 searchText: `${definition.toolId} ${definition.name}`.toLowerCase(),
660 inputSchemaJson: presentation.inputSchemaJson ?? null,
661 outputSchemaJson: presentation.outputSchemaJson ?? null,
662 providerDataJson: presentation.providerDataJson,
663 })],
664 },
665 })),
666 });
667
668 const recipes = await Effect.runPromise(
669 withRuntimeLocalWorkspace(
670 loadWorkspaceSourceRecipes({
671 rows: persistence.rows,
672 workspaceId,
673 }),
674 runtimeLocalWorkspace,
675 ),
676 );
677
678 expect(recipes).toHaveLength(2);
679 expect(recipes[0]?.documents).toHaveLength(1);
680 expect(recipes[0]?.operations).toHaveLength(1);
681 expect(recipes[0]?.documents[0]?.contentText).toBe(
682 recipes[1]?.documents[0]?.contentText,
683 );
684 expect(recipes[0]?.operations[0]?.providerDataJson).toBe(
685 recipes[1]?.operations[0]?.providerDataJson,
686 );
687 } finally {
688 await persistence.close();
689 }
690 }, 60_000);
691
692 it("loads sources with empty recipe documents and operations", async () => {
693 const persistence = await makePersistence();
694 try {
695 const workspaceId = WorkspaceIdSchema.make("ws_empty_recipe_rows");
696 const sourceId = SourceIdSchema.make("src_empty_recipe_rows");
697 const runtimeLocalWorkspace =
698 await makeRuntimeLocalWorkspaceState(workspaceId);
699
700 await seedLocalWorkspaceSources({
701 runtimeLocalWorkspace,
702 fixtures: [{
703 source: makeSource({
704 id: sourceId,
705 workspaceId,
706 name: "GraphQL Demo",
707 kind: "graphql",
708 endpoint: "https://example.com/graphql",
709 namespace: "graphql",
710 binding: {
711 defaultHeaders: null,
712 },
713 }),
714 materialization: {
715 manifestJson: null,
716 manifestHash: null,
717 sourceHash: null,
718 documents: [],
719 schemaBundles: [],
720 operations: [],
721 },
722 }],
723 });
724
725 const recipes = await Effect.runPromise(
726 withRuntimeLocalWorkspace(
727 loadWorkspaceSourceRecipes({
728 rows: persistence.rows,
729 workspaceId,
730 }),
731 runtimeLocalWorkspace,
732 ),
733 );
734
735 expect(recipes).toHaveLength(1);
736 expect(recipes[0]?.documents).toEqual([]);
737 expect(recipes[0]?.operations).toEqual([]);
738 expect(recipes[0]?.manifest).toBeNull();
739 } finally {
740 await persistence.close();
741 }
742 }, 60_000);
743
744 it("fails clearly when loading a missing source, missing revision, or invalid manifest", async () => {
745 const persistence = await makePersistence();
746 try {
747 const missingWorkspaceId = WorkspaceIdSchema.make("ws_missing_source");
748 const missingRuntimeLocalWorkspace =
749 await makeRuntimeLocalWorkspaceState(missingWorkspaceId);
750
751 await expect(
752 Effect.runPromise(
753 withRuntimeLocalWorkspace(
754 loadSourceWithRecipe({
755 rows: persistence.rows,
756 workspaceId: missingWorkspaceId,
757 sourceId: SourceIdSchema.make("src_missing_source"),
758 }),
759 missingRuntimeLocalWorkspace,
760 ),
761 ),
762 ).rejects.toThrow("Source not found");
763
764 const workspaceId = WorkspaceIdSchema.make("ws_bad_recipe_runtime");
765 const sourceId = SourceIdSchema.make("src_bad_recipe_runtime");
766 const runtimeLocalWorkspace =
767 await makeRuntimeLocalWorkspaceState(workspaceId);
768
769 await Effect.runPromise(writeProjectLocalExecutorConfig({
770 context: runtimeLocalWorkspace.context,
771 config: {
772 sources: {
773 [sourceId]: configSourceFromSource(
774 makeSource({
775 id: sourceId,
776 workspaceId,
777 name: "Broken GitHub",
778 }),
779 ),
780 },
781 },
782 }));
783 await Effect.runPromise(writeLocalWorkspaceState({
784 context: runtimeLocalWorkspace.context,
785 state: {
786 version: 1,
787 sources: {
788 [sourceId]: {
789 status: "connected",
790 lastError: null,
791 sourceHash: null,
792 createdAt: 1000,
793 updatedAt: 1000,
794 },
795 },
796 policies: {},
797 },
798 }));
799
800 await expect(
801 Effect.runPromise(
802 withRuntimeLocalWorkspace(
803 loadSourceWithRecipe({
804 rows: persistence.rows,
805 workspaceId,
806 sourceId,
807 }),
808 runtimeLocalWorkspace,
809 ),
810 ),
811 ).rejects.toThrow("Recipe artifact missing");
812
813 const invalidManifestSource = makeSource({
814 id: sourceId,
815 workspaceId,
816 name: "Broken GitHub",
817 });
818 const invalidArtifact = buildLocalSourceArtifact({
819 source: invalidManifestSource,
820 materialization: {
821 manifestJson: "{}",
822 manifestHash: "manifest_hash_invalid",
823 sourceHash: "manifest_hash_invalid",
824 documents: [],
825 schemaBundles: [],
826 operations: [],
827 },
828 });
829 await Effect.runPromise(writeLocalSourceArtifact({
830 context: runtimeLocalWorkspace.context,
831 sourceId,
832 artifact: {
833 ...invalidArtifact,
834 revision: {
835 ...invalidArtifact.revision,
836 manifestJson: "{bad-json",
837 },
838 },
839 }));
840
841 await expect(
842 Effect.runPromise(
843 withRuntimeLocalWorkspace(
844 loadSourceWithRecipe({
845 rows: persistence.rows,
846 workspaceId,
847 sourceId,
848 }),
849 runtimeLocalWorkspace,
850 ),
851 ),
852 ).rejects.toThrow(`Invalid OpenAPI manifest for ${sourceId}`);
853 } finally {
854 await persistence.close();
855 }
856 });
857 });
858});
packages/control-plane/src/runtime/source-recipes-runtime.ts+28−15
@@ -1,7 +1,7 @@
1import {1import {
2 type ToolCatalogEntry,
2 type ToolDescriptor,3 type ToolDescriptor,
3} from "@executor/codemode-core";4} from "@executor/codemode-core";
4import type { SqlControlPlaneRows } from "#persistence";
5import type {5import type {
6 AccountId,6 AccountId,
7 Source,7 Source,
@@ -13,19 +13,20 @@ import type { · 5 unmodified lines
13 WorkspaceId,13 WorkspaceId,
14} from "#schema";14} from "#schema";
15import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
16
17import {
18 readLocalSourceArtifact,
19} from "./local-source-artifacts";
20import { LocalSourceArtifactMissingError } from "./local-errors";16import { LocalSourceArtifactMissingError } from "./local-errors";
21import {17import {
22 requireRuntimeLocalWorkspace,18 requireRuntimeLocalWorkspace,
23} from "./local-runtime-context";19} from "./local-runtime-context";
20import {
21 type WorkspaceStorageServices,
22 SourceArtifactStore,
23} from "./local-storage";
24import { namespaceFromSourceName } from "./source-names";24import { namespaceFromSourceName } from "./source-names";
25import {25import {
26 getSourceAdapterForOperation,26 getSourceAdapterForOperation,
27 getSourceAdapterForSource,27 getSourceAdapterForSource,
28} from "./source-adapters";28} from "./source-adapters";
29import type { ControlPlaneStoreShape } from "./store";
29import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";30import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
30import { firstSchemaBundle } from "./source-adapters/shared";31import { firstSchemaBundle } from "./source-adapters/shared";
31import { loadSourceById, loadSourcesInWorkspace } from "./source-store";32import { loadSourceById, loadSourcesInWorkspace } from "./source-store";
@@ -68,6 +69,16 @@ export type LoadedSourceRecipeToolIndexEntry = { · 36 unmodified lines
68 descriptor: ToolDescriptor;69 descriptor: ToolDescriptor;
69};70};
70 71
72export const recipeToolCatalogEntry = (input: {
73 tool: LoadedSourceRecipeToolIndexEntry;
74 score: (queryTokens: readonly string[]) => number;
75}): ToolCatalogEntry => ({
76 descriptor: input.tool.descriptor,
77 namespace: input.tool.searchNamespace,
78 searchText: input.tool.searchText,
79 score: input.score,
80});
81
71const parseManifestForRecipe = (input: {82const parseManifestForRecipe = (input: {
72 source: Source;83 source: Source;
73 revision: StoredSourceRecipeRevisionRecord;84 revision: StoredSourceRecipeRevisionRecord;
@@ -157,18 +168,19 @@ const primarySchemaBundleForRevision = (input: { · 83 unmodified lines
157};168};
158 169
159export const loadWorkspaceSourceRecipes = (input: {170export const loadWorkspaceSourceRecipes = (input: {
160 rows: SqlControlPlaneRows;171 rows: ControlPlaneStoreShape;
161 workspaceId: WorkspaceId;172 workspaceId: WorkspaceId;
162 actorAccountId?: AccountId | null;173 actorAccountId?: AccountId | null;
163}): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> =>174}): Effect.Effect<readonly LoadedSourceRecipe[], Error, WorkspaceStorageServices> =>
164 Effect.gen(function* () {175 Effect.gen(function* () {
165 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);176 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
177 const sourceArtifactStore = yield* SourceArtifactStore;
166 const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, {178 const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, {
167 actorAccountId: input.actorAccountId,179 actorAccountId: input.actorAccountId,
168 });180 });
169 const localRecipes = yield* Effect.forEach(sources, (source) =>181 const localRecipes = yield* Effect.forEach(sources, (source) =>
170 Effect.gen(function* () {182 Effect.gen(function* () {
171 const artifact = yield* readLocalSourceArtifact({183 const artifact = yield* sourceArtifactStore.read({
172 context: runtimeLocalWorkspace.context,184 context: runtimeLocalWorkspace.context,
173 sourceId: source.id,185 sourceId: source.id,
174 });186 });
@@ -215,19 +227,20 @@ export const loadWorkspaceSourceRecipes = (input: { · 40 unmodified lines
215 });227 });
216 228
217export const loadSourceWithRecipe = (input: {229export const loadSourceWithRecipe = (input: {
218 rows: SqlControlPlaneRows;230 rows: ControlPlaneStoreShape;
219 workspaceId: WorkspaceId;231 workspaceId: WorkspaceId;
220 sourceId: Source["id"];232 sourceId: Source["id"];
221 actorAccountId?: AccountId | null;233 actorAccountId?: AccountId | null;
222}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> =>234}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, WorkspaceStorageServices> =>
223 Effect.gen(function* () {235 Effect.gen(function* () {
224 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);236 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
237 const sourceArtifactStore = yield* SourceArtifactStore;
225 const source = yield* loadSourceById(input.rows, {238 const source = yield* loadSourceById(input.rows, {
226 workspaceId: input.workspaceId,239 workspaceId: input.workspaceId,
227 sourceId: input.sourceId,240 sourceId: input.sourceId,
228 actorAccountId: input.actorAccountId,241 actorAccountId: input.actorAccountId,
229 });242 });
230 const artifact = yield* readLocalSourceArtifact({243 const artifact = yield* sourceArtifactStore.read({
231 context: runtimeLocalWorkspace.context,244 context: runtimeLocalWorkspace.context,
232 sourceId: source.id,245 sourceId: source.id,
233 });246 });
@@ -335,11 +348,11 @@ export const expandRecipeTools = (input: { · 101 unmodified lines
335 );348 );
336 349
337export const loadWorkspaceSourceRecipeToolIndex = (input: {350export const loadWorkspaceSourceRecipeToolIndex = (input: {
338 rows: SqlControlPlaneRows;351 rows: ControlPlaneStoreShape;
339 workspaceId: WorkspaceId;352 workspaceId: WorkspaceId;
340 actorAccountId?: AccountId | null;353 actorAccountId?: AccountId | null;
341 includeSchemas: boolean;354 includeSchemas: boolean;
342}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, never> =>355}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> =>
343 Effect.gen(function* () {356 Effect.gen(function* () {
344 const recipes = yield* loadWorkspaceSourceRecipes({357 const recipes = yield* loadWorkspaceSourceRecipes({
345 rows: input.rows,358 rows: input.rows,
@@ -364,12 +377,12 @@ export const loadWorkspaceSourceRecipeToolIndex = (input: { · 18 unmodified lines
364 });377 });
365 378
366export const loadWorkspaceSourceRecipeToolByPath = (input: {379export const loadWorkspaceSourceRecipeToolByPath = (input: {
367 rows: SqlControlPlaneRows;380 rows: ControlPlaneStoreShape;
368 workspaceId: WorkspaceId;381 workspaceId: WorkspaceId;
369 path: string;382 path: string;
370 actorAccountId?: AccountId | null;383 actorAccountId?: AccountId | null;
371 includeSchemas: boolean;384 includeSchemas: boolean;
372}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, never> =>385}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> =>
373 Effect.gen(function* () {386 Effect.gen(function* () {
374 const recipes = yield* loadWorkspaceSourceRecipes({387 const recipes = yield* loadWorkspaceSourceRecipes({
375 rows: input.rows,388 rows: input.rows,
packages/control-plane/src/runtime/source-store.test.ts+0−310
@@ -1,310 +0,0 @@
1import { mkdtempSync } from "node:fs";
2import { tmpdir } from "node:os";
3import { join } from "node:path";
4
5import { describe, expect, it } from "@effect/vitest";
6
7import {
8 AccountIdSchema,
9 decodeBuiltInAuthArtifactConfig,
10 McpSourceAuthSessionDataJsonSchema,
11 SecretMaterialIdSchema,
12 SourceAuthSessionIdSchema,
13 SourceIdSchema,
14 WorkspaceIdSchema,
15 type Source,
16} from "#schema";
17import * as Effect from "effect/Effect";
18import * as Option from "effect/Option";
19import * as Schema from "effect/Schema";
20import * as Scope from "effect/Scope";
21
22import {
23 createSqlControlPlanePersistence,
24 type SqlControlPlanePersistence,
25} from "../persistence";
26import {
27 loadLocalExecutorConfig,
28 resolveLocalWorkspaceContext,
29} from "./local-config";
30import {
31 RuntimeLocalWorkspaceService,
32 type RuntimeLocalWorkspaceState,
33} from "./local-runtime-context";
34import {
35 loadLocalWorkspaceState,
36} from "./local-workspace-state";
37import { persistSource, removeSourceById } from "./source-store";
38
39const makePersistence: Effect.Effect<SqlControlPlanePersistence, unknown, Scope.Scope> =
40 Effect.acquireRelease(
41 createSqlControlPlanePersistence({
42 localDataDir: ":memory:",
43 }),
44 (persistence) =>
45 Effect.tryPromise({
46 try: () => persistence.close(),
47 catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
48 }).pipe(Effect.orDie),
49 );
50
51const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema);
52
53const 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* resolveLocalWorkspaceContext({ workspaceRoot });
60 const loadedConfig = yield* loadLocalExecutorConfig(context);
61
62 return {
63 context,
64 installation: {
65 workspaceId: input.workspaceId,
66 accountId: input.accountId,
67 },
68 loadedConfig,
69 } satisfies RuntimeLocalWorkspaceState;
70 });
71
72const withRuntimeLocalWorkspace = <A, E>(
73 effect: Effect.Effect<A, E, never>,
74 runtimeLocalWorkspace: RuntimeLocalWorkspaceState,
75) =>
76 effect.pipe(
77 Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
78 );
79
80const makeOpenApiSource = (input: {
81 workspaceId: Source["workspaceId"];
82 sourceId: Source["id"];
83 now: number;
84 updatedAt?: number;
85 name?: string;
86 endpoint?: string;
87 specUrl?: string;
88 auth: Source["auth"];
89}): Source => ({
90 id: input.sourceId,
91 workspaceId: input.workspaceId,
92 name: input.name ?? "GitHub",
93 kind: "openapi",
94 endpoint: input.endpoint ?? "https://api.github.com",
95 status: "connected",
96 enabled: true,
97 namespace: "github",
98 bindingVersion: 1,
99 binding: {
100 specUrl: input.specUrl ?? "https://example.com/openapi.json",
101 defaultHeaders: null,
102 },
103 importAuthPolicy: "reuse_runtime",
104 importAuth: { kind: "none" },
105 auth: input.auth,
106 sourceHash: null,
107 lastError: null,
108 createdAt: input.now,
109 updatedAt: input.updatedAt ?? input.now,
110});
111
112describe("source-store", () => {
113 it.scoped("replaces superseded secrets and removes source auth state cleanly", () =>
114 Effect.gen(function* () {
115 const persistence = yield* makePersistence;
116 const now = Date.now();
117 const accountId = AccountIdSchema.make("acc_source_store");
118 const workspaceId = WorkspaceIdSchema.make("ws_source_store");
119 const sourceId = SourceIdSchema.make("github");
120 const firstTokenId = SecretMaterialIdSchema.make("sec_source_store_first");
121 const secondTokenId = SecretMaterialIdSchema.make("sec_source_store_second");
122 const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({
123 workspaceId,
124 accountId,
125 });
126
127 yield* persistence.rows.secretMaterials.upsert({
128 id: firstTokenId,
129 name: null,
130 purpose: "auth_material",
131 value: "ghp_first",
132 createdAt: now,
133 updatedAt: now,
134 });
135 yield* persistence.rows.secretMaterials.upsert({
136 id: secondTokenId,
137 name: null,
138 purpose: "auth_material",
139 value: "ghp_second",
140 createdAt: now,
141 updatedAt: now,
142 });
143
144 yield* withRuntimeLocalWorkspace(
145 persistSource(
146 persistence.rows,
147 makeOpenApiSource({
148 workspaceId,
149 sourceId,
150 now,
151 auth: {
152 kind: "bearer",
153 headerName: "Authorization",
154 prefix: "Bearer ",
155 token: {
156 providerId: "postgres",
157 handle: firstTokenId,
158 },
159 },
160 }),
161 ),
162 runtimeLocalWorkspace,
163 );
164
165 yield* persistence.rows.sourceAuthSessions.upsert({
166 id: SourceAuthSessionIdSchema.make("src_auth_source_store"),
167 workspaceId,
168 sourceId,
169 actorAccountId: accountId,
170 executionId: null,
171 interactionId: null,
172 providerKind: "mcp_oauth",
173 credentialSlot: "runtime",
174 status: "pending",
175 state: "state_source_store",
176 sessionDataJson: encodeSessionDataJson({
177 kind: "mcp_oauth",
178 endpoint: "https://api.github.com",
179 redirectUri: "http://127.0.0.1/callback",
180 scope: null,
181 resourceMetadataUrl: null,
182 authorizationServerUrl: null,
183 resourceMetadata: null,
184 authorizationServerMetadata: null,
185 clientInformation: null,
186 codeVerifier: "verifier",
187 authorizationUrl: "https://example.com/auth",
188 }),
189 errorText: null,
190 completedAt: null,
191 createdAt: now,
192 updatedAt: now,
193 });
194
195 yield* withRuntimeLocalWorkspace(
196 persistSource(
197 persistence.rows,
198 makeOpenApiSource({
199 workspaceId,
200 sourceId,
201 now,
202 updatedAt: now + 1,
203 auth: {
204 kind: "bearer",
205 headerName: "Authorization",
206 prefix: "Bearer ",
207 token: {
208 providerId: "postgres",
209 handle: secondTokenId,
210 },
211 },
212 }),
213 ),
214 runtimeLocalWorkspace,
215 );
216
217 expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(firstTokenId))).toBe(true);
218 const authArtifacts = yield* persistence.rows.authArtifacts.listByWorkspaceId(workspaceId);
219 expect(authArtifacts).toHaveLength(1);
220 const decoded = authArtifacts[0] ? decodeBuiltInAuthArtifactConfig(authArtifacts[0]) : null;
221 expect(
222 decoded !== null && decoded.artifactKind === "static_bearer"
223 ? decoded.config.token.handle
224 : null,
225 ).toBe(secondTokenId);
226
227 const removed = yield* withRuntimeLocalWorkspace(
228 removeSourceById(persistence.rows, {
229 workspaceId,
230 sourceId,
231 }),
232 runtimeLocalWorkspace,
233 );
234 expect(removed).toBe(true);
235
236 expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(secondTokenId))).toBe(true);
237 expect(yield* persistence.rows.authArtifacts.listByWorkspaceId(workspaceId)).toHaveLength(0);
238 expect(yield* persistence.rows.sourceAuthSessions.listByWorkspaceId(workspaceId)).toHaveLength(0);
239 const config = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);
240 expect(config.config?.sources?.[sourceId]).toBeUndefined();
241 const workspaceState = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context);
242 expect(workspaceState.sources[sourceId]).toBeUndefined();
243 }),
244 );
245
246 it.scoped("creates a fresh actor-scoped credential when a shared credential already exists", () =>
247 Effect.gen(function* () {
248 const persistence = yield* makePersistence;
249 const now = Date.now();
250 const accountId = AccountIdSchema.make("acc_actor_scoped");
251 const workspaceId = WorkspaceIdSchema.make("ws_actor_scoped");
252 const sourceId = SourceIdSchema.make("github");
253 const sharedTokenId = SecretMaterialIdSchema.make("sec_actor_scoped_shared");
254 const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({
255 workspaceId,
256 accountId,
257 });
258 yield* persistence.rows.secretMaterials.upsert({
259 id: sharedTokenId,
260 name: null,
261 purpose: "auth_material",
262 value: "ghp_shared",
263 createdAt: now,
264 updatedAt: now,
265 });
266
267 const source = makeOpenApiSource({
268 workspaceId,
269 sourceId,
270 now,
271 auth: {
272 kind: "bearer",
273 headerName: "Authorization",
274 prefix: "Bearer ",
275 token: {
276 providerId: "postgres",
277 handle: sharedTokenId,
278 },
279 },
280 });
281
282 yield* withRuntimeLocalWorkspace(
283 persistSource(persistence.rows, source),
284 runtimeLocalWorkspace,
285 );
286 yield* withRuntimeLocalWorkspace(
287 persistSource(
288 persistence.rows,
289 {
290 ...source,
291 updatedAt: now + 1,
292 },
293 {
294 actorAccountId: accountId,
295 },
296 ),
297 runtimeLocalWorkspace,
298 );
299
300 const authArtifacts = yield* persistence.rows.authArtifacts.listByWorkspaceAndSourceId({
301 workspaceId,
302 sourceId,
303 });
304 expect(authArtifacts).toHaveLength(2);
305 expect(authArtifacts.some((artifact) => artifact.actorAccountId === null)).toBe(true);
306 expect(authArtifacts.some((artifact) => artifact.actorAccountId === accountId)).toBe(true);
307 expect(new Set(authArtifacts.map((artifact) => artifact.id)).size).toBe(2);
308 }),
309 );
310});
packages/control-plane/src/runtime/source-store.ts+49−29
@@ -9,7 +9,6 @@ import type { · 8 unmodified lines
9 WorkspaceId,9 WorkspaceId,
10} from "#schema";10} from "#schema";
11import { SourceIdSchema } from "#schema";11import { SourceIdSchema } from "#schema";
12import { type SqlControlPlaneRows } from "#persistence";
13import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
14 13
15import {14import {
@@ -21,15 +20,9 @@ import { · 5 unmodified lines
21 sourceAuthFromAuthArtifact,20 sourceAuthFromAuthArtifact,
22} from "./auth-artifacts";21} from "./auth-artifacts";
23import {22import {
24 loadLocalExecutorConfig,
25 type LoadedLocalExecutorConfig,23 type LoadedLocalExecutorConfig,
26 type ResolvedLocalWorkspaceContext,24 type ResolvedLocalWorkspaceContext,
27 writeProjectLocalExecutorConfig,
28} from "./local-config";25} from "./local-config";
29import {
30 readLocalSourceArtifact,
31 removeLocalSourceArtifact,
32} from "./local-source-artifacts";
33import {26import {
34 LocalConfiguredSourceNotFoundError,27 LocalConfiguredSourceNotFoundError,
35 LocalExecutorConfigDecodeError,28 LocalExecutorConfigDecodeError,
@@ -42,9 +35,18 @@ import { · 6 unmodified lines
42import {35import {
43 requireRuntimeLocalWorkspace,36 requireRuntimeLocalWorkspace,
44} from "./local-runtime-context";37} from "./local-runtime-context";
38import type {
39 SourceArtifactStoreShape,
40 WorkspaceStorageServices,
41 WorkspaceConfigStoreShape,
42 WorkspaceStateStoreShape,
43} from "./local-storage";
44import {
45 SourceArtifactStore,
46 WorkspaceConfigStore,
47 WorkspaceStateStore,
48} from "./local-storage";
45import {49import {
46 loadLocalWorkspaceState,
47 writeLocalWorkspaceState,
48 type LocalWorkspaceState,50 type LocalWorkspaceState,
49} from "./local-workspace-state";51} from "./local-workspace-state";
50import {52import {
@@ -54,13 +56,14 @@ import { · 3 unmodified lines
54import { createDefaultSecretMaterialDeleter } from "./secret-material-providers";56import { createDefaultSecretMaterialDeleter } from "./secret-material-providers";
55import { authArtifactSecretMaterialRefs } from "./auth-artifacts";57import { authArtifactSecretMaterialRefs } from "./auth-artifacts";
56import { removeAuthLeaseAndSecrets } from "./auth-leases";58import { removeAuthLeaseAndSecrets } from "./auth-leases";
59import type { ControlPlaneStoreShape } from "./store";
57import { getSourceAdapter } from "./source-adapters";60import { getSourceAdapter } from "./source-adapters";
58import { slugify } from "./slug";61import { slugify } from "./slug";
59 62
60const secretRefKey = (ref: { providerId: string; handle: string }): string =>63const secretRefKey = (ref: { providerId: string; handle: string }): string =>
61 `${ref.providerId}:${ref.handle}`;64 `${ref.providerId}:${ref.handle}`;
62 65
63const cleanupAuthArtifactSecretRefs = (rows: SqlControlPlaneRows, input: {66const cleanupAuthArtifactSecretRefs = (rows: ControlPlaneStoreShape, input: {
64 previous: AuthArtifact | null;67 previous: AuthArtifact | null;
65 next: AuthArtifact | null;68 next: AuthArtifact | null;
66}) =>69}) =>
@@ -246,6 +249,9 @@ const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{ · 179 unmodified lines
246 workspaceId: WorkspaceId;249 workspaceId: WorkspaceId;
247 accountId: AccountId;250 accountId: AccountId;
248 };251 };
252 workspaceConfigStore: WorkspaceConfigStoreShape;
253 workspaceStateStore: WorkspaceStateStoreShape;
254 sourceArtifactStore: SourceArtifactStoreShape;
249 loadedConfig: LoadedLocalExecutorConfig;255 loadedConfig: LoadedLocalExecutorConfig;
250 workspaceState: LocalWorkspaceState;256 workspaceState: LocalWorkspaceState;
251},257},
@@ -253,16 +259,27 @@ const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{ · 1 unmodified line
253 | RuntimeLocalWorkspaceMismatchError259 | RuntimeLocalWorkspaceMismatchError
254 | LocalFileSystemError260 | LocalFileSystemError
255 | LocalExecutorConfigDecodeError261 | LocalExecutorConfigDecodeError
256 | LocalWorkspaceStateDecodeError,262 | LocalWorkspaceStateDecodeError
257never> =>263 | Error,
264WorkspaceStorageServices> =>
258 Effect.gen(function* () {265 Effect.gen(function* () {
259 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);266 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
260 const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);267 const workspaceConfigStore = yield* WorkspaceConfigStore;
261 const workspaceState = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context);268 const workspaceStateStore = yield* WorkspaceStateStore;
269 const sourceArtifactStore = yield* SourceArtifactStore;
270 const loadedConfig = yield* workspaceConfigStore.load(
271 runtimeLocalWorkspace.context,
272 );
273 const workspaceState = yield* workspaceStateStore.load(
274 runtimeLocalWorkspace.context,
275 );
262 276
263 return {277 return {
264 context: runtimeLocalWorkspace.context,278 context: runtimeLocalWorkspace.context,
265 installation: runtimeLocalWorkspace.installation,279 installation: runtimeLocalWorkspace.installation,
280 workspaceConfigStore,
281 workspaceStateStore,
282 sourceArtifactStore,
266 loadedConfig,283 loadedConfig,
267 workspaceState,284 workspaceState,
268 };285 };
@@ -270,6 +287,7 @@ never> => · 1 unmodified line
270 287
271const buildLocalSourceRecord = (input: {288const buildLocalSourceRecord = (input: {
272 context: ResolvedLocalWorkspaceContext;289 context: ResolvedLocalWorkspaceContext;
290 sourceArtifactStore: SourceArtifactStoreShape;
273 workspaceId: WorkspaceId;291 workspaceId: WorkspaceId;
274 loadedConfig: LoadedLocalExecutorConfig;292 loadedConfig: LoadedLocalExecutorConfig;
275 workspaceState: LocalWorkspaceState;293 workspaceState: LocalWorkspaceState;
@@ -317,7 +335,7 @@ const buildLocalSourceRecord = (input: { · 41 unmodified lines
317 updatedAt: existingState?.updatedAt ?? Date.now(),335 updatedAt: existingState?.updatedAt ?? Date.now(),
318 });336 });
319 337
320 const artifact = yield* readLocalSourceArtifact({338 const artifact = yield* input.sourceArtifactStore.read({
321 context: input.context,339 context: input.context,
322 sourceId: input.sourceId,340 sourceId: input.sourceId,
323 }).pipe(341 }).pipe(
@@ -379,7 +397,7 @@ const buildLocalSourceRecord = (input: { · 55 unmodified lines
379 });397 });
380 398
381export const loadSourcesInWorkspace = (399export const loadSourcesInWorkspace = (
382 rows: SqlControlPlaneRows,400 rows: ControlPlaneStoreShape,
383 workspaceId: WorkspaceId,401 workspaceId: WorkspaceId,
384 options: {402 options: {
385 actorAccountId?: AccountId | null;403 actorAccountId?: AccountId | null;
@@ -393,7 +411,7 @@ export const loadSourcesInWorkspace = ( · 7 unmodified lines
393 | LocalWorkspaceStateDecodeError411 | LocalWorkspaceStateDecodeError
394 | LocalConfiguredSourceNotFoundError412 | LocalConfiguredSourceNotFoundError
395 | Error,413 | Error,
396 never414 WorkspaceStorageServices
397> =>415> =>
398 Effect.gen(function* () {416 Effect.gen(function* () {
399 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);417 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);
@@ -404,6 +422,7 @@ export const loadSourcesInWorkspace = ( · 4 unmodified lines
404 Effect.map(422 Effect.map(
405 buildLocalSourceRecord({423 buildLocalSourceRecord({
406 context: localWorkspace.context,424 context: localWorkspace.context,
425 sourceArtifactStore: localWorkspace.sourceArtifactStore,
407 workspaceId,426 workspaceId,
408 loadedConfig: localWorkspace.loadedConfig,427 loadedConfig: localWorkspace.loadedConfig,
409 workspaceState: localWorkspace.workspaceState,428 workspaceState: localWorkspace.workspaceState,
@@ -416,7 +435,7 @@ export const loadSourcesInWorkspace = ( · 6 unmodified lines
416 );435 );
417 });436 });
418 437
419export const loadSourceById = (rows: SqlControlPlaneRows, input: {438export const loadSourceById = (rows: ControlPlaneStoreShape, input: {
420 workspaceId: WorkspaceId;439 workspaceId: WorkspaceId;
421 sourceId: Source["id"];440 sourceId: Source["id"];
422 actorAccountId?: AccountId | null;441 actorAccountId?: AccountId | null;
@@ -429,7 +448,7 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 6 unmodified lines
429 | LocalWorkspaceStateDecodeError448 | LocalWorkspaceStateDecodeError
430 | LocalConfiguredSourceNotFoundError449 | LocalConfiguredSourceNotFoundError
431 | Error,450 | Error,
432 never451 WorkspaceStorageServices
433> =>452> =>
434 Effect.gen(function* () {453 Effect.gen(function* () {
435 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);454 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
@@ -445,6 +464,7 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 9 unmodified lines
445 464
446 const localSource = yield* buildLocalSourceRecord({465 const localSource = yield* buildLocalSourceRecord({
447 context: localWorkspace.context,466 context: localWorkspace.context,
467 sourceArtifactStore: localWorkspace.sourceArtifactStore,
448 workspaceId: input.workspaceId,468 workspaceId: input.workspaceId,
449 loadedConfig: localWorkspace.loadedConfig,469 loadedConfig: localWorkspace.loadedConfig,
450 workspaceState: localWorkspace.workspaceState,470 workspaceState: localWorkspace.workspaceState,
@@ -514,7 +534,7 @@ const configSourceFromLocalSource = (input: { · 63 unmodified lines
514 }534 }
515};535};
516 536
517const removeAuthArtifactsForSource = (rows: SqlControlPlaneRows, input: {537const removeAuthArtifactsForSource = (rows: ControlPlaneStoreShape, input: {
518 workspaceId: WorkspaceId;538 workspaceId: WorkspaceId;
519 sourceId: Source["id"];539 sourceId: Source["id"];
520}) =>540}) =>
@@ -551,10 +571,10 @@ const removeAuthArtifactsForSource = (rows: SqlControlPlaneRows, input: { · 30 unmodified lines
551 return existingAuthArtifacts.length;571 return existingAuthArtifacts.length;
552 });572 });
553 573
554export const removeSourceById = (rows: SqlControlPlaneRows, input: {574export const removeSourceById = (rows: ControlPlaneStoreShape, input: {
555 workspaceId: WorkspaceId;575 workspaceId: WorkspaceId;
556 sourceId: Source["id"];576 sourceId: Source["id"];
557}) =>577}): Effect.Effect<boolean, Error, WorkspaceStorageServices> =>
558 Effect.gen(function* () {578 Effect.gen(function* () {
559 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);579 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
560 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {580 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
@@ -566,7 +586,7 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 5 unmodified lines
566 ...(projectConfig.sources ?? {}),586 ...(projectConfig.sources ?? {}),
567 };587 };
568 delete sources[input.sourceId];588 delete sources[input.sourceId];
569 yield* writeProjectLocalExecutorConfig({589 yield* localWorkspace.workspaceConfigStore.writeProject({
570 context: localWorkspace.context,590 context: localWorkspace.context,
571 config: {591 config: {
572 ...projectConfig,592 ...projectConfig,
@@ -582,11 +602,11 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 9 unmodified lines
582 ...localWorkspace.workspaceState,602 ...localWorkspace.workspaceState,
583 sources: remainingSources,603 sources: remainingSources,
584 };604 };
585 yield* writeLocalWorkspaceState({605 yield* localWorkspace.workspaceStateStore.write({
586 context: localWorkspace.context,606 context: localWorkspace.context,
587 state: workspaceState,607 state: workspaceState,
588 });608 });
589 yield* removeLocalSourceArtifact({609 yield* localWorkspace.sourceArtifactStore.remove({
590 context: localWorkspace.context,610 context: localWorkspace.context,
591 sourceId: input.sourceId,611 sourceId: input.sourceId,
592 });612 });
@@ -605,12 +625,12 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 12 unmodified lines
605 });625 });
606 626
607export const persistSource = (627export const persistSource = (
608 rows: SqlControlPlaneRows,628 rows: ControlPlaneStoreShape,
609 source: Source,629 source: Source,
610 options: {630 options: {
611 actorAccountId?: AccountId | null;631 actorAccountId?: AccountId | null;
612 } = {},632 } = {},
613) =>633): Effect.Effect<Source, Error, WorkspaceStorageServices> =>
614 Effect.gen(function* () {634 Effect.gen(function* () {
615 const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId);635 const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId);
616 const nextSource = {636 const nextSource = {
@@ -648,7 +668,7 @@ export const persistSource = ( · 31 unmodified lines
648 existingConfigAuth: existingConfigSource?.connection.auth,668 existingConfigAuth: existingConfigSource?.connection.auth,
649 config: localWorkspace.loadedConfig.config,669 config: localWorkspace.loadedConfig.config,
650 });670 });
651 yield* writeProjectLocalExecutorConfig({671 yield* localWorkspace.workspaceConfigStore.writeProject({
652 context: localWorkspace.context,672 context: localWorkspace.context,
653 config: {673 config: {
654 ...projectConfig,674 ...projectConfig,
@@ -737,7 +757,7 @@ export const persistSource = ( · 82 unmodified lines
737 },757 },
738 },758 },
739 };759 };
740 yield* writeLocalWorkspaceState({760 yield* localWorkspace.workspaceStateStore.write({
741 context: localWorkspace.context,761 context: localWorkspace.context,
742 state: workspaceState,762 state: workspaceState,
743 });763 });
packages/control-plane/src/runtime/store.ts+199−9
@@ -1,15 +1,205 @@
1import { type SqlControlPlaneRows, SqlControlPlaneRowsService } from "#persistence";
2import * as Context from "effect/Context";1import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";2import type * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5 3
6export type ControlPlaneStoreShape = SqlControlPlaneRows;4import type {
5 AuthArtifact,
6 AuthLease,
7 Execution,
8 ExecutionInteraction,
9 ExecutionStep,
10 SecretMaterial,
11 SourceAuthSession,
12 WorkspaceSourceOauthClient,
13} from "#schema";
14
15type SecretMaterialSummary = {
16 id: string;
17 name: string | null;
18 purpose: string;
19 createdAt: number;
20 updatedAt: number;
21};
22
23export type ControlPlaneStoreShape = {
24 authArtifacts: {
25 listByWorkspaceId: (
26 workspaceId: AuthArtifact["workspaceId"],
27 ) => Effect.Effect<readonly AuthArtifact[], Error, never>;
28 listByWorkspaceAndSourceId: (input: {
29 workspaceId: AuthArtifact["workspaceId"];
30 sourceId: AuthArtifact["sourceId"];
31 }) => Effect.Effect<readonly AuthArtifact[], Error, never>;
32 getByWorkspaceSourceAndActor: (input: {
33 workspaceId: AuthArtifact["workspaceId"];
34 sourceId: AuthArtifact["sourceId"];
35 actorAccountId: AuthArtifact["actorAccountId"];
36 slot: AuthArtifact["slot"];
37 }) => Effect.Effect<import("effect/Option").Option<AuthArtifact>, Error, never>;
38 upsert: (artifact: AuthArtifact) => Effect.Effect<void, Error, never>;
39 removeByWorkspaceSourceAndActor: (input: {
40 workspaceId: AuthArtifact["workspaceId"];
41 sourceId: AuthArtifact["sourceId"];
42 actorAccountId: AuthArtifact["actorAccountId"];
43 slot?: AuthArtifact["slot"];
44 }) => Effect.Effect<boolean, Error, never>;
45 removeByWorkspaceAndSourceId: (input: {
46 workspaceId: AuthArtifact["workspaceId"];
47 sourceId: AuthArtifact["sourceId"];
48 }) => Effect.Effect<number, Error, never>;
49 };
50 authLeases: {
51 listAll: () => Effect.Effect<readonly AuthLease[], Error, never>;
52 getByAuthArtifactId: (
53 authArtifactId: AuthLease["authArtifactId"],
54 ) => Effect.Effect<import("effect/Option").Option<AuthLease>, Error, never>;
55 upsert: (lease: AuthLease) => Effect.Effect<void, Error, never>;
56 removeByAuthArtifactId: (
57 authArtifactId: AuthLease["authArtifactId"],
58 ) => Effect.Effect<boolean, Error, never>;
59 };
60 sourceOauthClients: {
61 getByWorkspaceSourceAndProvider: (input: {
62 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
63 sourceId: WorkspaceSourceOauthClient["sourceId"];
64 providerKey: string;
65 }) => Effect.Effect<
66 import("effect/Option").Option<WorkspaceSourceOauthClient>,
67 Error,
68 never
69 >;
70 upsert: (
71 oauthClient: WorkspaceSourceOauthClient,
72 ) => Effect.Effect<void, Error, never>;
73 removeByWorkspaceAndSourceId: (input: {
74 workspaceId: WorkspaceSourceOauthClient["workspaceId"];
75 sourceId: WorkspaceSourceOauthClient["sourceId"];
76 }) => Effect.Effect<number, Error, never>;
77 };
78 sourceAuthSessions: {
79 listAll: () => Effect.Effect<readonly SourceAuthSession[], Error, never>;
80 listByWorkspaceId: (
81 workspaceId: SourceAuthSession["workspaceId"],
82 ) => Effect.Effect<readonly SourceAuthSession[], Error, never>;
83 getById: (
84 id: SourceAuthSession["id"],
85 ) => Effect.Effect<import("effect/Option").Option<SourceAuthSession>, Error, never>;
86 getByState: (
87 state: SourceAuthSession["state"],
88 ) => Effect.Effect<import("effect/Option").Option<SourceAuthSession>, Error, never>;
89 getPendingByWorkspaceSourceAndActor: (input: {
90 workspaceId: SourceAuthSession["workspaceId"];
91 sourceId: SourceAuthSession["sourceId"];
92 actorAccountId: SourceAuthSession["actorAccountId"];
93 credentialSlot?: SourceAuthSession["credentialSlot"];
94 }) => Effect.Effect<
95 import("effect/Option").Option<SourceAuthSession>,
96 Error,
97 never
98 >;
99 insert: (session: SourceAuthSession) => Effect.Effect<void, Error, never>;
100 update: (
101 id: SourceAuthSession["id"],
102 patch: Partial<
103 Omit<SourceAuthSession, "id" | "workspaceId" | "sourceId" | "createdAt">
104 >,
105 ) => Effect.Effect<import("effect/Option").Option<SourceAuthSession>, Error, never>;
106 upsert: (session: SourceAuthSession) => Effect.Effect<void, Error, never>;
107 removeByWorkspaceAndSourceId: (
108 workspaceId: SourceAuthSession["workspaceId"],
109 sourceId: SourceAuthSession["sourceId"],
110 ) => Effect.Effect<boolean, Error, never>;
111 };
112 secretMaterials: {
113 getById: (
114 id: SecretMaterial["id"],
115 ) => Effect.Effect<import("effect/Option").Option<SecretMaterial>, Error, never>;
116 listAll: () => Effect.Effect<readonly SecretMaterialSummary[], Error, never>;
117 upsert: (material: SecretMaterial) => Effect.Effect<void, Error, never>;
118 updateById: (
119 id: SecretMaterial["id"],
120 update: { name?: string | null; value?: string },
121 ) => Effect.Effect<
122 import("effect/Option").Option<SecretMaterialSummary>,
123 Error,
124 never
125 >;
126 removeById: (id: SecretMaterial["id"]) => Effect.Effect<boolean, Error, never>;
127 listLinkedSources: () => Effect.Effect<
128 Map<string, Array<{ sourceId: string; sourceName: string }>>,
129 Error,
130 never
131 >;
132 };
133 executions: {
134 getById: (
135 executionId: Execution["id"],
136 ) => Effect.Effect<import("effect/Option").Option<Execution>, Error, never>;
137 getByWorkspaceAndId: (
138 workspaceId: Execution["workspaceId"],
139 executionId: Execution["id"],
140 ) => Effect.Effect<import("effect/Option").Option<Execution>, Error, never>;
141 insert: (execution: Execution) => Effect.Effect<void, Error, never>;
142 update: (
143 executionId: Execution["id"],
144 patch: Partial<
145 Omit<Execution, "id" | "workspaceId" | "createdByAccountId" | "createdAt">
146 >,
147 ) => Effect.Effect<import("effect/Option").Option<Execution>, Error, never>;
148 };
149 executionInteractions: {
150 getById: (
151 interactionId: ExecutionInteraction["id"],
152 ) => Effect.Effect<
153 import("effect/Option").Option<ExecutionInteraction>,
154 Error,
155 never
156 >;
157 listByExecutionId: (
158 executionId: ExecutionInteraction["executionId"],
159 ) => Effect.Effect<readonly ExecutionInteraction[], Error, never>;
160 getPendingByExecutionId: (
161 executionId: ExecutionInteraction["executionId"],
162 ) => Effect.Effect<
163 import("effect/Option").Option<ExecutionInteraction>,
164 Error,
165 never
166 >;
167 insert: (
168 interaction: ExecutionInteraction,
169 ) => Effect.Effect<void, Error, never>;
170 update: (
171 interactionId: ExecutionInteraction["id"],
172 patch: Partial<
173 Omit<ExecutionInteraction, "id" | "executionId" | "createdAt">
174 >,
175 ) => Effect.Effect<
176 import("effect/Option").Option<ExecutionInteraction>,
177 Error,
178 never
179 >;
180 };
181 executionSteps: {
182 getByExecutionAndSequence: (
183 executionId: ExecutionStep["executionId"],
184 sequence: ExecutionStep["sequence"],
185 ) => Effect.Effect<import("effect/Option").Option<ExecutionStep>, Error, never>;
186 listByExecutionId: (
187 executionId: ExecutionStep["executionId"],
188 ) => Effect.Effect<readonly ExecutionStep[], Error, never>;
189 insert: (step: ExecutionStep) => Effect.Effect<void, Error, never>;
190 deleteByExecutionId: (
191 executionId: ExecutionStep["executionId"],
192 ) => Effect.Effect<void, Error, never>;
193 updateByExecutionAndSequence: (
194 executionId: ExecutionStep["executionId"],
195 sequence: ExecutionStep["sequence"],
196 patch: Partial<
197 Omit<ExecutionStep, "id" | "executionId" | "sequence" | "createdAt">
198 >,
199 ) => Effect.Effect<import("effect/Option").Option<ExecutionStep>, Error, never>;
200 };
201};
7 202
8export class ControlPlaneStore extends Context.Tag(203export class ControlPlaneStore extends Context.Tag(
9 "#runtime/ControlPlaneStore",204 "#runtime/ControlPlaneStore",
10)<ControlPlaneStore, ControlPlaneStoreShape>() {}205)<ControlPlaneStore, ControlPlaneStoreShape>() {}
11
12export const ControlPlaneStoreLive = Layer.effect(
13 ControlPlaneStore,
14 Effect.map(SqlControlPlaneRowsService, (rows) => rows),
15);
packages/control-plane/src/runtime/workspace-execution-environment.test.ts+0−1986
@@ -1,1986 +0,0 @@
1import { randomUUID } from "node:crypto";
2import { createServer } from "node:http";
3import { dirname, join } from "node:path";
4
5import {
6 FileSystem,
7 HttpApi,
8 HttpApiBuilder,
9 HttpApiEndpoint,
10 HttpApiGroup,
11 HttpApiSchema,
12 HttpServer,
13} from "@effect/platform";
14import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
15import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
16import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
17import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
19import {
20 buildOpenApiToolPresentation,
21 compileOpenApiToolDefinitions,
22 extractOpenApiManifest,
23} from "@executor/codemode-openapi";
24import { describe, expect, it } from "@effect/vitest";
25import {
26 graphql,
27 GraphQLBoolean,
28 GraphQLID,
29 GraphQLInputObjectType,
30 GraphQLInt,
31 GraphQLList,
32 GraphQLNonNull,
33 GraphQLObjectType,
34 GraphQLSchema,
35 GraphQLString,
36} from "graphql";
37import {
38 AccountIdSchema,
39 ExecutionIdSchema,
40 SecretMaterialIdSchema,
41 SourceIdSchema,
42 SourceRecipeSchemaBundleIdSchema,
43 WorkspaceIdSchema,
44 type Source,
45} from "#schema";
46import * as Context from "effect/Context";
47import * as Effect from "effect/Effect";
48import * as Exit from "effect/Exit";
49import * as Layer from "effect/Layer";
50import * as Scope from "effect/Scope";
51import { Schema } from "effect";
52import * as z from "zod/v4";
53
54import {
55 createSqlControlPlanePersistence,
56 type SqlControlPlanePersistence,
57} from "#persistence";
58import type { SourceId, WorkspaceId } from "#schema";
59
60import {
61 createRuntimeSourceAuthService,
62 type RuntimeSourceAuthService,
63} from "./source-auth-service";
64import {
65 loadLocalExecutorConfig,
66 resolveLocalWorkspaceContext,
67 writeProjectLocalExecutorConfig,
68} from "./local-config";
69import { createLiveExecutionManager } from "./live-execution";
70import {
71 RuntimeLocalWorkspaceService,
72 type RuntimeLocalWorkspaceState,
73} from "./local-runtime-context";
74import {
75 buildLocalSourceArtifact,
76 readLocalSourceArtifact,
77 writeLocalSourceArtifact,
78} from "./local-source-artifacts";
79import {
80 loadLocalWorkspaceState,
81 writeLocalWorkspaceState,
82} from "./local-workspace-state";
83import { materializationFromMcpManifestEntries } from "./source-adapters/mcp";
84import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment";
85
86type CountedMcpServer = {
87 endpoint: string;
88 counts: {
89 listTools: number;
90 callTool: number;
91 };
92 close: () => Promise<void>;
93};
94
95type OpenApiSpecServer = {
96 baseUrl: string;
97 specUrl: string;
98 seenAuthHeaders: Array<string | null>;
99 close: () => Promise<void>;
100};
101
102type GraphqlServer = {
103 endpoint: string;
104 seenAuthHeaders: Array<string | null>;
105 close: () => Promise<void>;
106};
107
108const normalizeSearchText = (
109 ...parts: ReadonlyArray<string | null | undefined>
110): string =>
111 parts
112 .flatMap((part) => (part ? [part.trim()] : []))
113 .filter(Boolean)
114 .join(" ")
115 .toLowerCase();
116
117const provideNodeFileSystem = <A, E, R>(
118 effect: Effect.Effect<A, E, R | FileSystem.FileSystem>,
119): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> =>
120 effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
121 A,
122 E,
123 Exclude<R, FileSystem.FileSystem>
124 >;
125
126const GraphqlUserType = new GraphQLObjectType({
127 name: "User",
128 fields: {
129 id: {
130 type: new GraphQLNonNull(GraphQLID),
131 },
132 name: {
133 type: new GraphQLNonNull(GraphQLString),
134 },
135 },
136});
137
138const GraphqlTeamType = new GraphQLObjectType({
139 name: "Team",
140 fields: {
141 id: {
142 type: new GraphQLNonNull(GraphQLID),
143 },
144 key: {
145 type: new GraphQLNonNull(GraphQLString),
146 },
147 name: {
148 type: new GraphQLNonNull(GraphQLString),
149 },
150 },
151});
152
153const GraphqlIssueType = new GraphQLObjectType({
154 name: "Issue",
155 fields: {
156 id: {
157 type: new GraphQLNonNull(GraphQLID),
158 },
159 identifier: {
160 type: new GraphQLNonNull(GraphQLString),
161 },
162 title: {
163 type: new GraphQLNonNull(GraphQLString),
164 },
165 team: {
166 type: new GraphQLNonNull(GraphqlTeamType),
167 },
168 },
169});
170
171const GraphqlPageInfoType = new GraphQLObjectType({
172 name: "PageInfo",
173 fields: {
174 hasNextPage: {
175 type: new GraphQLNonNull(GraphQLBoolean),
176 },
177 endCursor: {
178 type: GraphQLString,
179 },
180 },
181});
182
183const GraphqlIssueConnectionType = new GraphQLObjectType({
184 name: "IssueConnection",
185 fields: {
186 nodes: {
187 type: new GraphQLNonNull(
188 new GraphQLList(new GraphQLNonNull(GraphqlIssueType)),
189 ),
190 },
191 pageInfo: {
192 type: new GraphQLNonNull(GraphqlPageInfoType),
193 },
194 },
195});
196
197const GraphqlCreateIssueInputType = new GraphQLInputObjectType({
198 name: "CreateIssueInput",
199 fields: {
200 title: {
201 type: new GraphQLNonNull(GraphQLString),
202 },
203 teamId: {
204 type: GraphQLID,
205 },
206 },
207});
208
209const GraphqlQueryType = new GraphQLObjectType({
210 name: "Query",
211 fields: {
212 viewer: {
213 type: new GraphQLNonNull(GraphqlUserType),
214 description: "The current user",
215 resolve: () => ({
216 id: "usr_123",
217 name: "Rhys",
218 }),
219 },
220 issue: {
221 type: GraphqlIssueType,
222 description: "Find a single issue by id",
223 args: {
224 id: {
225 type: new GraphQLNonNull(GraphQLID),
226 },
227 },
228 resolve: (_source, args: { id: string }) => ({
229 id: args.id,
230 identifier: "ENG-123",
231 title: "Investigate GraphQL support",
232 team: {
233 id: "team_eng",
234 key: "ENG",
235 name: "Engineering",
236 },
237 }),
238 },
239 issues: {
240 type: new GraphQLNonNull(GraphqlIssueConnectionType),
241 description: "List issues for the workspace",
242 args: {
243 first: {
244 type: GraphQLInt,
245 },
246 query: {
247 type: GraphQLString,
248 },
249 },
250 resolve: () => ({
251 nodes: [
252 {
253 id: "iss_1",
254 identifier: "ENG-123",
255 title: "Investigate GraphQL support",
256 team: {
257 id: "team_eng",
258 key: "ENG",
259 name: "Engineering",
260 },
261 },
262 {
263 id: "iss_2",
264 identifier: "ENG-124",
265 title: "Ship GraphQL field tools",
266 team: {
267 id: "team_eng",
268 key: "ENG",
269 name: "Engineering",
270 },
271 },
272 ],
273 pageInfo: {
274 hasNextPage: false,
275 endCursor: "iss_2",
276 },
277 }),
278 },
279 },
280});
281
282const GraphqlMutationType = new GraphQLObjectType({
283 name: "Mutation",
284 fields: {
285 noop: {
286 type: GraphQLString,
287 resolve: () => "ok",
288 },
289 createIssue: {
290 type: new GraphQLNonNull(GraphqlIssueType),
291 description: "Create a new issue",
292 args: {
293 input: {
294 type: new GraphQLNonNull(GraphqlCreateIssueInputType),
295 },
296 },
297 resolve: (
298 _source,
299 args: { input: { title: string; teamId?: string | null } },
300 ) => ({
301 id: "iss_created",
302 identifier: "ENG-999",
303 title: args.input.title,
304 team: {
305 id: args.input.teamId ?? "team_eng",
306 key: "ENG",
307 name: "Engineering",
308 },
309 }),
310 },
311 },
312});
313
314const testGraphqlSchema = new GraphQLSchema({
315 query: GraphqlQueryType,
316 mutation: GraphqlMutationType,
317});
318
319const closeScope = (scope: Scope.CloseableScope) =>
320 Scope.close(scope, Exit.void).pipe(Effect.orDie);
321
322const makePersistence: Effect.Effect<
323 SqlControlPlanePersistence,
324 unknown,
325 Scope.Scope
326> = Effect.acquireRelease(
327 createSqlControlPlanePersistence({
328 localDataDir: ":memory:",
329 }),
330 (persistence) =>
331 Effect.tryPromise({
332 try: () => persistence.close(),
333 catch: (cause) =>
334 cause instanceof Error ? cause : new Error(String(cause)),
335 }).pipe(Effect.orDie),
336);
337
338const makeCountedMcpServer = Effect.acquireRelease(
339 Effect.promise<CountedMcpServer>(
340 () =>
341 new Promise<CountedMcpServer>((resolve, reject) => {
342 const counts = {
343 listTools: 0,
344 callTool: 0,
345 };
346 let closed = false;
347 const app = createMcpExpressApp({ host: "127.0.0.1" });
348
349 const createServer = () => {
350 const server = new McpServer(
351 {
352 name: "workspace-execution-environment-test-server",
353 version: "1.0.0",
354 },
355 {
356 capabilities: {
357 tools: {},
358 },
359 },
360 );
361
362 server.registerTool(
363 "echo",
364 {
365 description: "Echo the provided string",
366 inputSchema: {
367 value: z.string(),
368 },
369 },
370 async ({ value }: { value: string }) => ({
371 content: [{ type: "text", text: `echo:${value}` }],
372 }),
373 );
374
375 return server;
376 };
377
378 const countMethods = (body: unknown) => {
379 const payloads = Array.isArray(body) ? body : [body];
380 for (const payload of payloads) {
381 if (!payload || typeof payload !== "object") {
382 continue;
383 }
384
385 const method = "method" in payload ? payload.method : undefined;
386 if (method === "tools/list") {
387 counts.listTools += 1;
388 } else if (method === "tools/call") {
389 counts.callTool += 1;
390 }
391 }
392 };
393
394 const handle = async (req: any, res: any, parsedBody?: unknown) => {
395 countMethods(parsedBody);
396
397 const server = createServer();
398 const transport = new StreamableHTTPServerTransport({
399 sessionIdGenerator: undefined,
400 });
401
402 try {
403 await server.connect(transport);
404 await transport.handleRequest(req, res, parsedBody);
405 } finally {
406 await transport.close().catch(() => undefined);
407 await server.close().catch(() => undefined);
408 }
409 };
410
411 app.post("/mcp", async (req: any, res: any) => {
412 await handle(req, res, req.body);
413 });
414
415 app.get("/mcp", async (req: any, res: any) => {
416 await handle(req, res);
417 });
418
419 app.delete("/mcp", async (req: any, res: any) => {
420 await handle(req, res, req.body);
421 });
422
423 const listener = app.listen(0, "127.0.0.1", () => {
424 const address = listener.address();
425 if (!address || typeof address === "string") {
426 reject(new Error("failed to resolve MCP test server address"));
427 return;
428 }
429
430 resolve({
431 endpoint: `http://127.0.0.1:${address.port}/mcp`,
432 counts,
433 close: async () => {
434 if (closed) {
435 return;
436 }
437 closed = true;
438
439 await new Promise<void>((closeResolve, closeReject) => {
440 listener.close((error: Error | undefined) => {
441 if (error) {
442 closeReject(error);
443 return;
444 }
445 closeResolve();
446 });
447 });
448 },
449 });
450 });
451
452 listener.once("error", reject);
453 }),
454 ),
455 (server) =>
456 Effect.tryPromise({
457 try: () => server.close(),
458 catch: (error: unknown) =>
459 error instanceof Error ? error : new Error(String(error)),
460 }).pipe(Effect.orDie),
461);
462
463const ownerParam = HttpApiSchema.param("owner", Schema.String);
464const repoParam = HttpApiSchema.param("repo", Schema.String);
465
466class GeneratedReposApi extends HttpApiGroup.make("repos").add(
467 HttpApiEndpoint.get("getRepo")`/repos/${ownerParam}/${repoParam}`.addSuccess(
468 Schema.Struct({
469 ok: Schema.Boolean,
470 full_name: Schema.String,
471 }),
472 ),
473) {}
474
475class GeneratedApi extends HttpApi.make("generated").add(GeneratedReposApi) {}
476
477const makeOpenApiSpecServer = Effect.acquireRelease(
478 Effect.gen(function* () {
479 const seenAuthHeaders: OpenApiSpecServer["seenAuthHeaders"] = [];
480 const handlersLayer = HttpApiBuilder.group(
481 GeneratedApi,
482 "repos",
483 (handlers) =>
484 handlers.handle("getRepo", ({ path, request }) =>
485 Effect.sync(() => {
486 seenAuthHeaders.push(
487 typeof request.headers.authorization === "string"
488 ? request.headers.authorization

1498 more lines aren’t shown here.

packages/control-plane/src/runtime/workspace-execution-environment.ts+164−266

This diff is large: its 614 lines aren’t shown here.

packages/control-plane/src/schema/models/auth-artifact.ts+25−31

This diff is large: its 117 lines aren’t shown here.

packages/control-plane/src/schema/models/auth-lease.ts+2−9

This diff is large: its 31 lines aren’t shown here.

packages/control-plane/src/schema/models/auth/account.ts+12−17

This diff is large: its 47 lines aren’t shown here.

packages/control-plane/src/schema/models/code-migration.ts+5−8

This diff is large: its 20 lines aren’t shown here.

packages/control-plane/src/schema/models/execution.ts+46−55

This diff is large: its 160 lines aren’t shown here.

packages/control-plane/src/schema/models/secret-material.ts+3−9

This diff is large: its 30 lines aren’t shown here.

packages/control-plane/src/schema/models/source-auth-session.ts+5−9

This diff is large: its 37 lines aren’t shown here.

packages/control-plane/src/schema/models/source-oauth-client.ts+12−12

This diff is large: its 37 lines aren’t shown here.

packages/control-plane/src/schema/models/source-recipe.ts+34−45

This diff is large: its 126 lines aren’t shown here.

packages/control-plane/src/schema/models/source.ts+11−10

This diff is large: its 57 lines aren’t shown here.

packages/control-plane/tsconfig.json+4−0

This diff is large: its 10 lines aren’t shown here.

packages/kitchen-sink/src/source-runtime.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/react/src/index.test.tsx+0−1

This diff is large: its 8 lines aren’t shown here.

packages/server/src/config.ts+0−1

This diff is large: its 8 lines aren’t shown here.

packages/server/src/index.ts+0−4

This diff is large: its 25 lines aren’t shown here.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

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

Repository code

Go to file
t