History

Add executor MCP source OAuth flow

Rhys Sullivan committed 8 months ago

Commit d48e6bc1 parent ea7c60b25 files · +4295 −23Browse files

Unified · Split

25 files
  1. apps/executor/package.json +3 −1
  2. apps/executor/src/cli/main.ts +47 −2
  3. apps/executor/src/server/index.ts +6 −1
  4. apps/executor/src/server/server.test.ts +388 −0
  5. bun.lock +2 −0
  6. packages/control-plane/drizzle/20260306094249_large_giant_girl/migration.sql +39 −0
  7. packages/control-plane/drizzle/20260306094249_large_giant_girl/snapshot.json +2425 −0
  8. packages/control-plane/src/api/local/api.ts +10 −0
  9. packages/control-plane/src/api/local/http.ts +24 −7
  10. packages/control-plane/src/api/service.ts +10 −0
  11. packages/control-plane/src/persistence/control-plane-rows.ts +4 −0
  12. packages/control-plane/src/persistence/repos/index.ts +2 −0
  13. packages/control-plane/src/persistence/repos/secret-materials-repo.ts +55 −0
  14. packages/control-plane/src/persistence/repos/source-auth-sessions-repo.ts +153 −0
  15. packages/control-plane/src/persistence/schema.ts +66 −0
  16. packages/control-plane/src/runtime/execution-service.ts +32 −1
  17. packages/control-plane/src/runtime/executor-tools.ts +112 −0
  18. packages/control-plane/src/runtime/index.ts +15 −1
  19. packages/control-plane/src/runtime/services.ts +28 −2
  20. packages/control-plane/src/runtime/source-auth-service.ts +769 −0
  21. packages/control-plane/src/runtime/workspace-execution-environment.ts +22 −8
  22. packages/control-plane/src/schema/ids.ts +8 −0
  23. packages/control-plane/src/schema/index.ts +2 −0
  24. packages/control-plane/src/schema/models/secret-material.ts +28 −0
  25. packages/control-plane/src/schema/models/source-auth-session.ts +45 −0
apps/executor/package.json+3−1
@@ -25,8 +25,10 @@ · 24 unmodified lines
2525 "devDependencies": {
2626 "@executor-v3/mcp-elicitation-demo": "workspace:*",
2727 "@effect/vitest": "catalog:",
28+ "@modelcontextprotocol/sdk": "^1.26.0",
2829 "@types/node": "catalog:",
2930 "bun-types": "catalog:",
30− "vitest": "catalog:"
31+ "vitest": "catalog:",
32+ "zod": "catalog:"
3133 }
3234 }
apps/executor/src/cli/main.ts+47−2
@@ -205,8 +205,7 @@ const promptInteraction = (interaction: ExecutionInteraction) => · 204 unmodified lines
205205 yield* Effect.sync(() => {
206206 process.stdout.write(`${parsed.message}\n${parsed.url ?? ""}\n`);
207207 });
208− yield* promptLine("Press enter when done. ");
209− return JSON.stringify({ action: "accept" });
208+ return null;
210209 }
211210
212211 const line = yield* promptLine(`${parsed.message} [y/N] `);
@@ -227,6 +226,33 @@ const promptInteraction = (interaction: ExecutionInteraction) => · 14 unmodified lines
227226 });
228227 });
229228
229+const waitForExecutionProgress = (input: {
230+ client: ControlPlaneClient;
231+ workspaceId: ExecutionEnvelope["execution"]["workspaceId"];
232+ executionId: ExecutionEnvelope["execution"]["id"];
233+ pendingInteractionId: ExecutionInteraction["id"];
234+}) =>
235+ Effect.gen(function* () {
236+ while (true) {
237+ yield* sleep(SERVER_POLL_INTERVAL_MS);
238+
239+ const next = yield* input.client.executions.get({
240+ path: {
241+ workspaceId: input.workspaceId,
242+ executionId: input.executionId,
243+ },
244+ });
245+
246+ if (
247+ next.execution.status !== "waiting_for_interaction"
248+ || next.pendingInteraction === null
249+ || next.pendingInteraction.id !== input.pendingInteractionId
250+ ) {
251+ return next;
252+ }
253+ }
254+ });
255+
230256 const printExecution = (envelope: ExecutionEnvelope) =>
231257 Effect.sync(() => {
232258 const execution = envelope.execution;
@@ -314,6 +340,25 @@ const driveExecution = (input: { · 81 unmodified lines
314340 return current;
315341 }
316342
343+ const parsed = parseInteractionPayload(pending);
344+ if (parsed?.mode === "url") {
345+ yield* Effect.sync(() => {
346+ process.stdout.write(`${parsed.message}\n${parsed.url ?? ""}\n`);
347+ });
348+
349+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
350+ return current;
351+ }
352+
353+ current = yield* waitForExecutionProgress({
354+ client: input.client,
355+ workspaceId: input.workspaceId,
356+ executionId: current.execution.id,
357+ pendingInteractionId: pending.id,
358+ });
359+ continue;
360+ }
361+
317362 const responseJson = yield* promptInteraction(pending);
318363 if (responseJson === null) {
319364 yield* Effect.sync(() => {
apps/executor/src/server/index.ts+6−1
@@ -80,11 +80,14 @@ export const makeLocalExecutorServer = ( · 79 unmodified lines
8080 });
8181 }
8282
83+ let baseUrlRef: string | undefined;
84+
8385 const runtime = yield* Effect.acquireRelease(
8486 makeSqlControlPlaneRuntime({
8587 localDataDir,
8688 executionResolver: options.executionResolver,
8789 resolveSecretMaterial: options.resolveSecretMaterial,
90+ getLocalServerBaseUrl: () => baseUrlRef,
8891 }).pipe(
8992 Effect.mapError((cause) =>
9093 cause instanceof Error ? cause : new Error(String(cause)),
@@ -110,11 +113,13 @@ export const makeLocalExecutorServer = ( · 19 unmodified lines
110113 return yield* Effect.fail(new Error("Local executor server did not bind to a TCP address"));
111114 }
112115
116+ baseUrlRef = `http://${server.address.hostname}:${server.address.port}`;
117+
113118 return {
114119 runtime,
115120 host: server.address.hostname,
116121 port: server.address.port,
117− baseUrl: `http://${server.address.hostname}:${server.address.port}`,
122+ baseUrl: baseUrlRef,
118123 } satisfies LocalExecutorServer;
119124 });
120125
apps/executor/src/server/server.test.ts+388−0
@@ -1,4 +1,5 @@
11 import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2+import { randomUUID } from "node:crypto";
23
34 import {
45 HttpApi,
@@ -16,9 +17,14 @@ import { makeToolInvokerFromTools } from "@executor-v3/codemode-core"; · 11 unmodified lines
1617 import {
1718 ControlPlaneStorageError,
1819 makeControlPlaneClient,
20+ type ControlPlaneClient,
1921 type ResolveExecutionEnvironment,
2022 } from "@executor-v3/control-plane";
2123 import { makeInProcessExecutor } from "@executor-v3/runtime-local-inproc";
24+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
25+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
27+import { z } from "zod/v4";
2228
2329 import {
2430 seedDemoMcpSourceInWorkspace,
@@ -122,6 +128,304 @@ const startOpenApiDemoServer = async () => { · 97 unmodified lines
122128 };
123129 };
124130
131+const DEMO_OAUTH_ACCESS_TOKEN = "demo-access-token";
132+const DEMO_OAUTH_REFRESH_TOKEN = "demo-refresh-token";
133+
134+const startOAuthProtectedMcpServer = async () => {
135+ const host = "127.0.0.1";
136+ const app = createMcpExpressApp({ host });
137+ const transports: Record<string, StreamableHTTPServerTransport> = {};
138+ const servers: Record<string, McpServer> = {};
139+
140+ const createAuthorizedServer = () => {
141+ const server = new McpServer(
142+ {
143+ name: "executor-oauth-mcp-demo",
144+ version: "1.0.0",
145+ },
146+ {
147+ capabilities: {
148+ tools: {},
149+ },
150+ },
151+ );
152+
153+ server.registerTool(
154+ "whoami",
155+ {
156+ description: "Return the active identity for the OAuth demo source.",
157+ inputSchema: {},
158+ },
159+ async () => ({
160+ content: [{ type: "text", text: "oauth-demo" }],
161+ }),
162+ );
163+
164+ return server;
165+ };
166+
167+ const listener = await new Promise<import("node:http").Server>((resolve, reject) => {
168+ const server = app.listen(0, host);
169+
170+ const onListening = () => {
171+ server.off("error", onError);
172+ resolve(server);
173+ };
174+
175+ const onError = (error: Error) => {
176+ server.off("listening", onListening);
177+ reject(error);
178+ };
179+
180+ server.once("listening", onListening);
181+ server.once("error", onError);
182+ });
183+
184+ const address = listener.address();
185+ if (!address || typeof address === "string") {
186+ throw new Error("Failed to bind OAuth MCP demo server");
187+ }
188+
189+ const baseUrl = `http://${host}:${address.port}`;
190+ const endpoint = `${baseUrl}/mcp`;
191+ const resourceMetadataUrl = `${baseUrl}/.well-known/oauth-protected-resource/mcp`;
192+
193+ const requireBearerAuth = (req: IncomingMessage, res: ServerResponse) => {
194+ const authorization = req.headers.authorization;
195+ if (authorization === `Bearer ${DEMO_OAUTH_ACCESS_TOKEN}`) {
196+ return true;
197+ }
198+
199+ res.statusCode = 401;
200+ res.setHeader(
201+ "WWW-Authenticate",
202+ `Bearer resource_metadata="${resourceMetadataUrl}"`,
203+ );
204+ res.setHeader("content-type", "application/json");
205+ res.end(
206+ JSON.stringify({
207+ error: "unauthorized",
208+ }),
209+ );
210+ return false;
211+ };
212+
213+ app.get("/.well-known/oauth-protected-resource/mcp", (_req: any, res: any) => {
214+ res.status(200).json({
215+ resource: endpoint,
216+ authorization_servers: [baseUrl],
217+ scopes_supported: ["openid", "offline_access"],
218+ bearer_methods_supported: ["header"],
219+ });
220+ });
221+
222+ app.get("/.well-known/oauth-authorization-server", (_req: any, res: any) => {
223+ res.status(200).json({
224+ issuer: baseUrl,
225+ authorization_endpoint: `${baseUrl}/authorize`,
226+ token_endpoint: `${baseUrl}/token`,
227+ registration_endpoint: `${baseUrl}/register`,
228+ response_types_supported: ["code"],
229+ grant_types_supported: ["authorization_code", "refresh_token"],
230+ code_challenge_methods_supported: ["S256"],
231+ token_endpoint_auth_methods_supported: ["none"],
232+ });
233+ });
234+
235+ app.post("/register", (req: any, res: any) => {
236+ res.status(201).json({
237+ ...(req.body ?? {}),
238+ client_id: `client_${randomUUID()}`,
239+ client_id_issued_at: Math.floor(Date.now() / 1000),
240+ });
241+ });
242+
243+ app.get("/authorize", (req: any, res: any) => {
244+ const redirectUri = new URL(String(req.query.redirect_uri));
245+ redirectUri.searchParams.set("code", "demo-code");
246+ if (typeof req.query.state === "string") {
247+ redirectUri.searchParams.set("state", req.query.state);
248+ }
249+
250+ res.redirect(302, redirectUri.toString());
251+ });
252+
253+ app.post("/token", (_req: any, res: any) => {
254+ res.status(200).json({
255+ access_token: DEMO_OAUTH_ACCESS_TOKEN,
256+ token_type: "Bearer",
257+ expires_in: 3600,
258+ refresh_token: DEMO_OAUTH_REFRESH_TOKEN,
259+ scope: "openid offline_access",
260+ });
261+ });
262+
263+ app.post("/mcp", async (req: any, res: any) => {
264+ if (!requireBearerAuth(req, res)) {
265+ return;
266+ }
267+
268+ const sessionIdHeader = req.headers["mcp-session-id"];
269+ const sessionId =
270+ typeof sessionIdHeader === "string"
271+ ? sessionIdHeader
272+ : Array.isArray(sessionIdHeader)
273+ ? sessionIdHeader[0]
274+ : undefined;
275+
276+ try {
277+ let transport: StreamableHTTPServerTransport;
278+
279+ if (sessionId && transports[sessionId]) {
280+ transport = transports[sessionId];
281+ } else {
282+ transport = new StreamableHTTPServerTransport({
283+ sessionIdGenerator: () => randomUUID(),
284+ onsessioninitialized: (newSessionId) => {
285+ transports[newSessionId] = transport;
286+ },
287+ });
288+
289+ transport.onclose = () => {
290+ const closedSessionId = transport.sessionId;
291+ if (closedSessionId && transports[closedSessionId]) {
292+ delete transports[closedSessionId];
293+ }
294+ if (closedSessionId && servers[closedSessionId]) {
295+ void servers[closedSessionId].close().catch(() => undefined);
296+ delete servers[closedSessionId];
297+ }
298+ };
299+
300+ const server = createAuthorizedServer();
301+ await server.connect(transport);
302+ const newSessionId = transport.sessionId;
303+ if (newSessionId) {
304+ servers[newSessionId] = server;
305+ }
306+ }
307+
308+ await transport.handleRequest(req, res, req.body);
309+ } catch (error) {
310+ if (!res.headersSent) {
311+ res.status(500).json({
312+ jsonrpc: "2.0",
313+ error: {
314+ code: -32603,
315+ message: error instanceof Error ? error.message : "Internal server error",
316+ },
317+ id: null,
318+ });
319+ }
320+ }
321+ });
322+
323+ app.get("/mcp", async (req: any, res: any) => {
324+ if (!requireBearerAuth(req, res)) {
325+ return;
326+ }
327+
328+ const sessionIdHeader = req.headers["mcp-session-id"];
329+ const sessionId =
330+ typeof sessionIdHeader === "string"
331+ ? sessionIdHeader
332+ : Array.isArray(sessionIdHeader)
333+ ? sessionIdHeader[0]
334+ : undefined;
335+
336+ if (!sessionId || !transports[sessionId]) {
337+ res.status(400).send("Invalid or missing session ID");
338+ return;
339+ }
340+
341+ await transports[sessionId].handleRequest(req, res);
342+ });
343+
344+ app.delete("/mcp", async (req: any, res: any) => {
345+ if (!requireBearerAuth(req, res)) {
346+ return;
347+ }
348+
349+ const sessionIdHeader = req.headers["mcp-session-id"];
350+ const sessionId =
351+ typeof sessionIdHeader === "string"
352+ ? sessionIdHeader
353+ : Array.isArray(sessionIdHeader)
354+ ? sessionIdHeader[0]
355+ : undefined;
356+
357+ if (!sessionId || !transports[sessionId]) {
358+ res.status(400).send("Invalid or missing session ID");
359+ return;
360+ }
361+
362+ const transport = transports[sessionId];
363+ await transport.handleRequest(req, res, req.body);
364+ await transport.close();
365+ delete transports[sessionId];
366+
367+ if (servers[sessionId]) {
368+ await servers[sessionId].close().catch(() => undefined);
369+ delete servers[sessionId];
370+ }
371+ });
372+
373+ return {
374+ baseUrl,
375+ endpoint,
376+ close: async () => {
377+ for (const transport of Object.values(transports)) {
378+ await transport.close().catch(() => undefined);
379+ }
380+
381+ for (const server of Object.values(servers)) {
382+ await server.close().catch(() => undefined);
383+ }
384+
385+ await new Promise<void>((resolve, reject) => {
386+ listener.close((error) => (error ? reject(error) : resolve()));
387+ });
388+ },
389+ };
390+};
391+
392+const extractUrlInteractionUrl = (payloadJson: string): string => {
393+ const parsed = JSON.parse(payloadJson) as {
394+ elicitation?: {
395+ url?: string;
396+ };
397+ };
398+
399+ const url = parsed.elicitation?.url;
400+ if (typeof url !== "string" || url.trim().length === 0) {
401+ throw new Error("Missing URL elicitation in payloadJson");
402+ }
403+
404+ return url;
405+};
406+
407+const waitForExecutionCompletion = (input: {
408+ client: ControlPlaneClient;
409+ workspaceId: string;
410+ executionId: string;
411+}) =>
412+ Effect.gen(function* () {
413+ while (true) {
414+ const next = yield* input.client.executions.get({
415+ path: {
416+ workspaceId: input.workspaceId as never,
417+ executionId: input.executionId as never,
418+ },
419+ });
420+
421+ if (next.execution.status !== "waiting_for_interaction") {
422+ return next;
423+ }
424+
425+ yield* Effect.sleep("100 millis");
426+ }
427+ });
428+
125429 describe("local-executor-server", () => {
126430 it.scoped("serves the control-plane API and executes code", () =>
127431 Effect.gen(function* () {
@@ -296,6 +600,7 @@ describe("local-executor-server", () => { · 168 unmodified lines
296600 expect(resumed.execution.resultJson).toContain(`approved:${value}`);
297601 }
298602 }),
603+ 15_000,
299604 );
300605
301606 it.scoped("updates an existing demo MCP source instead of creating a duplicate", () =>
@@ -413,6 +718,89 @@ describe("local-executor-server", () => { · 111 unmodified lines
413718 }),
414719 );
415720
721+ it.scoped("adds an OAuth-protected MCP source via executor.sources.add and resumes after callback", () =>
722+ Effect.gen(function* () {
723+ const oauthServer = yield* Effect.acquireRelease(
724+ Effect.promise(() => startOAuthProtectedMcpServer()),
725+ (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
726+ );
727+
728+ const server = yield* makeLocalExecutorServer({
729+ port: 0,
730+ localDataDir: ":memory:",
731+ });
732+
733+ const bootstrapClient = yield* makeControlPlaneClient({
734+ baseUrl: server.baseUrl,
735+ });
736+ const installation = yield* bootstrapClient.local.installation({});
737+ const client = yield* makeControlPlaneClient({
738+ baseUrl: server.baseUrl,
739+ accountId: installation.accountId,
740+ });
741+
742+ const added = yield* client.executions.create({
743+ path: {
744+ workspaceId: installation.workspaceId,
745+ },
746+ payload: {
747+ code: `return await tools.executor.sources.add({ endpoint: ${JSON.stringify(oauthServer.endpoint)}, name: "Axiom", namespace: "axiom" });`,
748+ },
749+ });
750+
751+ expect(added.execution.status).toBe("waiting_for_interaction");
752+ expect(added.pendingInteraction).not.toBeNull();
753+ if (added.pendingInteraction === null) {
754+ throw new Error("Expected pending OAuth interaction");
755+ }
756+ expect(added.pendingInteraction.kind).toBe("url");
757+
758+ const authorizationUrl = extractUrlInteractionUrl(added.pendingInteraction.payloadJson);
759+ const callbackResponse = yield* Effect.promise(() =>
760+ fetch(authorizationUrl, {
761+ redirect: "follow",
762+ }),
763+ );
764+ expect(callbackResponse.ok).toBe(true);
765+ expect(yield* Effect.promise(() => callbackResponse.text())).toContain("Source connected:");
766+
767+ const completed = yield* waitForExecutionCompletion({
768+ client,
769+ workspaceId: installation.workspaceId,
770+ executionId: added.execution.id,
771+ });
772+
773+ expect(completed.execution.status).toBe("completed");
774+ expect(completed.pendingInteraction).toBeNull();
775+ expect(completed.execution.resultJson).toContain('"name":"Axiom"');
776+ expect(completed.execution.resultJson).toContain('"status":"connected"');
777+
778+ const sources = yield* client.sources.list({
779+ path: {
780+ workspaceId: installation.workspaceId,
781+ },
782+ });
783+
784+ expect(sources).toHaveLength(1);
785+ expect(sources[0]?.name).toBe("Axiom");
786+ expect(sources[0]?.status).toBe("connected");
787+ expect(sources[0]?.auth.kind).toBe("oauth2");
788+
789+ const toolCall = yield* client.executions.create({
790+ path: {
791+ workspaceId: installation.workspaceId,
792+ },
793+ payload: {
794+ code: "return await tools.axiom.whoami({});",
795+ },
796+ });
797+
798+ expect(toolCall.execution.status).toBe("completed");
799+ expect(toolCall.pendingInteraction).toBeNull();
800+ expect(toolCall.execution.resultJson).toContain("oauth-demo");
801+ }),
802+ );
803+
416804 it.scoped("returns a typed storage error when a configured MCP endpoint is invalid", () =>
417805 Effect.gen(function* () {
418806 const server = yield* makeLocalExecutorServer({
bun.lock+2−0
@@ -27,9 +27,11 @@ · 26 unmodified lines
2727 "devDependencies": {
2828 "@effect/vitest": "catalog:",
2929 "@executor-v3/mcp-elicitation-demo": "workspace:*",
30+ "@modelcontextprotocol/sdk": "^1.26.0",
3031 "@types/node": "catalog:",
3132 "bun-types": "catalog:",
3233 "vitest": "catalog:",
34+ "zod": "catalog:",
3335 },
3436 },
3537 "apps/mcp-elicitation-demo": {
packages/control-plane/drizzle/20260306094249_large_giant_girl/migration.sql+39−0
@@ -0,0 +1,39 @@
1+CREATE 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
10+CREATE 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
37+CREATE INDEX "secret_materials_updated_idx" ON "secret_materials" ("updated_at","id");--> statement-breakpoint
38+CREATE INDEX "source_auth_sessions_workspace_idx" ON "source_auth_sessions" ("workspace_id","updated_at","id");--> statement-breakpoint
39+CREATE UNIQUE INDEX "source_auth_sessions_state_idx" ON "source_auth_sessions" ("state");
packages/control-plane/drizzle/20260306094249_large_giant_girl/snapshot.json+2425−0
@@ -0,0 +1,2425 @@
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/src/api/local/api.ts+10−0
@@ -1,7 +1,9 @@
11 import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
2+import * as Schema from "effect/Schema";
23 import { LocalInstallationSchema } from "#schema";
34
45 import {
6+ ControlPlaneBadRequestError,
57 ControlPlaneNotFoundError,
68 ControlPlaneStorageError,
79 } from "../errors";
@@ -10,6 +12,14 @@ export class LocalApi extends HttpApiGroup.make("local") · 2 unmodified lines
1012 .add(
1113 HttpApiEndpoint.get("installation")`/local/installation`
1214 .addSuccess(LocalInstallationSchema)
15+ .addError(ControlPlaneBadRequestError)
16+ .addError(ControlPlaneNotFoundError)
17+ .addError(ControlPlaneStorageError),
18+ )
19+ .add(
20+ HttpApiEndpoint.get("oauthCallback")`/local/oauth/callback`
21+ .addSuccess(Schema.String)
22+ .addError(ControlPlaneBadRequestError)
1323 .addError(ControlPlaneNotFoundError)
1424 .addError(ControlPlaneStorageError),
1525 )
packages/control-plane/src/api/local/http.ts+24−7
@@ -1,4 +1,4 @@
1−import { HttpApiBuilder } from "@effect/platform";
1+import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
33
44 import { ControlPlaneApi } from "../api";
@@ -8,10 +8,27 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 3 unmodified lines
88 ControlPlaneApi,
99 "local",
1010 (handlers) =>
11− handlers.handle("installation", () =>
12− Effect.gen(function* () {
13− const service = yield* ControlPlaneService;
14− return yield* service.getLocalInstallation();
15− })
16− ),
11+ handlers
12+ .handle("installation", () =>
13+ Effect.gen(function* () {
14+ const service = yield* ControlPlaneService;
15+ return yield* service.getLocalInstallation();
16+ }),
17+ )
18+ .handle("oauthCallback", () =>
19+ Effect.gen(function* () {
20+ const service = yield* ControlPlaneService;
21+ const request = yield* HttpServerRequest.HttpServerRequest;
22+ const requestUrl = new URL(request.url, "http://127.0.0.1");
23+
24+ const source = yield* service.completeSourceAuthCallback({
25+ state: requestUrl.searchParams.get("state") ?? "",
26+ code: requestUrl.searchParams.get("code"),
27+ error: requestUrl.searchParams.get("error"),
28+ errorDescription: requestUrl.searchParams.get("error_description"),
29+ });
30+
31+ return `Source connected: ${source.id}. You can close this window.`;
32+ }),
33+ ),
1734 );
packages/control-plane/src/api/service.ts+10−0
@@ -112,6 +112,13 @@ export type RemoveSourceInput = { · 111 unmodified lines
112112 sourceId: SourceId;
113113 };
114114
115+export type CompleteSourceAuthCallbackInput = {
116+ state: string;
117+ code?: string | null;
118+ error?: string | null;
119+ errorDescription?: string | null;
120+};
121+
115122 export type CreatePolicyInput = {
116123 workspaceId: WorkspaceId;
117124 payload: CreatePolicyPayload;
@@ -255,6 +262,9 @@ export type ControlPlaneServiceShape = { · 137 unmodified lines
255262 LocalInstallation,
256263 ControlPlaneNotFoundError | ControlPlaneStorageError
257264 >;
265+ completeSourceAuthCallback: (
266+ input: CompleteSourceAuthCallbackInput,
267+ ) => Effect.Effect<Source, ControlPlaneBadRequestError | ControlPlaneNotFoundError | ControlPlaneStorageError>;
258268
259269 listPolicies: (
260270 workspaceId: WorkspaceId,
packages/control-plane/src/persistence/control-plane-rows.ts+4−0
@@ -8,7 +8,9 @@ import { · 7 unmodified lines
88 createOrganizationMembershipsRepo,
99 createOrganizationsRepo,
1010 createPoliciesRepo,
11+ createSecretMaterialsRepo,
1112 createSourceCredentialBindingsRepo,
13+ createSourceAuthSessionsRepo,
1214 createSourcesRepo,
1315 createWorkspacesRepo,
1416 } from "./repos";
@@ -28,6 +30,8 @@ export const createControlPlaneRows = ({ · 13 unmodified lines
2830 workspaces: createWorkspacesRepo(client, tables),
2931 sources: createSourcesRepo(client, tables),
3032 sourceCredentialBindings: createSourceCredentialBindingsRepo(client, tables),
33+ secretMaterials: createSecretMaterialsRepo(client, tables),
34+ sourceAuthSessions: createSourceAuthSessionsRepo(client, tables),
3135 policies: createPoliciesRepo(client, tables),
3236 localInstallations: createLocalInstallationsRepo(client, tables),
3337 executions: createExecutionsRepo(client, tables),
packages/control-plane/src/persistence/repos/index.ts+2−0
@@ -4,6 +4,8 @@ export { createOrganizationMembershipsRepo } from "./organization-memberships-re · 3 unmodified lines
44 export { createWorkspacesRepo } from "./workspaces-repo";
55 export { createSourcesRepo } from "./sources-repo";
66 export { createSourceCredentialBindingsRepo } from "./source-credential-bindings-repo";
7+export { createSecretMaterialsRepo } from "./secret-materials-repo";
8+export { createSourceAuthSessionsRepo } from "./source-auth-sessions-repo";
79 export { createPoliciesRepo } from "./policies-repo";
810 export { createLocalInstallationsRepo } from "./local-installations-repo";
911 export { createExecutionsRepo } from "./executions-repo";
packages/control-plane/src/persistence/repos/secret-materials-repo.ts+55−0
@@ -0,0 +1,55 @@
1+import {
2+ type SecretMaterial,
3+ SecretMaterialSchema,
4+} from "#schema";
5+import * as Option from "effect/Option";
6+import { Schema } from "effect";
7+import { eq } from "drizzle-orm";
8+
9+import type { DrizzleClient } from "../client";
10+import type { DrizzleTables } from "../schema";
11+import { firstOption, withoutCreatedAt } from "./shared";
12+
13+const decodeSecretMaterial = Schema.decodeUnknownSync(SecretMaterialSchema);
14+
15+export const createSecretMaterialsRepo = (
16+ client: DrizzleClient,
17+ tables: DrizzleTables,
18+) => ({
19+ getById: (id: SecretMaterial["id"]) =>
20+ client.use("rows.secret_materials.get_by_id", async (db) => {
21+ const rows = await db
22+ .select()
23+ .from(tables.secretMaterialsTable)
24+ .where(eq(tables.secretMaterialsTable.id, id))
25+ .limit(1);
26+
27+ const row = firstOption(rows);
28+ return Option.isSome(row)
29+ ? Option.some(decodeSecretMaterial(row.value))
30+ : Option.none<SecretMaterial>();
31+ }),
32+
33+ upsert: (material: SecretMaterial) =>
34+ client.use("rows.secret_materials.upsert", async (db) => {
35+ await db
36+ .insert(tables.secretMaterialsTable)
37+ .values(material)
38+ .onConflictDoUpdate({
39+ target: [tables.secretMaterialsTable.id],
40+ set: {
41+ ...withoutCreatedAt(material),
42+ },
43+ });
44+ }),
45+
46+ removeById: (id: SecretMaterial["id"]) =>
47+ client.use("rows.secret_materials.remove", async (db) => {
48+ const deleted = await db
49+ .delete(tables.secretMaterialsTable)
50+ .where(eq(tables.secretMaterialsTable.id, id))
51+ .returning();
52+
53+ return deleted.length > 0;
54+ }),
55+});
packages/control-plane/src/persistence/repos/source-auth-sessions-repo.ts+153−0
@@ -0,0 +1,153 @@
1+import {
2+ type SourceAuthSession,
3+ SourceAuthSessionSchema,
4+} from "#schema";
5+import * as Option from "effect/Option";
6+import { Schema } from "effect";
7+import { and, asc, eq } from "drizzle-orm";
8+
9+import type { DrizzleClient } from "../client";
10+import type { DrizzleTables } from "../schema";
11+import { firstOption, withoutCreatedAt } from "./shared";
12+
13+const decodeSourceAuthSession = Schema.decodeUnknownSync(SourceAuthSessionSchema);
14+
15+const toUpdateSet = (
16+ patch: Partial<Omit<SourceAuthSession, "id" | "workspaceId" | "sourceId" | "createdAt">>,
17+): Partial<DrizzleTables["sourceAuthSessionsTable"]["$inferInsert"]> => {
18+ const set: Partial<DrizzleTables["sourceAuthSessionsTable"]["$inferInsert"]> = {};
19+
20+ if (patch.executionId !== undefined) set.executionId = patch.executionId;
21+ if (patch.interactionId !== undefined) set.interactionId = patch.interactionId;
22+ if (patch.strategy !== undefined) set.strategy = patch.strategy;
23+ if (patch.status !== undefined) set.status = patch.status;
24+ if (patch.endpoint !== undefined) set.endpoint = patch.endpoint;
25+ if (patch.state !== undefined) set.state = patch.state;
26+ if (patch.redirectUri !== undefined) set.redirectUri = patch.redirectUri;
27+ if (patch.scope !== undefined) set.scope = patch.scope;
28+ if (patch.resourceMetadataUrl !== undefined) {
29+ set.resourceMetadataUrl = patch.resourceMetadataUrl;
30+ }
31+ if (patch.authorizationServerUrl !== undefined) {
32+ set.authorizationServerUrl = patch.authorizationServerUrl;
33+ }
34+ if (patch.resourceMetadataJson !== undefined) {
35+ set.resourceMetadataJson = patch.resourceMetadataJson;
36+ }
37+ if (patch.authorizationServerMetadataJson !== undefined) {
38+ set.authorizationServerMetadataJson = patch.authorizationServerMetadataJson;
39+ }
40+ if (patch.clientInformationJson !== undefined) {
41+ set.clientInformationJson = patch.clientInformationJson;
42+ }
43+ if (patch.codeVerifier !== undefined) set.codeVerifier = patch.codeVerifier;
44+ if (patch.authorizationUrl !== undefined) set.authorizationUrl = patch.authorizationUrl;
45+ if (patch.errorText !== undefined) set.errorText = patch.errorText;
46+ if (patch.completedAt !== undefined) set.completedAt = patch.completedAt;
47+ if (patch.updatedAt !== undefined) set.updatedAt = patch.updatedAt;
48+
49+ return set;
50+};
51+
52+export const createSourceAuthSessionsRepo = (
53+ client: DrizzleClient,
54+ tables: DrizzleTables,
55+) => ({
56+ listByWorkspaceId: (workspaceId: SourceAuthSession["workspaceId"]) =>
57+ client.use("rows.source_auth_sessions.list_by_workspace", async (db) => {
58+ const rows = await db
59+ .select()
60+ .from(tables.sourceAuthSessionsTable)
61+ .where(eq(tables.sourceAuthSessionsTable.workspaceId, workspaceId))
62+ .orderBy(asc(tables.sourceAuthSessionsTable.updatedAt), asc(tables.sourceAuthSessionsTable.id));
63+
64+ return rows.map((row) => decodeSourceAuthSession(row));
65+ }),
66+
67+ getById: (id: SourceAuthSession["id"]) =>
68+ client.use("rows.source_auth_sessions.get_by_id", async (db) => {
69+ const rows = await db
70+ .select()
71+ .from(tables.sourceAuthSessionsTable)
72+ .where(eq(tables.sourceAuthSessionsTable.id, id))
73+ .limit(1);
74+
75+ const row = firstOption(rows);
76+ return Option.isSome(row)
77+ ? Option.some(decodeSourceAuthSession(row.value))
78+ : Option.none<SourceAuthSession>();
79+ }),
80+
81+ getByState: (state: SourceAuthSession["state"]) =>
82+ client.use("rows.source_auth_sessions.get_by_state", async (db) => {
83+ const rows = await db
84+ .select()
85+ .from(tables.sourceAuthSessionsTable)
86+ .where(eq(tables.sourceAuthSessionsTable.state, state))
87+ .limit(1);
88+
89+ const row = firstOption(rows);
90+ return Option.isSome(row)
91+ ? Option.some(decodeSourceAuthSession(row.value))
92+ : Option.none<SourceAuthSession>();
93+ }),
94+
95+ getPendingByWorkspaceAndSourceId: (
96+ workspaceId: SourceAuthSession["workspaceId"],
97+ sourceId: SourceAuthSession["sourceId"],
98+ ) =>
99+ client.use("rows.source_auth_sessions.get_pending_by_workspace_and_source_id", async (db) => {
100+ const rows = await db
101+ .select()
102+ .from(tables.sourceAuthSessionsTable)
103+ .where(
104+ and(
105+ eq(tables.sourceAuthSessionsTable.workspaceId, workspaceId),
106+ eq(tables.sourceAuthSessionsTable.sourceId, sourceId),
107+ eq(tables.sourceAuthSessionsTable.status, "pending"),
108+ ),
109+ )
110+ .orderBy(asc(tables.sourceAuthSessionsTable.updatedAt), asc(tables.sourceAuthSessionsTable.id))
111+ .limit(1);
112+
113+ const row = firstOption(rows);
114+ return Option.isSome(row)
115+ ? Option.some(decodeSourceAuthSession(row.value))
116+ : Option.none<SourceAuthSession>();
117+ }),
118+
119+ insert: (session: SourceAuthSession) =>
120+ client.use("rows.source_auth_sessions.insert", async (db) => {
121+ await db.insert(tables.sourceAuthSessionsTable).values(session);
122+ }),
123+
124+ update: (
125+ id: SourceAuthSession["id"],
126+ patch: Partial<Omit<SourceAuthSession, "id" | "workspaceId" | "sourceId" | "createdAt">>,
127+ ) =>
128+ client.use("rows.source_auth_sessions.update", async (db) => {
129+ const rows = await db
130+ .update(tables.sourceAuthSessionsTable)
131+ .set(toUpdateSet(patch))
132+ .where(eq(tables.sourceAuthSessionsTable.id, id))
133+ .returning();
134+
135+ const row = firstOption(rows);
136+ return Option.isSome(row)
137+ ? Option.some(decodeSourceAuthSession(row.value))
138+ : Option.none<SourceAuthSession>();
139+ }),
140+
141+ upsert: (session: SourceAuthSession) =>
142+ client.use("rows.source_auth_sessions.upsert", async (db) => {
143+ await db
144+ .insert(tables.sourceAuthSessionsTable)
145+ .values(session)
146+ .onConflictDoUpdate({
147+ target: [tables.sourceAuthSessionsTable.id],
148+ set: {
149+ ...withoutCreatedAt(session),
150+ },
151+ });
152+ }),
153+});
packages/control-plane/src/persistence/schema.ts+66−0
@@ -17,6 +17,8 @@ export const tableNames = { · 16 unmodified lines
1717 workspaces: "workspaces",
1818 sources: "sources",
1919 sourceCredentialBindings: "source_credential_bindings",
20+ secretMaterials: "secret_materials",
21+ sourceAuthSessions: "source_auth_sessions",
2022 policies: "policies",
2123 localInstallations: "local_installations",
2224 executions: "executions",
@@ -181,6 +183,68 @@ export const sourceCredentialBindingsTable = pgTable( · 158 unmodified lines
181183 ],
182184 );
183185
186+export const secretMaterialsTable = pgTable(
187+ tableNames.secretMaterials,
188+ {
189+ id: text("id").notNull().primaryKey(),
190+ purpose: text("purpose").notNull(),
191+ value: text("value").notNull(),
192+ createdAt: bigint("created_at", { mode: "number" }).notNull(),
193+ updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
194+ },
195+ (table) => [
196+ index("secret_materials_updated_idx").on(table.updatedAt, table.id),
197+ check(
198+ "secret_materials_purpose_check",
199+ sql`${table.purpose} in ('auth_material', 'oauth_access_token', 'oauth_refresh_token', 'oauth_client_info')`,
200+ ),
201+ ],
202+);
203+
204+export const sourceAuthSessionsTable = pgTable(
205+ tableNames.sourceAuthSessions,
206+ {
207+ id: text("id").notNull().primaryKey(),
208+ workspaceId: text("workspace_id").notNull(),
209+ sourceId: text("source_id").notNull(),
210+ executionId: text("execution_id"),
211+ interactionId: text("interaction_id"),
212+ strategy: text("strategy").notNull(),
213+ status: text("status").notNull(),
214+ endpoint: text("endpoint").notNull(),
215+ state: text("state").notNull(),
216+ redirectUri: text("redirect_uri").notNull(),
217+ scope: text("scope"),
218+ resourceMetadataUrl: text("resource_metadata_url"),
219+ authorizationServerUrl: text("authorization_server_url"),
220+ resourceMetadataJson: text("resource_metadata_json"),
221+ authorizationServerMetadataJson: text("authorization_server_metadata_json"),
222+ clientInformationJson: text("client_information_json"),
223+ codeVerifier: text("code_verifier"),
224+ authorizationUrl: text("authorization_url"),
225+ errorText: text("error_text"),
226+ completedAt: bigint("completed_at", { mode: "number" }),
227+ createdAt: bigint("created_at", { mode: "number" }).notNull(),
228+ updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
229+ },
230+ (table) => [
231+ index("source_auth_sessions_workspace_idx").on(
232+ table.workspaceId,
233+ table.updatedAt,
234+ table.id,
235+ ),
236+ uniqueIndex("source_auth_sessions_state_idx").on(table.state),
237+ check(
238+ "source_auth_sessions_strategy_check",
239+ sql`${table.strategy} in ('oauth2_authorization_code')`,
240+ ),
241+ check(
242+ "source_auth_sessions_status_check",
243+ sql`${table.status} in ('pending', 'completed', 'failed', 'cancelled')`,
244+ ),
245+ ],
246+);
247+
184248 export const policiesTable = pgTable(
185249 tableNames.policies,
186250 {
@@ -286,6 +350,8 @@ export const drizzleSchema = { · 99 unmodified lines
286350 workspacesTable,
287351 sourcesTable,
288352 sourceCredentialBindingsTable,
353+ secretMaterialsTable,
354+ sourceAuthSessionsTable,
289355 policiesTable,
290356 localInstallationsTable,
291357 executionsTable,
packages/control-plane/src/runtime/execution-service.ts+32−1
@@ -4,6 +4,7 @@ import { · 3 unmodified lines
44 ControlPlaneNotFoundError,
55 ControlPlaneStorageError,
66 } from "#api";
7+import type { ToolInvoker } from "@executor-v3/codemode-core";
78 import {
89 ControlPlanePersistenceError,
910 type SqlControlPlaneRows,
@@ -109,6 +110,32 @@ const defaultExecutionResolver: ResolveExecutionEnvironment = () => · 99 unmodified lines
109110
110111 const defaultLiveExecutionManager = makeLiveExecutionManager();
111112
113+const withExecutionInvocationContext = (input: {
114+ executionId: Execution["id"];
115+ toolInvoker: ToolInvoker;
116+}): ToolInvoker => {
117+ let sequence = 0;
118+
119+ return {
120+ invoke: ({ path, args, context }) => {
121+ sequence += 1;
122+
123+ return input.toolInvoker.invoke({
124+ path,
125+ args,
126+ context: {
127+ ...context,
128+ runId: input.executionId,
129+ callId:
130+ typeof context?.callId === "string" && context.callId.length > 0
131+ ? context.callId
132+ : `call_${String(sequence)}`,
133+ },
134+ });
135+ },
136+ };
137+};
138+
112139 const fetchExecution = (
113140 rows: SqlControlPlaneRows,
114141 input: {
@@ -229,10 +256,14 @@ export const makeRuntimeExecutionsService = ( · 114 unmodified lines
229256 );
230257
231258 const nextState = yield* liveExecutionManager.registerStateWaiter(execution.id);
259+ const toolInvoker = withExecutionInvocationContext({
260+ executionId: execution.id,
261+ toolInvoker: environment.toolInvoker,
262+ });
232263
233264 yield* Effect.sync(() => {
234265 Effect.runFork(
235− environment.executor.execute(code, environment.toolInvoker).pipe(
266+ environment.executor.execute(code, toolInvoker).pipe(
236267 Effect.flatMap((outcome) => {
237268 const completedAt = Date.now();
238269 return mapStorageError(
packages/control-plane/src/runtime/executor-tools.ts+112−0
@@ -0,0 +1,112 @@
1+import {
2+ type ElicitationResponse,
3+ toTool,
4+ type ToolMap,
5+ type ToolPath,
6+} from "@executor-v3/codemode-core";
7+import {
8+ ExecutionIdSchema,
9+ ExecutionInteractionIdSchema,
10+ type Source,
11+ type WorkspaceId,
12+} from "#schema";
13+import * as Effect from "effect/Effect";
14+import * as Schema from "effect/Schema";
15+
16+import { type RuntimeSourceAuthService } from "./source-auth-service";
17+
18+const ExecutorSourcesAddInputSchema = Schema.standardSchemaV1(
19+ Schema.Struct({
20+ endpoint: Schema.String,
21+ name: Schema.optional(Schema.NullOr(Schema.String)),
22+ namespace: Schema.optional(Schema.NullOr(Schema.String)),
23+ }),
24+);
25+
26+const ExecutorSourcesAddOutputSchema = Schema.standardSchemaV1(Schema.Unknown);
27+
28+const toExecutionId = (value: unknown) => {
29+ if (typeof value !== "string" || value.trim().length === 0) {
30+ throw new Error("Missing execution run id for executor.sources.add");
31+ }
32+
33+ return ExecutionIdSchema.make(value);
34+};
35+
36+const asToolPath = (value: string): ToolPath => value as ToolPath;
37+
38+export const createExecutorToolMap = (input: {
39+ workspaceId: WorkspaceId;
40+ sourceAuthService: RuntimeSourceAuthService;
41+}): ToolMap => ({
42+ "executor.sources.add": toTool({
43+ tool: {
44+ description: "Add an MCP source to the current workspace, starting OAuth if the source requires it",
45+ inputSchema: ExecutorSourcesAddInputSchema,
46+ outputSchema: ExecutorSourcesAddOutputSchema,
47+ execute: async (
48+ args: {
49+ endpoint: string;
50+ name?: string | null;
51+ namespace?: string | null;
52+ },
53+ context,
54+ ): Promise<Source> => {
55+ const executionId = toExecutionId(context?.invocation?.runId);
56+ const interactionId = ExecutionInteractionIdSchema.make(
57+ `executor.sources.add:${crypto.randomUUID()}`,
58+ );
59+ const result = await Effect.runPromise(
60+ input.sourceAuthService.addExecutorMcpSource({
61+ workspaceId: input.workspaceId,
62+ executionId,
63+ interactionId,
64+ endpoint: args.endpoint,
65+ name: args.name ?? null,
66+ namespace: args.namespace ?? null,
67+ }),
68+ );
69+
70+ if (result.kind === "connected") {
71+ return result.source;
72+ }
73+
74+ if (!context?.onElicitation) {
75+ throw new Error("executor.sources.add requires an elicitation-capable host");
76+ }
77+
78+ const response: ElicitationResponse = await Effect.runPromise(
79+ context.onElicitation({
80+ interactionId,
81+ path: context.path ?? asToolPath("executor.sources.add"),
82+ sourceKey: context.sourceKey,
83+ args,
84+ metadata: context.metadata,
85+ context: context.invocation,
86+ elicitation: {
87+ mode: "url",
88+ message: `Open the provider sign-in page to connect ${result.source.name}`,
89+ url: result.authorizationUrl,
90+ elicitationId: result.sessionId,
91+ },
92+ }),
93+ );
94+
95+ if (response.action !== "accept") {
96+ throw new Error(`Source add was not completed for ${result.source.id}`);
97+ }
98+
99+ return await Effect.runPromise(
100+ input.sourceAuthService.getSourceById({
101+ workspaceId: input.workspaceId,
102+ sourceId: result.source.id,
103+ }),
104+ );
105+ },
106+ },
107+ metadata: {
108+ sourceKey: "executor",
109+ interaction: "auto",
110+ },
111+ }),
112+});
packages/control-plane/src/runtime/index.ts+15−1
@@ -27,8 +27,11 @@ import { · 26 unmodified lines
2727 } from "./live-execution";
2828 import {
2929 makeWorkspaceExecutionEnvironmentResolver,
30− type ResolveSecretMaterial,
3130 } from "./workspace-execution-environment";
31+import {
32+ makeRuntimeSourceAuthService,
33+ type ResolveSecretMaterial,
34+} from "./source-auth-service";
3235 import { makeRuntimeControlPlaneService } from "./services";
3336
3437 export {
@@ -40,6 +43,7 @@ export { · 5 unmodified lines
4043 export * from "./execution-state";
4144 export * from "./live-execution";
4245 export * from "./local-installation";
46+export * from "./source-auth-service";
4347 export * from "./workspace-execution-environment";
4448
4549 export type RuntimeControlPlaneInput = {
@@ -47,6 +51,7 @@ export type RuntimeControlPlaneInput = { · 1 unmodified line
4751 actorResolver?: ControlPlaneActorResolverShape;
4852 executionResolver?: ResolveExecutionEnvironment;
4953 resolveSecretMaterial?: ResolveSecretMaterial;
54+ getLocalServerBaseUrl?: () => string | undefined;
5055 };
5156
5257 export const makeRuntimeControlPlane = (
@@ -56,15 +61,22 @@ export const makeRuntimeControlPlane = ( · 3 unmodified lines
5661 actorResolver: ControlPlaneActorResolverShape;
5762 } => {
5863 const liveExecutionManager = makeLiveExecutionManager();
64+ const sourceAuthService = makeRuntimeSourceAuthService({
65+ rows: input.persistence.rows,
66+ liveExecutionManager,
67+ getLocalServerBaseUrl: input.getLocalServerBaseUrl,
68+ });
5969 const executionResolver =
6070 input.executionResolver
6171 ?? makeWorkspaceExecutionEnvironmentResolver({
6272 rows: input.persistence.rows,
6373 resolveSecretMaterial: input.resolveSecretMaterial,
74+ sourceAuthService,
6475 });
6576 const service = makeRuntimeControlPlaneService(input.persistence.rows, {
6677 executionResolver,
6778 liveExecutionManager,
79+ sourceAuthService,
6880 });
6981 const actorResolver = input.actorResolver ?? makeHeaderActorResolver(input.persistence.rows);
7082
@@ -86,6 +98,7 @@ export type CreateSqlControlPlaneRuntimeOptions = CreateSqlRuntimeOptions & { · 15 unmodified lines
8698 actorResolver?: ControlPlaneActorResolverShape;
8799 executionResolver?: ResolveExecutionEnvironment;
88100 resolveSecretMaterial?: ResolveSecretMaterial;
101+ getLocalServerBaseUrl?: () => string | undefined;
89102 };
90103
91104 export const makeSqlControlPlaneRuntime = (
@@ -98,6 +111,7 @@ export const makeSqlControlPlaneRuntime = ( · 6 unmodified lines
98111 actorResolver: options.actorResolver,
99112 executionResolver: options.executionResolver,
100113 resolveSecretMaterial: options.resolveSecretMaterial,
114+ getLocalServerBaseUrl: options.getLocalServerBaseUrl,
101115 });
102116 const localInstallation = yield* getOrProvisionLocalInstallation(persistence.rows).pipe(
103117 Effect.mapError((cause) =>
packages/control-plane/src/runtime/services.ts+28−2
@@ -25,6 +25,7 @@ import { type ResolveExecutionEnvironment } from "./execution-state"; · 24 unmodified lines
2525 import { type LiveExecutionManager } from "./live-execution";
2626 import { makeRuntimeExecutionsService } from "./execution-service";
2727 import { loadLocalInstallation } from "./local-installation";
28+import { type RuntimeSourceAuthService } from "./source-auth-service";
2829 import {
2930 createSourceFromPayload,
3031 projectSourceFromStorage,
@@ -982,7 +983,8 @@ export const makeRuntimePoliciesService = ( · 951 unmodified lines
982983
983984 export const makeRuntimeLocalService = (
984985 rows: SqlControlPlaneRows,
985−): Pick<ControlPlaneServiceShape, "getLocalInstallation"> => ({
986+ sourceAuthService?: RuntimeSourceAuthService,
987+): Pick<ControlPlaneServiceShape, "getLocalInstallation" | "completeSourceAuthCallback"> => ({
986988 getLocalInstallation: () =>
987989 Effect.gen(function* () {
988990 const installation = yield* loadLocalInstallation(rows).pipe(
@@ -1009,6 +1011,29 @@ export const makeRuntimeLocalService = ( · 20 unmodified lines
10091011
10101012 return installation;
10111013 }),
1014+
1015+ completeSourceAuthCallback: (input) =>
1016+ Effect.gen(function* () {
1017+ if (!sourceAuthService) {
1018+ return yield* Effect.fail(
1019+ badRequest(
1020+ "local.oauth.callback",
1021+ "Source auth callback handling is unavailable",
1022+ "Runtime source auth service is not configured",
1023+ ),
1024+ );
1025+ }
1026+
1027+ return yield* sourceAuthService.completeSourceAuthCallback(input).pipe(
1028+ Effect.mapError((error) =>
1029+ new ControlPlaneStorageError({
1030+ operation: "local.oauth.callback",
1031+ message: error instanceof Error ? error.message : String(error),
1032+ details: "Failed completing source auth callback",
1033+ }),
1034+ ),
1035+ );
1036+ }),
10121037 });
10131038
10141039 export const makeRuntimeControlPlaneService = (
@@ -1016,9 +1041,10 @@ export const makeRuntimeControlPlaneService = ( · 1 unmodified line
10161041 options: {
10171042 executionResolver?: ResolveExecutionEnvironment;
10181043 liveExecutionManager?: LiveExecutionManager;
1044+ sourceAuthService?: RuntimeSourceAuthService;
10191045 } = {},
10201046 ): ControlPlaneServiceShape => ({
1021− ...makeRuntimeLocalService(rows),
1047+ ...makeRuntimeLocalService(rows, options.sourceAuthService),
10221048 ...makeRuntimeOrganizationsService(rows),
10231049 ...makeRuntimeMembershipsService(rows),
10241050 ...makeRuntimeWorkspacesService(rows),
packages/control-plane/src/runtime/source-auth-service.ts+769−0
@@ -0,0 +1,769 @@
1+import {
2+ auth,
3+ type OAuthClientProvider,
4+ type OAuthDiscoveryState,
5+} from "@modelcontextprotocol/sdk/client/auth.js";
6+import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
7+import {
8+ createSdkMcpConnector,
9+ discoverMcpToolsFromConnector,
10+} from "@executor-v3/codemode-mcp";
11+import { type SqlControlPlaneRows } from "#persistence";
12+import {
13+ ExecutionIdSchema,
14+ SecretMaterialIdSchema,
15+ Source,
16+ SourceAuthSession,
17+ SourceAuthSessionIdSchema,
18+ SourceIdSchema,
19+ SourceSchema,
20+ type SecretRef,
21+ type WorkspaceId,
22+} from "#schema";
23+import * as Effect from "effect/Effect";
24+import * as Option from "effect/Option";
25+import * as Schema from "effect/Schema";
26+
27+import type { LiveExecutionManager } from "./live-execution";
28+import {
29+ createSourceFromPayload,
30+ projectSourceFromStorage,
31+ projectSourcesFromStorage,
32+ splitSourceForStorage,
33+ updateSourceFromPayload,
34+} from "./source-definitions";
35+
36+export const CONTROL_PLANE_SECRET_PROVIDER_ID = "control-plane";
37+const OAUTH_CALLBACK_PATH = "/v1/local/oauth/callback";
38+
39+const trimOrNull = (value: string | null | undefined): string | null => {
40+ if (value === null || value === undefined) {
41+ return null;
42+ }
43+
44+ const trimmed = value.trim();
45+ return trimmed.length > 0 ? trimmed : null;
46+};
47+
48+const defaultSourceNameFromEndpoint = (endpoint: string): string => {
49+ const url = new URL(endpoint);
50+ return url.hostname;
51+};
52+
53+const defaultNamespaceFromName = (name: string): string => {
54+ const normalized = name
55+ .trim()
56+ .toLowerCase()
57+ .replace(/[^a-z0-9]+/g, ".")
58+ .replace(/^\.+|\.+$/g, "");
59+
60+ return normalized.length > 0 ? normalized : "source";
61+};
62+
63+const serializeJson = (value: unknown): string | null => {
64+ if (value === undefined || value === null) {
65+ return null;
66+ }
67+
68+ return JSON.stringify(value);
69+};
70+
71+const decodeJson = <A>(input: {
72+ value: string | null;
73+ fallback: A;
74+}): A => {
75+ if (input.value === null) {
76+ return input.fallback;
77+ }
78+
79+ try {
80+ return JSON.parse(input.value) as A;
81+ } catch {
82+ return input.fallback;
83+ }
84+};
85+
86+const resolveRedirectUrl = (baseUrl: string): string =>
87+ new URL(OAUTH_CALLBACK_PATH, baseUrl).toString();
88+
89+const normalizeEndpoint = (endpoint: string): string => {
90+ const url = new URL(endpoint.trim());
91+ return url.toString();
92+};
93+
94+const loadSourcesInWorkspace = (rows: SqlControlPlaneRows, workspaceId: WorkspaceId) =>
95+ Effect.gen(function* () {
96+ const sourceRecords = yield* rows.sources.listByWorkspaceId(workspaceId);
97+ const credentialBindings = yield* rows.sourceCredentialBindings.listByWorkspaceId(workspaceId);
98+
99+ return yield* projectSourcesFromStorage({
100+ sourceRecords,
101+ credentialBindings,
102+ });
103+ });
104+
105+const loadSourceById = (rows: SqlControlPlaneRows, input: {
106+ workspaceId: WorkspaceId;
107+ sourceId: Source["id"];
108+}) =>
109+ Effect.gen(function* () {
110+ const sourceRecord = yield* rows.sources.getByWorkspaceAndId(
111+ input.workspaceId,
112+ input.sourceId,
113+ );
114+
115+ if (Option.isNone(sourceRecord)) {
116+ return yield* Effect.fail(
117+ new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
118+ );
119+ }
120+
121+ const credentialBinding = yield* rows.sourceCredentialBindings.getByWorkspaceAndSourceId(
122+ input.workspaceId,
123+ input.sourceId,
124+ );
125+
126+ return yield* projectSourceFromStorage({
127+ sourceRecord: sourceRecord.value,
128+ credentialBinding: Option.isSome(credentialBinding) ? credentialBinding.value : null,
129+ });
130+ });
131+
132+const persistSource = (rows: SqlControlPlaneRows, source: Source) =>
133+ Effect.gen(function* () {
134+ const { sourceRecord, credentialBinding } = splitSourceForStorage({ source });
135+ const existing = yield* rows.sources.getByWorkspaceAndId(source.workspaceId, source.id);
136+
137+ if (Option.isNone(existing)) {
138+ yield* rows.sources.insert(sourceRecord);
139+ } else {
140+ const { id: _id, workspaceId: _workspaceId, createdAt: _createdAt, ...patch } = sourceRecord;
141+ yield* rows.sources.update(source.workspaceId, source.id, patch);
142+ }
143+
144+ if (credentialBinding === null) {
145+ yield* rows.sourceCredentialBindings.removeByWorkspaceAndSourceId(
146+ source.workspaceId,
147+ source.id,
148+ );
149+ } else {
150+ yield* rows.sourceCredentialBindings.upsert(credentialBinding);
151+ }
152+
153+ return source;
154+ });
155+
156+const probeMcpSourceWithoutAuth = (source: Source) =>
157+ Effect.gen(function* () {
158+ if (source.kind !== "mcp") {
159+ return yield* Effect.fail(new Error(`Expected MCP source, received ${source.kind}`));
160+ }
161+
162+ const connector = yield* Effect.try({
163+ try: () =>
164+ createSdkMcpConnector({
165+ endpoint: source.endpoint,
166+ transport: source.transport ?? undefined,
167+ queryParams: source.queryParams ?? undefined,
168+ headers: source.headers ?? undefined,
169+ }),
170+ catch: (cause) =>
171+ cause instanceof Error ? cause : new Error(String(cause)),
172+ });
173+
174+ return yield* discoverMcpToolsFromConnector({
175+ connect: connector,
176+ namespace: source.namespace ?? defaultNamespaceFromName(source.name),
177+ sourceKey: source.id,
178+ });
179+ });
180+
181+const makeAddSourceClientMetadata = (redirectUrl: string) => ({
182+ redirect_uris: [redirectUrl],
183+ grant_types: ["authorization_code", "refresh_token"],
184+ response_types: ["code"],
185+ token_endpoint_auth_method: "none",
186+ client_name: "Executor Local",
187+});
188+
189+type PendingOAuthStart = {
190+ authorizationUrl: string;
191+ codeVerifier: string;
192+ resourceMetadataUrl: string | null;
193+ authorizationServerUrl: string | null;
194+ resourceMetadataJson: string | null;
195+ authorizationServerMetadataJson: string | null;
196+ clientInformationJson: string | null;
197+};
198+
199+const startOAuthAuthorization = (input: {
200+ endpoint: string;
201+ redirectUrl: string;
202+ state: string;
203+}): Effect.Effect<PendingOAuthStart, Error, never> =>
204+ Effect.tryPromise({
205+ try: async () => {
206+ const captured: {
207+ authorizationUrl?: URL;
208+ codeVerifier?: string;
209+ discoveryState?: OAuthDiscoveryState;
210+ clientInformation?: unknown;
211+ } = {};
212+
213+ const provider: OAuthClientProvider = {
214+ get redirectUrl() {
215+ return input.redirectUrl;
216+ },
217+ get clientMetadata() {
218+ return makeAddSourceClientMetadata(input.redirectUrl);
219+ },
220+ state: () => input.state,
221+ clientInformation: () => captured.clientInformation as any,
222+ saveClientInformation: (clientInformation) => {
223+ captured.clientInformation = clientInformation;
224+ },
225+ tokens: () => undefined,
226+ saveTokens: () => undefined,
227+ redirectToAuthorization: (authorizationUrl) => {
228+ captured.authorizationUrl = authorizationUrl;
229+ },
230+ saveCodeVerifier: (codeVerifier) => {
231+ captured.codeVerifier = codeVerifier;
232+ },
233+ codeVerifier: () => {
234+ if (!captured.codeVerifier) {
235+ throw new Error("OAuth code verifier was not captured");
236+ }
237+
238+ return captured.codeVerifier;
239+ },
240+ saveDiscoveryState: (state) => {
241+ captured.discoveryState = state;
242+ },
243+ discoveryState: () => captured.discoveryState,
244+ };
245+
246+ const result = await auth(provider, {
247+ serverUrl: input.endpoint,
248+ });
249+
250+ if (result !== "REDIRECT" || !captured.authorizationUrl || !captured.codeVerifier) {
251+ throw new Error("OAuth flow did not produce an authorization redirect");
252+ }
253+
254+ return {
255+ authorizationUrl: captured.authorizationUrl.toString(),
256+ codeVerifier: captured.codeVerifier,
257+ resourceMetadataUrl: captured.discoveryState?.resourceMetadataUrl ?? null,
258+ authorizationServerUrl: captured.discoveryState?.authorizationServerUrl ?? null,
259+ resourceMetadataJson: serializeJson(captured.discoveryState?.resourceMetadata),
260+ authorizationServerMetadataJson: serializeJson(
261+ captured.discoveryState?.authorizationServerMetadata,
262+ ),
263+ clientInformationJson: serializeJson(captured.clientInformation),
264+ } satisfies PendingOAuthStart;
265+ },
266+ catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
267+ });
268+
269+type ExchangedTokens = {
270+ tokens: OAuthTokens;
271+ clientInformationJson: string | null;
272+ resourceMetadataUrl: string | null;
273+ authorizationServerUrl: string | null;
274+ resourceMetadataJson: string | null;
275+ authorizationServerMetadataJson: string | null;
276+};
277+
278+const exchangeOAuthAuthorizationCode = (input: {
279+ session: SourceAuthSession;
280+ code: string;
281+}): Effect.Effect<ExchangedTokens, Error, never> =>
282+ Effect.tryPromise({
283+ try: async () => {
284+ const captured: {
285+ tokens?: OAuthTokens;
286+ discoveryState?: OAuthDiscoveryState;
287+ clientInformation?: unknown;
288+ } = {
289+ discoveryState: {
290+ authorizationServerUrl: input.session.authorizationServerUrl ?? new URL("/", input.session.endpoint).toString(),
291+ resourceMetadataUrl: input.session.resourceMetadataUrl ?? undefined,
292+ resourceMetadata: decodeJson({
293+ value: input.session.resourceMetadataJson,
294+ fallback: undefined,
295+ }),
296+ authorizationServerMetadata: decodeJson({
297+ value: input.session.authorizationServerMetadataJson,
298+ fallback: undefined,
299+ }),
300+ },
301+ clientInformation: decodeJson({
302+ value: input.session.clientInformationJson,
303+ fallback: undefined,
304+ }),
305+ };
306+
307+ const provider: OAuthClientProvider = {
308+ get redirectUrl() {
309+ return input.session.redirectUri;
310+ },
311+ get clientMetadata() {
312+ return makeAddSourceClientMetadata(input.session.redirectUri);
313+ },
314+ clientInformation: () => captured.clientInformation as any,
315+ saveClientInformation: (clientInformation) => {
316+ captured.clientInformation = clientInformation;
317+ },
318+ tokens: () => undefined,
319+ saveTokens: (tokens) => {
320+ captured.tokens = tokens;
321+ },
322+ redirectToAuthorization: () => {
323+ throw new Error("Unexpected redirect during OAuth callback completion");
324+ },
325+ saveCodeVerifier: () => undefined,
326+ codeVerifier: () => {
327+ if (!input.session.codeVerifier) {
328+ throw new Error("OAuth session is missing the PKCE code verifier");
329+ }
330+
331+ return input.session.codeVerifier;
332+ },
333+ saveDiscoveryState: (state) => {
334+ captured.discoveryState = state;
335+ },
336+ discoveryState: () => captured.discoveryState,
337+ };
338+
339+ const result = await auth(provider, {
340+ serverUrl: input.session.endpoint,
341+ authorizationCode: input.code,
342+ });
343+
344+ if (result !== "AUTHORIZED" || !captured.tokens) {
345+ throw new Error("OAuth callback did not complete authorization");
346+ }
347+
348+ return {
349+ tokens: captured.tokens,
350+ clientInformationJson: serializeJson(captured.clientInformation),
351+ resourceMetadataUrl: captured.discoveryState?.resourceMetadataUrl ?? null,
352+ authorizationServerUrl: captured.discoveryState?.authorizationServerUrl ?? null,
353+ resourceMetadataJson: serializeJson(captured.discoveryState?.resourceMetadata),
354+ authorizationServerMetadataJson: serializeJson(
355+ captured.discoveryState?.authorizationServerMetadata,
356+ ),
357+ } satisfies ExchangedTokens;
358+ },
359+ catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
360+ });
361+
362+const completeLiveInteraction = (input: {
363+ liveExecutionManager: LiveExecutionManager;
364+ session: SourceAuthSession;
365+ response: {
366+ action: "accept" | "cancel";
367+ reason?: string;
368+ };
369+}) =>
370+ Effect.gen(function* () {
371+ if (input.session.executionId === null) {
372+ return;
373+ }
374+
375+ yield* input.liveExecutionManager.resolveInteraction({
376+ executionId: input.session.executionId,
377+ response:
378+ input.response.action === "accept"
379+ ? { action: "accept" }
380+ : {
381+ action: "cancel",
382+ ...(input.response.reason
383+ ? {
384+ content: {
385+ reason: input.response.reason,
386+ },
387+ }
388+ : {}),
389+ },
390+ });
391+ });
392+
393+const updateSourceStatus = (rows: SqlControlPlaneRows, source: Source, input: {
394+ status: Source["status"];
395+ lastError?: string | null;
396+ auth?: Source["auth"];
397+}) =>
398+ persistSource(rows, {
399+ ...source,
400+ status: input.status,
401+ lastError: input.lastError ?? null,
402+ auth: input.auth ?? source.auth,
403+ updatedAt: Date.now(),
404+ });
405+
406+const upsertSecretMaterial = (rows: SqlControlPlaneRows, input: {
407+ purpose: "oauth_access_token" | "oauth_refresh_token";
408+ value: string;
409+}) =>
410+ Effect.gen(function* () {
411+ const now = Date.now();
412+ const materialId = SecretMaterialIdSchema.make(`sec_${crypto.randomUUID()}`);
413+ yield* rows.secretMaterials.upsert({
414+ id: materialId,
415+ purpose: input.purpose,
416+ value: input.value,
417+ createdAt: now,
418+ updatedAt: now,
419+ });
420+
421+ return materialId;
422+ });
423+
424+export type ExecutorSourceAddResult =
425+ | {
426+ kind: "connected";
427+ source: Source;
428+ }
429+ | {
430+ kind: "oauth_required";
431+ source: Source;
432+ sessionId: SourceAuthSession["id"];
433+ authorizationUrl: string;
434+ };
435+
436+export type RuntimeSourceAuthService = {
437+ getSourceById: (input: {
438+ workspaceId: WorkspaceId;
439+ sourceId: Source["id"];
440+ }) => Effect.Effect<Source, Error, never>;
441+ addExecutorMcpSource: (input: {
442+ workspaceId: WorkspaceId;
443+ executionId: SourceAuthSession["executionId"];
444+ interactionId: SourceAuthSession["interactionId"];
445+ endpoint: string;
446+ name?: string | null;
447+ namespace?: string | null;
448+ }) => Effect.Effect<ExecutorSourceAddResult, Error, never>;
449+ completeSourceAuthCallback: (input: {
450+ state: string;
451+ code?: string | null;
452+ error?: string | null;
453+ errorDescription?: string | null;
454+ }) => Effect.Effect<Source, Error, never>;
455+};
456+
457+export type ResolveSecretMaterial = (ref: SecretRef) => Effect.Effect<string, Error, never>;
458+
459+export const makeDbBackedSecretMaterialResolver = (input: {
460+ rows: SqlControlPlaneRows;
461+ fallback?: ResolveSecretMaterial;
462+}): ResolveSecretMaterial =>
463+ (ref) =>
464+ Effect.gen(function* () {
465+ if (ref.providerId === CONTROL_PLANE_SECRET_PROVIDER_ID) {
466+ const materialId = SecretMaterialIdSchema.make(ref.handle);
467+ const stored = yield* input.rows.secretMaterials.getById(materialId);
468+ if (Option.isNone(stored)) {
469+ return yield* Effect.fail(
470+ new Error(`Secret material not found: ${ref.handle}`),
471+ );
472+ }
473+
474+ return stored.value.value;
475+ }
476+
477+ if (input.fallback) {
478+ return yield* input.fallback(ref);
479+ }
480+
481+ return yield* Effect.fail(
482+ new Error(`Unsupported secret provider ${ref.providerId}`),
483+ );
484+ });
485+
486+export const makeRuntimeSourceAuthService = (input: {
487+ rows: SqlControlPlaneRows;
488+ liveExecutionManager: LiveExecutionManager;
489+ getLocalServerBaseUrl?: () => string | undefined;
490+}): RuntimeSourceAuthService => ({
491+ getSourceById: ({ workspaceId, sourceId }) =>
492+ loadSourceById(input.rows, {
493+ workspaceId,
494+ sourceId,
495+ }),
496+
497+ addExecutorMcpSource: ({
498+ workspaceId,
499+ executionId,
500+ interactionId,
501+ endpoint,
502+ name,
503+ namespace,
504+ }) =>
505+ Effect.gen(function* () {
506+ const normalizedEndpoint = normalizeEndpoint(endpoint);
507+ const existingSources = yield* loadSourcesInWorkspace(input.rows, workspaceId);
508+ const existing = existingSources.find(
509+ (source) => source.kind === "mcp" && normalizeEndpoint(source.endpoint) === normalizedEndpoint,
510+ );
511+
512+ const chosenName =
513+ trimOrNull(name)
514+ ?? existing?.name
515+ ?? defaultSourceNameFromEndpoint(normalizedEndpoint);
516+ const chosenNamespace =
517+ trimOrNull(namespace)
518+ ?? existing?.namespace
519+ ?? defaultNamespaceFromName(chosenName);
520+ const now = Date.now();
521+
522+ const draftSource = existing
523+ ? yield* updateSourceFromPayload({
524+ source: existing,
525+ payload: {
526+ name: chosenName,
527+ endpoint: normalizedEndpoint,
528+ namespace: chosenNamespace,
529+ kind: "mcp",
530+ status: "probing",
531+ enabled: true,
532+ transport: existing.transport ?? "auto",
533+ auth: { kind: "none" },
534+ lastError: null,
535+ },
536+ now,
537+ })
538+ : yield* createSourceFromPayload({
539+ workspaceId,
540+ sourceId: SourceIdSchema.make(`src_${crypto.randomUUID()}`),
541+ payload: {
542+ name: chosenName,
543+ kind: "mcp",
544+ endpoint: normalizedEndpoint,
545+ namespace: chosenNamespace,
546+ status: "probing",
547+ enabled: true,
548+ transport: "auto",
549+ auth: { kind: "none" },
550+ },
551+ now,
552+ });
553+
554+ const persistedDraft = yield* persistSource(input.rows, draftSource);
555+
556+ const discovered = yield* Effect.either(
557+ probeMcpSourceWithoutAuth(persistedDraft),
558+ );
559+
560+ if (discovered._tag === "Right") {
561+ const connected = yield* updateSourceStatus(input.rows, persistedDraft, {
562+ status: "connected",
563+ lastError: null,
564+ auth: { kind: "none" },
565+ });
566+
567+ return {
568+ kind: "connected",
569+ source: connected,
570+ } satisfies ExecutorSourceAddResult;
571+ }
572+
573+ const localServerBaseUrl = input.getLocalServerBaseUrl?.();
574+ if (!localServerBaseUrl) {
575+ return yield* Effect.fail(
576+ new Error("Local executor server base URL is unavailable for OAuth callback handling"),
577+ );
578+ }
579+
580+ const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`);
581+ const state = crypto.randomUUID();
582+ const redirectUrl = resolveRedirectUrl(localServerBaseUrl);
583+ const oauthStart = yield* startOAuthAuthorization({
584+ endpoint: normalizedEndpoint,
585+ redirectUrl,
586+ state,
587+ });
588+
589+ const authRequiredSource = yield* updateSourceStatus(input.rows, persistedDraft, {
590+ status: "auth_required",
591+ lastError: null,
592+ });
593+
594+ const sessionNow = Date.now();
595+ yield* input.rows.sourceAuthSessions.upsert({
596+ id: sessionId,
597+ workspaceId,
598+ sourceId: authRequiredSource.id,
599+ executionId,
600+ interactionId,
601+ strategy: "oauth2_authorization_code",
602+ status: "pending",
603+ endpoint: normalizedEndpoint,
604+ state,
605+ redirectUri: redirectUrl,
606+ scope: null,
607+ resourceMetadataUrl: oauthStart.resourceMetadataUrl,
608+ authorizationServerUrl: oauthStart.authorizationServerUrl,
609+ resourceMetadataJson: oauthStart.resourceMetadataJson,
610+ authorizationServerMetadataJson: oauthStart.authorizationServerMetadataJson,
611+ clientInformationJson: oauthStart.clientInformationJson,
612+ codeVerifier: oauthStart.codeVerifier,
613+ authorizationUrl: oauthStart.authorizationUrl,
614+ errorText: null,
615+ completedAt: null,
616+ createdAt: sessionNow,
617+ updatedAt: sessionNow,
618+ });
619+
620+ return {
621+ kind: "oauth_required",
622+ source: authRequiredSource,
623+ sessionId,
624+ authorizationUrl: oauthStart.authorizationUrl,
625+ } satisfies ExecutorSourceAddResult;
626+ }),
627+
628+ completeSourceAuthCallback: ({ state, code, error, errorDescription }) =>
629+ Effect.gen(function* () {
630+ const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state);
631+ if (Option.isNone(sessionOption)) {
632+ return yield* Effect.fail(new Error(`Source auth session not found for state ${state}`));
633+ }
634+
635+ const session = sessionOption.value;
636+ const source = yield* loadSourceById(input.rows, {
637+ workspaceId: session.workspaceId,
638+ sourceId: session.sourceId,
639+ });
640+
641+ if (session.status === "completed") {
642+ return source;
643+ }
644+
645+ if (session.status !== "pending") {
646+ return yield* Effect.fail(
647+ new Error(`Source auth session ${session.id} is not pending`),
648+ );
649+ }
650+
651+ if (trimOrNull(error) !== null) {
652+ const reason = trimOrNull(errorDescription) ?? trimOrNull(error) ?? "OAuth authorization failed";
653+ const failedAt = Date.now();
654+
655+ yield* input.rows.sourceAuthSessions.update(session.id, {
656+ status: "failed",
657+ errorText: reason,
658+ completedAt: failedAt,
659+ updatedAt: failedAt,
660+ });
661+ yield* updateSourceStatus(input.rows, source, {
662+ status: "error",
663+ lastError: reason,
664+ });
665+ yield* completeLiveInteraction({
666+ liveExecutionManager: input.liveExecutionManager,
667+ session,
668+ response: {
669+ action: "cancel",
670+ reason,
671+ },
672+ });
673+
674+ return yield* Effect.fail(new Error(reason));
675+ }
676+
677+ const authorizationCode = trimOrNull(code);
678+ if (authorizationCode === null) {
679+ return yield* Effect.fail(new Error("Missing OAuth authorization code"));
680+ }
681+
682+ const exchanged = yield* exchangeOAuthAuthorizationCode({
683+ session,
684+ code: authorizationCode,
685+ });
686+
687+ const accessTokenId = yield* upsertSecretMaterial(input.rows, {
688+ purpose: "oauth_access_token",
689+ value: exchanged.tokens.access_token,
690+ });
691+ const refreshTokenId = exchanged.tokens.refresh_token
692+ ? yield* upsertSecretMaterial(input.rows, {
693+ purpose: "oauth_refresh_token",
694+ value: exchanged.tokens.refresh_token,
695+ })
696+ : null;
697+
698+ const now = Date.now();
699+ yield* input.rows.sourceCredentialBindings.upsert({
700+ workspaceId: session.workspaceId,
701+ sourceId: session.sourceId,
702+ tokenProviderId: CONTROL_PLANE_SECRET_PROVIDER_ID,
703+ tokenHandle: accessTokenId,
704+ refreshTokenProviderId:
705+ refreshTokenId === null ? null : CONTROL_PLANE_SECRET_PROVIDER_ID,
706+ refreshTokenHandle: refreshTokenId,
707+ createdAt: now,
708+ updatedAt: now,
709+ });
710+
711+ const connectedSource = yield* updateSourceStatus(input.rows, source, {
712+ status: "connected",
713+ lastError: null,
714+ auth: {
715+ kind: "oauth2",
716+ headerName: "Authorization",
717+ prefix: "Bearer ",
718+ accessToken: {
719+ providerId: CONTROL_PLANE_SECRET_PROVIDER_ID,
720+ handle: accessTokenId,
721+ },
722+ refreshToken:
723+ refreshTokenId === null
724+ ? null
725+ : {
726+ providerId: CONTROL_PLANE_SECRET_PROVIDER_ID,
727+ handle: refreshTokenId,
728+ },
729+ },
730+ });
731+
732+ yield* input.rows.sourceAuthSessions.update(session.id, {
733+ status: "completed",
734+ errorText: null,
735+ completedAt: now,
736+ updatedAt: now,
737+ resourceMetadataUrl: exchanged.resourceMetadataUrl,
738+ authorizationServerUrl: exchanged.authorizationServerUrl,
739+ resourceMetadataJson: exchanged.resourceMetadataJson,
740+ authorizationServerMetadataJson: exchanged.authorizationServerMetadataJson,
741+ clientInformationJson: exchanged.clientInformationJson,
742+ });
743+
744+ yield* completeLiveInteraction({
745+ liveExecutionManager: input.liveExecutionManager,
746+ session,
747+ response: {
748+ action: "accept",
749+ },
750+ });
751+
752+ return connectedSource;
753+ }),
754+});
755+
756+export const ExecutorAddSourceResultSchema = Schema.Union(
757+ Schema.Struct({
758+ kind: Schema.Literal("connected"),
759+ source: SourceSchema,
760+ }),
761+ Schema.Struct({
762+ kind: Schema.Literal("oauth_required"),
763+ source: SourceSchema,
764+ sessionId: SourceAuthSessionIdSchema,
765+ authorizationUrl: Schema.String,
766+ }),
767+);
768+
769+export type ExecutorAddSourceResult = typeof ExecutorAddSourceResultSchema.Type;
packages/control-plane/src/runtime/workspace-execution-environment.ts+22−8
@@ -16,7 +16,6 @@ import { · 15 unmodified lines
1616 import { makeInProcessExecutor } from "@executor-v3/runtime-local-inproc";
1717 import { type SqlControlPlaneRows } from "#persistence";
1818 import type {
19− SecretRef,
2019 Source,
2120 } from "#schema";
2221 import * as Effect from "effect/Effect";
@@ -25,11 +24,13 @@ import type { · 2 unmodified lines
2524 ExecutionEnvironment,
2625 ResolveExecutionEnvironment,
2726 } from "./execution-state";
27+import { createExecutorToolMap } from "./executor-tools";
2828 import { projectSourcesFromStorage } from "./source-definitions";
29−
30−export type ResolveSecretMaterial = (
31− ref: SecretRef,
32−) => Effect.Effect<string, Error, never>;
29+import {
30+ makeDbBackedSecretMaterialResolver,
31+ type ResolveSecretMaterial,
32+ type RuntimeSourceAuthService,
33+} from "./source-auth-service";
3334
3435 export type ResolvedSourceAuthMaterial = {
3536 headers: Readonly<Record<string, string>>;
@@ -204,8 +205,14 @@ const loadSourceTools = (input: { · 168 unmodified lines
204205 export const makeWorkspaceExecutionEnvironmentResolver = (input: {
205206 rows: SqlControlPlaneRows;
206207 resolveSecretMaterial?: ResolveSecretMaterial;
208+ sourceAuthService: RuntimeSourceAuthService;
207209 }): ResolveExecutionEnvironment => {
208− const resolveSecretMaterial = input.resolveSecretMaterial ?? makeEnvSecretMaterialResolver();
210+ const resolveSecretMaterial =
211+ input.resolveSecretMaterial
212+ ?? makeDbBackedSecretMaterialResolver({
213+ rows: input.rows,
214+ fallback: makeEnvSecretMaterialResolver(),
215+ });
209216
210217 return ({ workspaceId, onElicitation }) =>
211218 Effect.gen(function* () {
@@ -226,7 +233,9 @@ export const makeWorkspaceExecutionEnvironmentResolver = (input: { · 14 unmodified lines
226233 sourceRecords,
227234 credentialBindings,
228235 });
229− const enabledSources = sources.filter((source) => source.enabled);
236+ const enabledSources = sources.filter(
237+ (source) => source.enabled && source.status === "connected",
238+ );
230239
231240 const discoveredToolMaps = yield* Effect.forEach(
232241 enabledSources,
@@ -244,9 +253,14 @@ export const makeWorkspaceExecutionEnvironmentResolver = (input: { · 11 unmodified lines
244253 { concurrency: "unbounded" },
245254 );
246255
256+ const executorTools = createExecutorToolMap({
257+ workspaceId,
258+ sourceAuthService: input.sourceAuthService,
259+ });
260+
247261 const sourceTools = yield* Effect.try({
248262 try: () =>
249− mergeToolMaps(discoveredToolMaps, {
263+ mergeToolMaps([...discoveredToolMaps, executorTools], {
250264 conflictMode: "throw",
251265 }),
252266 catch: (cause) =>
packages/control-plane/src/schema/ids.ts+8−0
@@ -7,6 +7,12 @@ export const OrganizationMemberIdSchema = Schema.String.pipe( · 6 unmodified lines
77 );
88 export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"));
99 export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId"));
10+export const SourceAuthSessionIdSchema = Schema.String.pipe(
11+ Schema.brand("SourceAuthSessionId"),
12+);
13+export const SecretMaterialIdSchema = Schema.String.pipe(
14+ Schema.brand("SecretMaterialId"),
15+);
1016 export const PolicyIdSchema = Schema.String.pipe(Schema.brand("PolicyId"));
1117 export const InstallationIdSchema = Schema.String.pipe(Schema.brand("InstallationId"));
1218 export const ExecutionIdSchema = Schema.String.pipe(Schema.brand("ExecutionId"));
@@ -19,6 +25,8 @@ export type OrganizationId = typeof OrganizationIdSchema.Type; · 6 unmodified lines
1925 export type OrganizationMemberId = typeof OrganizationMemberIdSchema.Type;
2026 export type WorkspaceId = typeof WorkspaceIdSchema.Type;
2127 export type SourceId = typeof SourceIdSchema.Type;
28+export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type;
29+export type SecretMaterialId = typeof SecretMaterialIdSchema.Type;
2230 export type PolicyId = typeof PolicyIdSchema.Type;
2331 export type InstallationId = typeof InstallationIdSchema.Type;
2432 export type ExecutionId = typeof ExecutionIdSchema.Type;
packages/control-plane/src/schema/index.ts+2−0
@@ -5,6 +5,8 @@ export * from "./ids"; · 4 unmodified lines
55 export * from "./models/auth";
66 export * from "./models/workspace";
77 export * from "./models/source";
8+export * from "./models/secret-material";
9+export * from "./models/source-auth-session";
810 export * from "./models/policy";
911 export * from "./models/local-installation";
1012 export * from "./models/execution";
packages/control-plane/src/schema/models/secret-material.ts+28−0
@@ -0,0 +1,28 @@
1+import { createSelectSchema } from "drizzle-orm/effect-schema";
2+import { Schema } from "effect";
3+
4+import { secretMaterialsTable } from "../../persistence/schema";
5+import { TimestampMsSchema } from "../common";
6+import { SecretMaterialIdSchema } from "../ids";
7+
8+export const SecretMaterialPurposeSchema = Schema.Literal(
9+ "auth_material",
10+ "oauth_access_token",
11+ "oauth_refresh_token",
12+ "oauth_client_info",
13+);
14+
15+const secretMaterialSchemaOverrides = {
16+ id: SecretMaterialIdSchema,
17+ purpose: SecretMaterialPurposeSchema,
18+ createdAt: TimestampMsSchema,
19+ updatedAt: TimestampMsSchema,
20+} as const;
21+
22+export const SecretMaterialSchema = createSelectSchema(
23+ secretMaterialsTable,
24+ secretMaterialSchemaOverrides,
25+);
26+
27+export type SecretMaterialPurpose = typeof SecretMaterialPurposeSchema.Type;
28+export type SecretMaterial = typeof SecretMaterialSchema.Type;
packages/control-plane/src/schema/models/source-auth-session.ts+45−0
@@ -0,0 +1,45 @@
1+import { createSelectSchema } from "drizzle-orm/effect-schema";
2+import { Schema } from "effect";
3+
4+import { sourceAuthSessionsTable } from "../../persistence/schema";
5+import { TimestampMsSchema } from "../common";
6+import {
7+ ExecutionIdSchema,
8+ ExecutionInteractionIdSchema,
9+ SourceAuthSessionIdSchema,
10+ SourceIdSchema,
11+ WorkspaceIdSchema,
12+} from "../ids";
13+
14+export const SourceAuthSessionStrategySchema = Schema.Literal(
15+ "oauth2_authorization_code",
16+);
17+
18+export const SourceAuthSessionStatusSchema = Schema.Literal(
19+ "pending",
20+ "completed",
21+ "failed",
22+ "cancelled",
23+);
24+
25+const sourceAuthSessionSchemaOverrides = {
26+ id: SourceAuthSessionIdSchema,
27+ workspaceId: WorkspaceIdSchema,
28+ sourceId: SourceIdSchema,
29+ executionId: Schema.NullOr(ExecutionIdSchema),
30+ interactionId: Schema.NullOr(ExecutionInteractionIdSchema),
31+ strategy: SourceAuthSessionStrategySchema,
32+ status: SourceAuthSessionStatusSchema,
33+ completedAt: Schema.NullOr(TimestampMsSchema),
34+ createdAt: TimestampMsSchema,
35+ updatedAt: TimestampMsSchema,
36+} as const;
37+
38+export const SourceAuthSessionSchema = createSelectSchema(
39+ sourceAuthSessionsTable,
40+ sourceAuthSessionSchemaOverrides,
41+);
42+
43+export type SourceAuthSessionStrategy = typeof SourceAuthSessionStrategySchema.Type;
44+export type SourceAuthSessionStatus = typeof SourceAuthSessionStatusSchema.Type;
45+export type SourceAuthSession = typeof SourceAuthSessionSchema.Type;