Unified · Split
| @@ -1,25 +1,9 @@ | |||
| 1 | { | 1 | { |
| 2 | "sources": { | 2 | "policies": { |
| 3 | "vercel": { | 3 | "demo.*": { |
| 4 | "name": "Vercel API", | 4 | "match": "demo.*", |
| 5 | "connection": { | 5 | "action": "allow", |
| 6 | "endpoint": "https://api.vercel.com/" | 6 | "approval": "auto" |
| 7 | }, | ||
| 8 | "kind": "openapi", | ||
| 9 | "binding": { | ||
| 10 | "specUrl": "https://openapi.vercel.sh/", | ||
| 11 | "defaultHeaders": null | ||
| 12 | } | ||
| 13 | }, | ||
| 14 | "linear": { | ||
| 15 | "kind": "graphql", | ||
| 16 | "name": "Linear GraphQL", | ||
| 17 | "connection": { | ||
| 18 | "endpoint": "https://api.linear.app/graphql" | ||
| 19 | }, | ||
| 20 | "binding": { | ||
| 21 | "defaultHeaders": null | ||
| 22 | } | ||
| 23 | } | 7 | } |
| 24 | } | 8 | } |
| 25 | } | 9 | } |
| @@ -88,7 +88,7 @@ Responsibilities: · 87 unmodified lines | |||
| 88 | 88 | ||
| 89 | This is an important architectural choice: the API and UI are served by the same local process, so the product behaves like one install rather than a pile of separate services. | 89 | This is an important architectural choice: the API and UI are served by the same local process, so the product behaves like one install rather than a pile of separate services. |
| 90 | 90 | ||
| 91 | ### `packages/platform/control-plane`: product core | 91 | ### `packages/platform/sdk`: product core |
| 92 | 92 | ||
| 93 | This is the center of the system. | 93 | This is the center of the system. |
| 94 | 94 | ||
| @@ -408,4 +408,4 @@ A few practical boundaries are worth calling out: · 313 unmodified lines | |||
| 408 | - `README.md` for the product view and usage guidance | 408 | - `README.md` for the product view and usage guidance |
| 409 | - `apps/executor/src/cli/main.ts` for the CLI surface | 409 | - `apps/executor/src/cli/main.ts` for the CLI surface |
| 410 | - `packages/platform/server/src/index.ts` for how the local server is assembled | 410 | - `packages/platform/server/src/index.ts` for how the local server is assembled |
| 411 | - `packages/platform/control-plane/src/runtime/` for the core runtime flows | 411 | - `packages/platform/sdk/src/runtime/` for the core runtime flows |
| @@ -329,7 +329,9 @@ If you are exploring the repo, these are the directories that matter most: · 328 unmodified lines | |||
| 329 | - `apps/executor`: packaged CLI entrypoint and daemon lifecycle commands | 329 | - `apps/executor`: packaged CLI entrypoint and daemon lifecycle commands |
| 330 | - `apps/web`: local React web UI | 330 | - `apps/web`: local React web UI |
| 331 | - `packages/platform/server`: local HTTP server that serves API, MCP, and UI | 331 | - `packages/platform/server`: local HTTP server that serves API, MCP, and UI |
| 332 | - `packages/platform/control-plane`: source management, secrets, persistence, execution, and inspection | 332 | - `packages/platform/sdk`: source management, secrets, persistence, execution, and inspection |
| 333 | - `packages/platform/api`: thin HTTP adapter over the platform SDK | ||
| 334 | - `packages/platform/internal`: thin internal tool adapter over the platform SDK | ||
| 333 | - `packages/kernel/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution | 335 | - `packages/kernel/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution |
| 334 | - `packages/kernel/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution | 336 | - `packages/kernel/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution |
| 335 | - `packages/kernel/runtime-ses`: optional SES sandbox runtime for TypeScript execution | 337 | - `packages/kernel/runtime-ses`: optional SES sandbox runtime for TypeScript execution |
| @@ -349,7 +351,7 @@ If you are exploring the repo, these are the directories that matter most: · 13 unmodified lines | |||
| 349 | - One-time npm setup: either configure npm trusted publishing for `RhysSullivan/executor` with the workflow file `.github/workflows/publish-executor-package.yml`, or add a GitHub Actions secret named `NPM_TOKEN` that can publish the `executor` package. | 351 | - One-time npm setup: either configure npm trusted publishing for `RhysSullivan/executor` with the workflow file `.github/workflows/publish-executor-package.yml`, or add a GitHub Actions secret named `NPM_TOKEN` that can publish the `executor` package. |
| 350 | - Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`. | 352 | - Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`. |
| 351 | - Beta releases use a prerelease semver like `1.3.0-beta.1` and publish to npm under `beta`. | 353 | - Beta releases use a prerelease semver like `1.3.0-beta.1` and publish to npm under `beta`. |
| 352 | - When a release should become an upgrade test fixture, capture a real workspace snapshot with `bun run fixture:release:capture -- ...` and commit it under [`packages/platform/control-plane/src/runtime/__fixtures__`](./packages/platform/control-plane/src/runtime/__fixtures__/README.md). | 354 | - When a release should become an upgrade test fixture, capture a real workspace snapshot with `bun run fixture:release:capture -- ...` and commit it under [`packages/platform/sdk/src/runtime/__fixtures__`](./packages/platform/sdk/src/runtime/__fixtures__/README.md). |
| 353 | 355 | ||
| 354 | ## Project status | 356 | ## Project status |
| 355 | 357 | ||
| @@ -41,7 +41,7 @@ At a high level, the CLI: · 40 unmodified lines | |||
| 41 | 41 | ||
| 42 | 1. ensures the local daemon is available | 42 | 1. ensures the local daemon is available |
| 43 | 2. calls the local HTTP API exposed by `packages/platform/server` | 43 | 2. calls the local HTTP API exposed by `packages/platform/server` |
| 44 | 3. hands off real work to `packages/platform/control-plane` | 44 | 3. hands off real work to `packages/platform/sdk` and `packages/platform/api` |
| 45 | 45 | ||
| 46 | That makes the CLI a stable shell over a shared local runtime rather than a second product core. | 46 | That makes the CLI a stable shell over a shared local runtime rather than a second product core. |
| 47 | 47 | ||
| @@ -41,7 +41,7 @@ Responsibilities: · 40 unmodified lines | |||
| 41 | - mount the local MCP server at `/mcp` | 41 | - mount the local MCP server at `/mcp` |
| 42 | - serve the web UI | 42 | - serve the web UI |
| 43 | 43 | ||
| 44 | ### `packages/platform/control-plane` | 44 | ### `packages/platform/sdk` |
| 45 | 45 | ||
| 46 | The product core. | 46 | The product core. |
| 47 | 47 | ||
| @@ -37,8 +37,10 @@ · 36 unmodified lines | |||
| 37 | "@effect/platform": "catalog:", | 37 | "@effect/platform": "catalog:", |
| 38 | "@effect/platform-node": "catalog:", | 38 | "@effect/platform-node": "catalog:", |
| 39 | "@executor/codemode-core": "workspace:*", | 39 | "@executor/codemode-core": "workspace:*", |
| 40 | "@executor/control-plane": "workspace:*", | ||
| 41 | "@executor/executor-mcp": "workspace:*", | 40 | "@executor/executor-mcp": "workspace:*", |
| 41 | "@executor/platform-api": "workspace:*", | ||
| 42 | "@executor/platform-internal": "workspace:*", | ||
| 43 | "@executor/platform-sdk": "workspace:*", | ||
| 42 | "@executor/server": "workspace:*", | 44 | "@executor/server": "workspace:*", |
| 43 | "effect": "catalog:" | 45 | "effect": "catalog:" |
| 44 | }, | 46 | }, |
| @@ -1,7 +1,5 @@ | |||
| 1 | import { | 1 | import type { ControlPlaneClient } from "@executor/platform-api"; |
| 2 | type ControlPlaneClient, | 2 | import { WorkspaceIdSchema } from "@executor/platform-sdk/schema"; |
| 3 | WorkspaceIdSchema, | ||
| 4 | } from "@executor/control-plane"; | ||
| 5 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 6 | 4 | ||
| 7 | const readBindingString = (binding: Record<string, unknown>, key: string): string | null => | 5 | const readBindingString = (binding: Record<string, unknown>, key: string): string | null => |
| @@ -9,17 +9,22 @@ import { · 8 unmodified lines | |||
| 9 | NodePath, | 9 | NodePath, |
| 10 | NodeRuntime, | 10 | NodeRuntime, |
| 11 | } from "@effect/platform-node"; | 11 | } from "@effect/platform-node"; |
| 12 | import { | ||
| 13 | createControlPlaneClient, | ||
| 14 | type ControlPlaneClient, | ||
| 15 | } from "@executor/platform-api"; | ||
| 16 | import { createExecutorAdminToolMap } from "@executor/platform-internal"; | ||
| 12 | import { | 17 | import { |
| 13 | EXECUTOR_SOURCES_ADD_HELP_LINES, | 18 | EXECUTOR_SOURCES_ADD_HELP_LINES, |
| 14 | ExecutionIdSchema, | ||
| 15 | RuntimeExecutionResolverService, | 19 | RuntimeExecutionResolverService, |
| 16 | createControlPlaneClient, | ||
| 17 | createControlPlaneRuntime, | 20 | createControlPlaneRuntime, |
| 18 | type ControlPlaneClient, | 21 | type ControlPlaneRuntime, |
| 22 | } from "@executor/platform-sdk/runtime"; | ||
| 23 | import { | ||
| 24 | ExecutionIdSchema, | ||
| 19 | type ExecutionEnvelope, | 25 | type ExecutionEnvelope, |
| 20 | type ExecutionInteraction, | 26 | type ExecutionInteraction, |
| 21 | type ControlPlaneRuntime, | 27 | } from "@executor/platform-sdk/schema"; |
| 22 | } from "@executor/control-plane"; | ||
| 23 | import type { ToolCatalog } from "@executor/codemode-core"; | 28 | import type { ToolCatalog } from "@executor/codemode-core"; |
| 24 | 29 | ||
| 25 | import * as Effect from "effect/Effect"; | 30 | import * as Effect from "effect/Effect"; |
| @@ -272,6 +277,7 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 246 unmodified lines | |||
| 272 | Effect.acquireUseRelease( | 277 | Effect.acquireUseRelease( |
| 273 | createControlPlaneRuntime({ | 278 | createControlPlaneRuntime({ |
| 274 | localDataDir: DEFAULT_LOCAL_DATA_DIR, | 279 | localDataDir: DEFAULT_LOCAL_DATA_DIR, |
| 280 | createInternalToolMap: createExecutorAdminToolMap, | ||
| 275 | }).pipe(Effect.mapError(toError)), | 281 | }).pipe(Effect.mapError(toError)), |
| 276 | (runtime) => | 282 | (runtime) => |
| 277 | Effect.gen(function* () { | 283 | Effect.gen(function* () { |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import type { ExecutionInteraction } from "@executor/control-plane"; | 2 | import type { ExecutionInteraction } from "@executor/platform-sdk/schema"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | buildPausedExecutionOutput, | 5 | buildPausedExecutionOutput, |
| @@ -1,4 +1,4 @@ | |||
| 1 | import type { ExecutionInteraction } from "@executor/control-plane"; | 1 | import type { ExecutionInteraction } from "@executor/platform-sdk/schema"; |
| 2 | 2 | ||
| 3 | const isRecord = (value: unknown): value is Record<string, unknown> => | 3 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 4 | typeof value === "object" && value !== null && !Array.isArray(value); | 4 | typeof value === "object" && value !== null && !Array.isArray(value); |
| @@ -5,8 +5,8 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | import { | 6 | import { |
| 7 | createControlPlaneClient, | 7 | createControlPlaneClient, |
| 8 | type SourceDiscoveryKind, | 8 | } from "@executor/platform-api"; |
| 9 | } from "@executor/control-plane"; | 9 | import { type SourceDiscoveryKind } from "@executor/platform-sdk/schema"; |
| 10 | import { createLocalExecutorServer } from "@executor/server"; | 10 | import { createLocalExecutorServer } from "@executor/server"; |
| 11 | 11 | ||
| 12 | const REAL_VERCEL_SPEC_URL = "https://openapi.vercel.sh"; | 12 | const REAL_VERCEL_SPEC_URL = "https://openapi.vercel.sh"; |
| @@ -20,15 +20,17 @@ import { makeToolInvokerFromTools, toTool } from "@executor/codemode-core"; · 19 unmodified lines | |||
| 20 | import { | 20 | import { |
| 21 | createControlPlaneClient, | 21 | createControlPlaneClient, |
| 22 | controlPlaneOpenApiSpec, | 22 | controlPlaneOpenApiSpec, |
| 23 | } from "@executor/platform-api"; | ||
| 24 | import { | ||
| 23 | buildLocalSourceArtifact, | 25 | buildLocalSourceArtifact, |
| 24 | catalogSyncResultFromMcpManifest, | 26 | catalogSyncResultFromMcpManifest, |
| 25 | deriveLocalInstallation, | 27 | deriveLocalInstallation, |
| 26 | type ResolveExecutionEnvironment, | ||
| 27 | resolveLocalWorkspaceContext, | 28 | resolveLocalWorkspaceContext, |
| 28 | SourceIdSchema, | ||
| 29 | writeLocalSourceArtifact, | 29 | writeLocalSourceArtifact, |
| 30 | writeProjectLocalExecutorConfig, | 30 | writeProjectLocalExecutorConfig, |
| 31 | } from "@executor/control-plane"; | 31 | type ResolveExecutionEnvironment, |
| 32 | } from "@executor/platform-sdk/runtime"; | ||
| 33 | import { SourceIdSchema } from "@executor/platform-sdk/schema"; | ||
| 32 | import { makeSesExecutor } from "@executor/runtime-ses"; | 34 | import { makeSesExecutor } from "@executor/runtime-ses"; |
| 33 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | 35 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 34 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; | 36 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| @@ -19,7 +19,7 @@ · 18 unmodified lines | |||
| 19 | }, | 19 | }, |
| 20 | "apps/executor": { | 20 | "apps/executor": { |
| 21 | "name": "executor", | 21 | "name": "executor", |
| 22 | "version": "1.2.3", | 22 | "version": "1.2.4-beta.4", |
| 23 | "bin": { | 23 | "bin": { |
| 24 | "executor": "bin/executor", | 24 | "executor": "bin/executor", |
| 25 | }, | 25 | }, |
| @@ -28,8 +28,10 @@ · 2 unmodified lines | |||
| 28 | "@effect/platform": "catalog:", | 28 | "@effect/platform": "catalog:", |
| 29 | "@effect/platform-node": "catalog:", | 29 | "@effect/platform-node": "catalog:", |
| 30 | "@executor/codemode-core": "workspace:*", | 30 | "@executor/codemode-core": "workspace:*", |
| 31 | "@executor/control-plane": "workspace:*", | ||
| 32 | "@executor/executor-mcp": "workspace:*", | 31 | "@executor/executor-mcp": "workspace:*", |
| 32 | "@executor/platform-api": "workspace:*", | ||
| 33 | "@executor/platform-internal": "workspace:*", | ||
| 34 | "@executor/platform-sdk": "workspace:*", | ||
| 33 | "@executor/server": "workspace:*", | 35 | "@executor/server": "workspace:*", |
| 34 | "effect": "catalog:", | 36 | "effect": "catalog:", |
| 35 | }, | 37 | }, |
| @@ -129,7 +131,8 @@ · 93 unmodified lines | |||
| 129 | "dependencies": { | 131 | "dependencies": { |
| 130 | "@effect-atom/atom": "0.5.3", | 132 | "@effect-atom/atom": "0.5.3", |
| 131 | "@effect-atom/atom-react": "0.5.0", | 133 | "@effect-atom/atom-react": "0.5.0", |
| 132 | "@executor/control-plane": "workspace:*", | 134 | "@executor/platform-api": "workspace:*", |
| 135 | "@executor/platform-sdk": "workspace:*", | ||
| 133 | "effect": "catalog:", | 136 | "effect": "catalog:", |
| 134 | }, | 137 | }, |
| 135 | "devDependencies": { | 138 | "devDependencies": { |
| @@ -156,7 +159,7 @@ · 20 unmodified lines | |||
| 156 | "@effect/platform": "catalog:", | 159 | "@effect/platform": "catalog:", |
| 157 | "@executor/ai-sdk-adapter": "workspace:*", | 160 | "@executor/ai-sdk-adapter": "workspace:*", |
| 158 | "@executor/codemode-core": "workspace:*", | 161 | "@executor/codemode-core": "workspace:*", |
| 159 | "@executor/control-plane": "workspace:*", | 162 | "@executor/platform-sdk": "workspace:*", |
| 160 | "@executor/runtime-ses": "workspace:*", | 163 | "@executor/runtime-ses": "workspace:*", |
| 161 | "@executor/source-openapi": "workspace:*", | 164 | "@executor/source-openapi": "workspace:*", |
| 162 | "effect": "catalog:", | 165 | "effect": "catalog:", |
| @@ -187,7 +190,7 @@ · 24 unmodified lines | |||
| 187 | "name": "@executor/executor-mcp", | 190 | "name": "@executor/executor-mcp", |
| 188 | "version": "0.0.0", | 191 | "version": "0.0.0", |
| 189 | "dependencies": { | 192 | "dependencies": { |
| 190 | "@executor/control-plane": "workspace:*", | 193 | "@executor/platform-sdk": "workspace:*", |
| 191 | "@modelcontextprotocol/sdk": "^1.26.0", | 194 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 192 | "effect": "catalog:", | 195 | "effect": "catalog:", |
| 193 | "zod": "catalog:", | 196 | "zod": "catalog:", |
| @@ -270,11 +273,41 @@ · 76 unmodified lines | |||
| 270 | "bun-types": "catalog:", | 273 | "bun-types": "catalog:", |
| 271 | }, | 274 | }, |
| 272 | }, | 275 | }, |
| 273 | "packages/platform/control-plane": { | 276 | "packages/platform/api": { |
| 274 | "name": "@executor/control-plane", | 277 | "name": "@executor/platform-api", |
| 275 | "version": "0.0.0", | 278 | "version": "0.0.0", |
| 276 | "dependencies": { | 279 | "dependencies": { |
| 277 | "@effect/platform": "catalog:", | 280 | "@effect/platform": "catalog:", |
| 281 | "@executor/platform-sdk": "workspace:*", | ||
| 282 | "effect": "catalog:", | ||
| 283 | }, | ||
| 284 | "devDependencies": { | ||
| 285 | "@effect/vitest": "catalog:", | ||
| 286 | "bun-types": "catalog:", | ||
| 287 | "typescript": "^5.9.3", | ||
| 288 | "vitest": "catalog:", | ||
| 289 | }, | ||
| 290 | }, | ||
| 291 | "packages/platform/internal": { | ||
| 292 | "name": "@executor/platform-internal", | ||
| 293 | "version": "0.0.0", | ||
| 294 | "dependencies": { | ||
| 295 | "@executor/codemode-core": "workspace:*", | ||
| 296 | "@executor/platform-sdk": "workspace:*", | ||
| 297 | "effect": "catalog:", | ||
| 298 | }, | ||
| 299 | "devDependencies": { | ||
| 300 | "@types/node": "catalog:", | ||
| 301 | "bun-types": "catalog:", | ||
| 302 | "typescript": "^5.9.3", | ||
| 303 | }, | ||
| 304 | }, | ||
| 305 | "packages/platform/sdk": { | ||
| 306 | "name": "@executor/platform-sdk", | ||
| 307 | "version": "0.0.0", | ||
| 308 | "dependencies": { | ||
| 309 | "@effect/platform": "catalog:", | ||
| 310 | "@effect/platform-node": "catalog:", | ||
| 278 | "@executor/auth-mcp-oauth": "workspace:*", | 311 | "@executor/auth-mcp-oauth": "workspace:*", |
| 279 | "@executor/auth-oauth2": "workspace:*", | 312 | "@executor/auth-oauth2": "workspace:*", |
| 280 | "@executor/codemode-core": "workspace:*", | 313 | "@executor/codemode-core": "workspace:*", |
| @@ -289,20 +322,20 @@ · 8 unmodified lines | |||
| 289 | "@executor/source-mcp": "workspace:*", | 322 | "@executor/source-mcp": "workspace:*", |
| 290 | "@executor/source-openapi": "workspace:*", | 323 | "@executor/source-openapi": "workspace:*", |
| 291 | "effect": "catalog:", | 324 | "effect": "catalog:", |
| 292 | "graphql": "^16.12.0", | ||
| 293 | "jsonc-parser": "3.3.1", | ||
| 294 | "postgres": "^3.4.8", | ||
| 295 | "prettier": "^3.8.1", | ||
| 296 | "typescript": "^5.9.3", | ||
| 297 | }, | 325 | }, |
| 298 | "devDependencies": { | 326 | "devDependencies": { |
| 299 | "@effect/platform-node": "catalog:", | ||
| 300 | "@effect/vitest": "catalog:", | 327 | "@effect/vitest": "catalog:", |
| 328 | "@executor/platform-api": "workspace:*", | ||
| 329 | "@executor/platform-internal": "workspace:*", | ||
| 301 | "@modelcontextprotocol/sdk": "^1.26.0", | 330 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 302 | "@types/node": "catalog:", | 331 | "@types/node": "catalog:", |
| 303 | "bun-types": "catalog:", | 332 | "bun-types": "catalog:", |
| 333 | "graphql": "^16.12.0", | ||
| 334 | "jsonc-parser": "3.3.1", | ||
| 335 | "postgres": "^3.4.8", | ||
| 336 | "prettier": "^3.8.1", | ||
| 337 | "typescript": "^5.9.3", | ||
| 304 | "vitest": "catalog:", | 338 | "vitest": "catalog:", |
| 305 | "zod": "catalog:", | ||
| 306 | }, | 339 | }, |
| 307 | }, | 340 | }, |
| 308 | "packages/platform/server": { | 341 | "packages/platform/server": { |
| @@ -312,8 +345,10 @@ · 3 unmodified lines | |||
| 312 | "@effect/opentelemetry": "^0.61.0", | 345 | "@effect/opentelemetry": "^0.61.0", |
| 313 | "@effect/platform": "catalog:", | 346 | "@effect/platform": "catalog:", |
| 314 | "@effect/platform-node": "catalog:", | 347 | "@effect/platform-node": "catalog:", |
| 315 | "@executor/control-plane": "workspace:*", | ||
| 316 | "@executor/executor-mcp": "workspace:*", | 348 | "@executor/executor-mcp": "workspace:*", |
| 349 | "@executor/platform-api": "workspace:*", | ||
| 350 | "@executor/platform-internal": "workspace:*", | ||
| 351 | "@executor/platform-sdk": "workspace:*", | ||
| 317 | "@opentelemetry/api": "^1.9.0", | 352 | "@opentelemetry/api": "^1.9.0", |
| 318 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", | 353 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", |
| 319 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", | 354 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", |
| @@ -641,8 +676,6 @@ · 321 unmodified lines | |||
| 641 | 676 | ||
| 642 | "@executor/codemode-core": ["@executor/codemode-core@workspace:packages/kernel/core"], | 677 | "@executor/codemode-core": ["@executor/codemode-core@workspace:packages/kernel/core"], |
| 643 | 678 | ||
| 644 | "@executor/control-plane": ["@executor/control-plane@workspace:packages/platform/control-plane"], | ||
| 645 | |||
| 646 | "@executor/docs": ["@executor/docs@workspace:apps/docs"], | 679 | "@executor/docs": ["@executor/docs@workspace:apps/docs"], |
| 647 | 680 | ||
| 648 | "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/hosts/mcp"], | 681 | "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/hosts/mcp"], |
| @@ -653,6 +686,12 @@ · 4 unmodified lines | |||
| 653 | 686 | ||
| 654 | "@executor/mcp-elicitation-demo": ["@executor/mcp-elicitation-demo@workspace:examples/mcp-elicitation-demo"], | 687 | "@executor/mcp-elicitation-demo": ["@executor/mcp-elicitation-demo@workspace:examples/mcp-elicitation-demo"], |
| 655 | 688 | ||
| 689 | "@executor/platform-api": ["@executor/platform-api@workspace:packages/platform/api"], | ||
| 690 | |||
| 691 | "@executor/platform-internal": ["@executor/platform-internal@workspace:packages/platform/internal"], | ||
| 692 | |||
| 693 | "@executor/platform-sdk": ["@executor/platform-sdk@workspace:packages/platform/sdk"], | ||
| 694 | |||
| 656 | "@executor/react": ["@executor/react@workspace:packages/clients/react"], | 695 | "@executor/react": ["@executor/react@workspace:packages/clients/react"], |
| 657 | 696 | ||
| 658 | "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/kernel/runtime-deno-subprocess"], | 697 | "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/kernel/runtime-deno-subprocess"], |
| @@ -35,7 +35,7 @@ · 34 unmodified lines | |||
| 35 | "release:pre:exit": "changeset pre exit", | 35 | "release:pre:exit": "changeset pre exit", |
| 36 | "release:beta:start": "changeset pre enter beta", | 36 | "release:beta:start": "changeset pre enter beta", |
| 37 | "release:beta:stop": "changeset pre exit", | 37 | "release:beta:stop": "changeset pre exit", |
| 38 | "fixture:release:capture": "bun run --cwd packages/platform/control-plane fixture:release:capture", | 38 | "fixture:release:capture": "bun run --cwd packages/platform/sdk fixture:release:capture", |
| 39 | "typecheck": "bun run --filter='*' typecheck", | 39 | "typecheck": "bun run --filter='*' typecheck", |
| 40 | "knip": "knip" | 40 | "knip": "knip" |
| 41 | }, | 41 | }, |
| @@ -13,7 +13,8 @@ · 12 unmodified lines | |||
| 13 | "dependencies": { | 13 | "dependencies": { |
| 14 | "@effect-atom/atom": "0.5.3", | 14 | "@effect-atom/atom": "0.5.3", |
| 15 | "@effect-atom/atom-react": "0.5.0", | 15 | "@effect-atom/atom-react": "0.5.0", |
| 16 | "@executor/control-plane": "workspace:*", | 16 | "@executor/platform-api": "workspace:*", |
| 17 | "@executor/platform-sdk": "workspace:*", | ||
| 17 | "effect": "catalog:" | 18 | "effect": "catalog:" |
| 18 | }, | 19 | }, |
| 19 | "peerDependencies": { | 20 | "peerDependencies": { |
| @@ -5,11 +5,15 @@ import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node"; · 4 unmodified lines | |||
| 5 | import { describe, expect, it } from "@effect/vitest"; | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | import { | 6 | import { |
| 7 | createControlPlaneApiLayer, | 7 | createControlPlaneApiLayer, |
| 8 | } from "@executor/platform-api"; | ||
| 9 | import { | ||
| 8 | createControlPlaneRuntime, | 10 | createControlPlaneRuntime, |
| 9 | type LocalInstallation, | ||
| 10 | type Source, | ||
| 11 | type ControlPlaneRuntime, | 11 | type ControlPlaneRuntime, |
| 12 | } from "@executor/control-plane"; | 12 | } from "@executor/platform-sdk/runtime"; |
| 13 | import type { | ||
| 14 | LocalInstallation, | ||
| 15 | Source, | ||
| 16 | } from "@executor/platform-sdk/schema"; | ||
| 13 | import * as Context from "effect/Context"; | 17 | import * as Context from "effect/Context"; |
| 14 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 15 | import * as Exit from "effect/Exit"; | 19 | import * as Exit from "effect/Exit"; |
| @@ -1,35 +1,37 @@ | |||
| 1 | import { Atom, Result } from "@effect-atom/atom"; | 1 | import { Atom, Result } from "@effect-atom/atom"; |
| 2 | import type * as Registry from "@effect-atom/atom/Registry"; | 2 | import type * as Registry from "@effect-atom/atom/Registry"; |
| 3 | import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react"; | 3 | import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react"; |
| 4 | import { createControlPlaneClient } from "@executor/control-plane/client"; | 4 | import { |
| 5 | createControlPlaneClient, | ||
| 6 | type CompleteSourceOAuthResult, | ||
| 7 | type ConnectSourceBatchPayload, | ||
| 8 | type ConnectSourceBatchResult, | ||
| 9 | type ConnectSourcePayload, | ||
| 10 | type ConnectSourceResult, | ||
| 11 | type ControlPlaneClient, | ||
| 12 | type CreateSecretPayload, | ||
| 13 | type CreateSecretResult, | ||
| 14 | type CreateSourcePayload, | ||
| 15 | type CreateWorkspaceOauthClientPayload, | ||
| 16 | type DeleteSecretResult, | ||
| 17 | type DiscoverSourcePayload, | ||
| 18 | type InstanceConfig, | ||
| 19 | type LocalInstallation, | ||
| 20 | type SecretListItem, | ||
| 21 | type StartSourceOAuthPayload, | ||
| 22 | type StartSourceOAuthResult, | ||
| 23 | type UpdateSecretPayload, | ||
| 24 | type UpdateSecretResult, | ||
| 25 | type UpdateSourcePayload, | ||
| 26 | } from "@executor/platform-api"; | ||
| 5 | import type { | 27 | import type { |
| 6 | CompleteSourceOAuthResult, | ||
| 7 | ConnectSourceBatchPayload, | ||
| 8 | ConnectSourceBatchResult, | ||
| 9 | ConnectSourcePayload, | ||
| 10 | ConnectSourceResult, | ||
| 11 | ControlPlaneClient, | ||
| 12 | CreateSecretPayload, | ||
| 13 | CreateSecretResult, | ||
| 14 | CreateSourcePayload, | ||
| 15 | CreateWorkspaceOauthClientPayload, | ||
| 16 | DeleteSecretResult, | ||
| 17 | DiscoverSourcePayload, | ||
| 18 | InstanceConfig, | ||
| 19 | LocalInstallation, | ||
| 20 | SecretListItem, | ||
| 21 | Source, | 28 | Source, |
| 22 | SourceDiscoveryResult, | 29 | SourceDiscoveryResult, |
| 23 | SourceInspection, | 30 | SourceInspection, |
| 24 | SourceInspectionDiscoverResult, | 31 | SourceInspectionDiscoverResult, |
| 25 | SourceInspectionToolDetail, | 32 | SourceInspectionToolDetail, |
| 26 | StartSourceOAuthPayload, | ||
| 27 | StartSourceOAuthResult, | ||
| 28 | UpdateSecretPayload, | ||
| 29 | UpdateSecretResult, | ||
| 30 | UpdateSourcePayload, | ||
| 31 | WorkspaceOauthClient, | 33 | WorkspaceOauthClient, |
| 32 | } from "@executor/control-plane"; | 34 | } from "@executor/platform-sdk/schema"; |
| 33 | import * as Cause from "effect/Cause"; | 35 | import * as Cause from "effect/Cause"; |
| 34 | import * as Effect from "effect/Effect"; | 36 | import * as Effect from "effect/Effect"; |
| 35 | import * as Exit from "effect/Exit"; | 37 | import * as Exit from "effect/Exit"; |
| @@ -12,9 +12,9 @@ · 11 unmodified lines | |||
| 12 | "@effect/platform": "catalog:", | 12 | "@effect/platform": "catalog:", |
| 13 | "@executor/ai-sdk-adapter": "workspace:*", | 13 | "@executor/ai-sdk-adapter": "workspace:*", |
| 14 | "@executor/codemode-core": "workspace:*", | 14 | "@executor/codemode-core": "workspace:*", |
| 15 | "@executor/source-openapi": "workspace:*", | 15 | "@executor/platform-sdk": "workspace:*", |
| 16 | "@executor/control-plane": "workspace:*", | ||
| 17 | "@executor/runtime-ses": "workspace:*", | 16 | "@executor/runtime-ses": "workspace:*", |
| 17 | "@executor/source-openapi": "workspace:*", | ||
| 18 | "effect": "catalog:" | 18 | "effect": "catalog:" |
| 19 | }, | 19 | }, |
| 20 | "devDependencies": { | 20 | "devDependencies": { |
| @@ -17,7 +17,7 @@ import { · 16 unmodified lines | |||
| 17 | type Source, | 17 | type Source, |
| 18 | type SourceId, | 18 | type SourceId, |
| 19 | type WorkspaceId, | 19 | type WorkspaceId, |
| 20 | } from "@executor/control-plane"; | 20 | } from "@executor/platform-sdk/schema"; |
| 21 | import { | 21 | import { |
| 22 | createToolCatalogDiscovery, | 22 | createToolCatalogDiscovery, |
| 23 | createToolCatalogFromTools, | 23 | createToolCatalogFromTools, |
| @@ -11,7 +11,7 @@ · 10 unmodified lines | |||
| 11 | "test:watch": "vitest" | 11 | "test:watch": "vitest" |
| 12 | }, | 12 | }, |
| 13 | "dependencies": { | 13 | "dependencies": { |
| 14 | "@executor/control-plane": "workspace:*", | 14 | "@executor/platform-sdk": "workspace:*", |
| 15 | "@modelcontextprotocol/sdk": "^1.26.0", | 15 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 16 | "effect": "catalog:", | 16 | "effect": "catalog:", |
| 17 | "zod": "catalog:" | 17 | "zod": "catalog:" |
| @@ -1,15 +1,15 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | ExecutionEnvelope, | 2 | ExecutionEnvelope, |
| 3 | ControlPlaneRuntime, | 3 | } from "@executor/platform-sdk/schema"; |
| 4 | } from "@executor/control-plane"; | ||
| 5 | import { | 4 | import { |
| 6 | EXECUTOR_SOURCES_ADD_HELP_LINES, | 5 | EXECUTOR_SOURCES_ADD_HELP_LINES, |
| 7 | ExecutionIdSchema, | ||
| 8 | RuntimeExecutionResolverService, | 6 | RuntimeExecutionResolverService, |
| 9 | createExecution, | 7 | createExecution, |
| 10 | getExecution, | 8 | getExecution, |
| 11 | resumeExecution, | 9 | resumeExecution, |
| 12 | } from "@executor/control-plane"; | 10 | type ControlPlaneRuntime, |
| 11 | } from "@executor/platform-sdk/runtime"; | ||
| 12 | import { ExecutionIdSchema } from "@executor/platform-sdk/schema"; | ||
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Cause from "effect/Cause"; | 14 | import * as Cause from "effect/Cause"; |
| 15 | import * as Exit from "effect/Exit"; | 15 | import * as Exit from "effect/Exit"; |
| @@ -1,5 +1,8 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import type { ExecutionEnvelope, ExecutionInteraction } from "@executor/control-plane"; | 2 | import type { |
| 3 | ExecutionEnvelope, | ||
| 4 | ExecutionInteraction, | ||
| 5 | } from "@executor/platform-sdk/schema"; | ||
| 3 | 6 | ||
| 4 | import { | 7 | import { |
| 5 | buildPausedResultText, | 8 | buildPausedResultText, |
| @@ -1,7 +1,7 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | ExecutionEnvelope, | 2 | ExecutionEnvelope, |
| 3 | ExecutionInteraction, | 3 | ExecutionInteraction, |
| 4 | } from "@executor/control-plane"; | 4 | } from "@executor/platform-sdk/schema"; |
| 5 | 5 | ||
| 6 | export type ParsedInteractionPayload = { | 6 | export type ParsedInteractionPayload = { |
| 7 | mode: "form" | "url"; | 7 | mode: "form" | "url"; |
| @@ -0,0 +1,40 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/platform-api", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts", | ||
| 7 | "./api": "./src/api.ts", | ||
| 8 | "./client": "./src/client.ts", | ||
| 9 | "./http": "./src/http.ts", | ||
| 10 | "./errors": "./src/errors.ts", | ||
| 11 | "./local/api": "./src/local/api.ts", | ||
| 12 | "./local/http": "./src/local/http.ts", | ||
| 13 | "./local-context": "./src/local-context.ts", | ||
| 14 | "./oauth/api": "./src/oauth/api.ts", | ||
| 15 | "./oauth/http": "./src/oauth/http.ts", | ||
| 16 | "./policies/api": "./src/policies/api.ts", | ||
| 17 | "./policies/http": "./src/policies/http.ts", | ||
| 18 | "./sources/api": "./src/sources/api.ts", | ||
| 19 | "./sources/http": "./src/sources/http.ts", | ||
| 20 | "./executions/api": "./src/executions/api.ts", | ||
| 21 | "./executions/http": "./src/executions/http.ts" | ||
| 22 | }, | ||
| 23 | "scripts": { | ||
| 24 | "test": "vitest run", | ||
| 25 | "test:watch": "vitest", | ||
| 26 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | ||
| 27 | }, | ||
| 28 | "dependencies": { | ||
| 29 | "@effect/platform": "catalog:", | ||
| 30 | "@executor/platform-sdk": "workspace:*", | ||
| 31 | "effect": "catalog:" | ||
| 32 | }, | ||
| 33 | "devDependencies": { | ||
| 34 | "@effect/vitest": "catalog:", | ||
| 35 | "bun-types": "catalog:", | ||
| 36 | "typescript": "^5.9.3", | ||
| 37 | "vitest": "catalog:" | ||
| 38 | }, | ||
| 39 | "version": "0.0.0" | ||
| 40 | } | ||
File renamed without changes.
| @@ -4,7 +4,7 @@ import { · 3 unmodified lines | |||
| 4 | } from "@effect/platform"; | 4 | } from "@effect/platform"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | ||
| 7 | import { ControlPlaneApi } from "./api/api"; | 7 | import { ControlPlaneApi } from "./api"; |
| 8 | 8 | ||
| 9 | export const createControlPlaneClient = (input: { | 9 | export const createControlPlaneClient = (input: { |
| 10 | baseUrl: string; | 10 | baseUrl: string; |
| @@ -0,0 +1,7 @@ | |||
| 1 | export { | ||
| 2 | ControlPlaneBadRequestError, | ||
| 3 | ControlPlaneForbiddenError, | ||
| 4 | ControlPlaneNotFoundError, | ||
| 5 | ControlPlaneStorageError, | ||
| 6 | ControlPlaneUnauthorizedError, | ||
| 7 | } from "@executor/platform-sdk/errors"; | ||
| @@ -1,10 +1,17 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | import { | ||
| 3 | CreateExecutionPayloadSchema, | ||
| 4 | ResumeExecutionPayloadSchema, | ||
| 5 | } from "@executor/platform-sdk/executions/contracts"; | ||
| 6 | export type { | ||
| 7 | CreateExecutionPayload, | ||
| 8 | ResumeExecutionPayload, | ||
| 9 | } from "@executor/platform-sdk/executions/contracts"; | ||
| 2 | import { | 10 | import { |
| 3 | ExecutionIdSchema, | 11 | ExecutionIdSchema, |
| 4 | ExecutionEnvelopeSchema, | 12 | ExecutionEnvelopeSchema, |
| 5 | WorkspaceIdSchema, | 13 | WorkspaceIdSchema, |
| 6 | } from "#schema"; | 14 | } from "@executor/platform-sdk/schema"; |
| 7 | import * as Schema from "effect/Schema"; | ||
| 8 | 15 | ||
| 9 | import { | 16 | import { |
| 10 | ControlPlaneBadRequestError, | 17 | ControlPlaneBadRequestError, |
| @@ -12,22 +19,12 @@ import { · 1 unmodified line | |||
| 12 | ControlPlaneNotFoundError, | 19 | ControlPlaneNotFoundError, |
| 13 | ControlPlaneStorageError, | 20 | ControlPlaneStorageError, |
| 14 | ControlPlaneUnauthorizedError, | 21 | ControlPlaneUnauthorizedError, |
| 15 | } from "../errors"; | 22 | } from "@executor/platform-sdk/errors"; |
| 16 | import { TrimmedNonEmptyStringSchema } from "../string-schemas"; | ||
| 17 | |||
| 18 | export const CreateExecutionPayloadSchema = Schema.Struct({ | ||
| 19 | code: TrimmedNonEmptyStringSchema, | ||
| 20 | interactionMode: Schema.optional(Schema.Literal("live", "live_form", "detach")), | ||
| 21 | }); | ||
| 22 | |||
| 23 | export type CreateExecutionPayload = typeof CreateExecutionPayloadSchema.Type; | ||
| 24 | |||
| 25 | export const ResumeExecutionPayloadSchema = Schema.Struct({ | ||
| 26 | responseJson: Schema.optional(Schema.String), | ||
| 27 | interactionMode: Schema.optional(Schema.Literal("live", "live_form", "detach")), | ||
| 28 | }); | ||
| 29 | 23 | ||
| 30 | export type ResumeExecutionPayload = typeof ResumeExecutionPayloadSchema.Type; | 24 | export { |
| 25 | CreateExecutionPayloadSchema, | ||
| 26 | ResumeExecutionPayloadSchema, | ||
| 27 | }; | ||
| 31 | 28 | ||
| 32 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | 29 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 33 | const executionIdParam = HttpApiSchema.param("executionId", ExecutionIdSchema); | 30 | const executionIdParam = HttpApiSchema.param("executionId", ExecutionIdSchema); |
| @@ -4,7 +4,7 @@ import { · 3 unmodified lines | |||
| 4 | createExecution, | 4 | createExecution, |
| 5 | getExecution, | 5 | getExecution, |
| 6 | resumeExecution, | 6 | resumeExecution, |
| 7 | } from "../../runtime/execution/service"; | 7 | } from "@executor/platform-sdk/runtime"; |
| 8 | 8 | ||
| 9 | import { ControlPlaneApi } from "../api"; | 9 | import { ControlPlaneApi } from "../api"; |
| 10 | import { resolveRequestedLocalWorkspace } from "../local-context"; | 10 | import { resolveRequestedLocalWorkspace } from "../local-context"; |
File renamed without changes.
| @@ -2,8 +2,12 @@ export { · 1 unmodified line | |||
| 2 | ControlPlaneApi, | 2 | ControlPlaneApi, |
| 3 | controlPlaneOpenApiSpec, | 3 | controlPlaneOpenApiSpec, |
| 4 | } from "./api"; | 4 | } from "./api"; |
| 5 | export { | ||
| 6 | createControlPlaneClient, | ||
| 7 | type ControlPlaneClient, | ||
| 8 | } from "./client"; | ||
| 5 | 9 | ||
| 6 | export type { LocalInstallation } from "#schema"; | 10 | export type { LocalInstallation } from "@executor/platform-sdk/schema"; |
| 7 | 11 | ||
| 8 | export { | 12 | export { |
| 9 | ControlPlaneBadRequestError, | 13 | ControlPlaneBadRequestError, |
| @@ -22,10 +26,12 @@ export { · 12 unmodified lines | |||
| 22 | 26 | ||
| 23 | export { | 27 | export { |
| 24 | CreateExecutionPayloadSchema, | 28 | CreateExecutionPayloadSchema, |
| 29 | ExecutionsApi, | ||
| 25 | ResumeExecutionPayloadSchema, | 30 | ResumeExecutionPayloadSchema, |
| 26 | type CreateExecutionPayload, | 31 | type CreateExecutionPayload, |
| 27 | type ResumeExecutionPayload, | 32 | type ResumeExecutionPayload, |
| 28 | } from "./executions/api"; | 33 | } from "./executions/api"; |
| 34 | export { ControlPlaneExecutionsLive } from "./executions/http"; | ||
| 29 | 35 | ||
| 30 | export { | 36 | export { |
| 31 | LocalApi, | 37 | LocalApi, |
| @@ -38,16 +44,22 @@ export { · 6 unmodified lines | |||
| 38 | type UpdateSecretResult, | 44 | type UpdateSecretResult, |
| 39 | type DeleteSecretResult, | 45 | type DeleteSecretResult, |
| 40 | } from "./local/api"; | 46 | } from "./local/api"; |
| 47 | export { ControlPlaneLocalLive } from "./local/http"; | ||
| 41 | 48 | ||
| 42 | export { | 49 | export { |
| 43 | OAuthApi, | 50 | OAuthApi, |
| 44 | StartSourceOAuthPayloadSchema, | 51 | StartSourceOAuthPayloadSchema, |
| 45 | StartSourceOAuthResultSchema, | 52 | StartSourceOAuthResultSchema, |
| 46 | CompleteSourceOAuthResultSchema, | 53 | CompleteSourceOAuthResultSchema, |
| 54 | SourceOAuthPopupFailureResultSchema, | ||
| 55 | SourceOAuthPopupResultSchema, | ||
| 56 | SourceOAuthPopupSuccessResultSchema, | ||
| 47 | type StartSourceOAuthPayload, | 57 | type StartSourceOAuthPayload, |
| 48 | type StartSourceOAuthResult, | 58 | type StartSourceOAuthResult, |
| 49 | type CompleteSourceOAuthResult, | 59 | type CompleteSourceOAuthResult, |
| 60 | type SourceOAuthPopupResult, | ||
| 50 | } from "./oauth/api"; | 61 | } from "./oauth/api"; |
| 62 | export { ControlPlaneOAuthLive } from "./oauth/http"; | ||
| 51 | 63 | ||
| 52 | export { | 64 | export { |
| 53 | ConnectSourceBatchPayloadSchema, | 65 | ConnectSourceBatchPayloadSchema, |
| @@ -57,6 +69,7 @@ export { · 3 unmodified lines | |||
| 57 | CreateWorkspaceOauthClientPayloadSchema, | 69 | CreateWorkspaceOauthClientPayloadSchema, |
| 58 | CreateSourcePayloadSchema, | 70 | CreateSourcePayloadSchema, |
| 59 | DiscoverSourcePayloadSchema, | 71 | DiscoverSourcePayloadSchema, |
| 72 | SourcesApi, | ||
| 60 | UpdateSourcePayloadSchema, | 73 | UpdateSourcePayloadSchema, |
| 61 | type ConnectSourceBatchPayload, | 74 | type ConnectSourceBatchPayload, |
| 62 | type ConnectSourceBatchResult, | 75 | type ConnectSourceBatchResult, |
| @@ -67,10 +80,15 @@ export { · 4 unmodified lines | |||
| 67 | type DiscoverSourcePayload, | 80 | type DiscoverSourcePayload, |
| 68 | type UpdateSourcePayload, | 81 | type UpdateSourcePayload, |
| 69 | } from "./sources/api"; | 82 | } from "./sources/api"; |
| 83 | export { ControlPlaneSourcesLive } from "./sources/http"; | ||
| 70 | 84 | ||
| 71 | export { | 85 | export { |
| 72 | CreatePolicyPayloadSchema, | 86 | CreatePolicyPayloadSchema, |
| 87 | PoliciesApi, | ||
| 73 | UpdatePolicyPayloadSchema, | 88 | UpdatePolicyPayloadSchema, |
| 74 | type CreatePolicyPayload, | 89 | type CreatePolicyPayload, |
| 75 | type UpdatePolicyPayload, | 90 | type UpdatePolicyPayload, |
| 76 | } from "./policies/api"; | 91 | } from "./policies/api"; |
| 92 | export { ControlPlanePoliciesLive } from "./policies/http"; | ||
| 93 | |||
| 94 | export { resolveRequestedLocalWorkspace } from "./local-context"; | ||
| @@ -1,10 +1,10 @@ | |||
| 1 | import type { WorkspaceId } from "#schema"; | 1 | import type { WorkspaceId } from "@executor/platform-sdk/schema"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | RuntimeLocalWorkspaceMismatchError, | 5 | RuntimeLocalWorkspaceMismatchError, |
| 6 | } from "../runtime/local/errors"; | 6 | requireRuntimeLocalWorkspace, |
| 7 | import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context"; | 7 | } from "@executor/platform-sdk/runtime"; |
| 8 | import { ControlPlaneForbiddenError } from "./errors"; | 8 | import { ControlPlaneForbiddenError } from "./errors"; |
| 9 | 9 | ||
| 10 | export const resolveRequestedLocalWorkspace = ( | 10 | export const resolveRequestedLocalWorkspace = ( |
| @@ -0,0 +1,69 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | ||
| 2 | import { | ||
| 3 | CreateSecretPayloadSchema, | ||
| 4 | CreateSecretResultSchema, | ||
| 5 | DeleteSecretResultSchema, | ||
| 6 | InstanceConfigSchema, | ||
| 7 | SecretListItemSchema, | ||
| 8 | UpdateSecretPayloadSchema, | ||
| 9 | UpdateSecretResultSchema, | ||
| 10 | } from "@executor/platform-sdk/local/contracts"; | ||
| 11 | import { | ||
| 12 | ControlPlaneBadRequestError, | ||
| 13 | ControlPlaneNotFoundError, | ||
| 14 | ControlPlaneStorageError, | ||
| 15 | } from "@executor/platform-sdk/errors"; | ||
| 16 | import { LocalInstallationSchema } from "@executor/platform-sdk/schema"; | ||
| 17 | import * as Schema from "effect/Schema"; | ||
| 18 | |||
| 19 | export type { | ||
| 20 | CreateSecretPayload, | ||
| 21 | CreateSecretResult, | ||
| 22 | DeleteSecretResult, | ||
| 23 | InstanceConfig, | ||
| 24 | SecretLinkedSource, | ||
| 25 | SecretListItem, | ||
| 26 | SecretProvider, | ||
| 27 | UpdateSecretPayload, | ||
| 28 | UpdateSecretResult, | ||
| 29 | } from "@executor/platform-sdk/local/contracts"; | ||
| 30 | |||
| 31 | export class LocalApi extends HttpApiGroup.make("local") | ||
| 32 | .add( | ||
| 33 | HttpApiEndpoint.get("installation")`/local/installation` | ||
| 34 | .addSuccess(LocalInstallationSchema) | ||
| 35 | .addError(ControlPlaneBadRequestError) | ||
| 36 | .addError(ControlPlaneNotFoundError) | ||
| 37 | .addError(ControlPlaneStorageError), | ||
| 38 | ) | ||
| 39 | .add( | ||
| 40 | HttpApiEndpoint.get("config")`/local/config` | ||
| 41 | .addSuccess(InstanceConfigSchema), | ||
| 42 | ) | ||
| 43 | .add( | ||
| 44 | HttpApiEndpoint.get("listSecrets")`/local/secrets` | ||
| 45 | .addSuccess(Schema.Array(SecretListItemSchema)) | ||
| 46 | .addError(ControlPlaneStorageError), | ||
| 47 | ) | ||
| 48 | .add( | ||
| 49 | HttpApiEndpoint.post("createSecret")`/local/secrets` | ||
| 50 | .setPayload(CreateSecretPayloadSchema) | ||
| 51 | .addSuccess(CreateSecretResultSchema) | ||
| 52 | .addError(ControlPlaneBadRequestError) | ||
| 53 | .addError(ControlPlaneStorageError), | ||
| 54 | ) | ||
| 55 | .add( | ||
| 56 | HttpApiEndpoint.patch("updateSecret")`/local/secrets/${HttpApiSchema.param("secretId", Schema.String)}` | ||
| 57 | .setPayload(UpdateSecretPayloadSchema) | ||
| 58 | .addSuccess(UpdateSecretResultSchema) | ||
| 59 | .addError(ControlPlaneBadRequestError) | ||
| 60 | .addError(ControlPlaneNotFoundError) | ||
| 61 | .addError(ControlPlaneStorageError), | ||
| 62 | ) | ||
| 63 | .add( | ||
| 64 | HttpApiEndpoint.del("deleteSecret")`/local/secrets/${HttpApiSchema.param("secretId", Schema.String)}` | ||
| 65 | .addSuccess(DeleteSecretResultSchema) | ||
| 66 | .addError(ControlPlaneNotFoundError) | ||
| 67 | .addError(ControlPlaneStorageError), | ||
| 68 | ) | ||
| 69 | .prefix("/v1") {} | ||
| @@ -0,0 +1,29 @@ | |||
| 1 | import { HttpApiBuilder } from "@effect/platform"; | ||
| 2 | import { | ||
| 3 | createLocalSecret, | ||
| 4 | deleteLocalSecret, | ||
| 5 | getLocalInstanceConfig, | ||
| 6 | getLocalInstallation, | ||
| 7 | listLocalSecrets, | ||
| 8 | updateLocalSecret, | ||
| 9 | } from "@executor/platform-sdk"; | ||
| 10 | |||
| 11 | import { ControlPlaneApi } from "../api"; | ||
| 12 | |||
| 13 | export const ControlPlaneLocalLive = HttpApiBuilder.group( | ||
| 14 | ControlPlaneApi, | ||
| 15 | "local", | ||
| 16 | (handlers) => | ||
| 17 | handlers | ||
| 18 | .handle("installation", () => getLocalInstallation()) | ||
| 19 | .handle("config", () => getLocalInstanceConfig()) | ||
| 20 | .handle("listSecrets", () => listLocalSecrets()) | ||
| 21 | .handle("createSecret", ({ payload }) => createLocalSecret(payload)) | ||
| 22 | .handle("updateSecret", ({ path, payload }) => | ||
| 23 | updateLocalSecret({ | ||
| 24 | secretId: path.secretId, | ||
| 25 | payload, | ||
| 26 | }), | ||
| 27 | ) | ||
| 28 | .handle("deleteSecret", ({ path }) => deleteLocalSecret(path.secretId)), | ||
| 29 | ); | ||
| @@ -5,7 +5,7 @@ import { · 4 unmodified lines | |||
| 5 | SourceTransportSchema, | 5 | SourceTransportSchema, |
| 6 | StringMapSchema, | 6 | StringMapSchema, |
| 7 | WorkspaceIdSchema, | 7 | WorkspaceIdSchema, |
| 8 | } from "#schema"; | 8 | } from "@executor/platform-sdk/schema"; |
| 9 | import * as Schema from "effect/Schema"; | 9 | import * as Schema from "effect/Schema"; |
| 10 | 10 | ||
| 11 | import { | 11 | import { |
| @@ -13,8 +13,8 @@ import { · 1 unmodified line | |||
| 13 | ControlPlaneForbiddenError, | 13 | ControlPlaneForbiddenError, |
| 14 | ControlPlaneStorageError, | 14 | ControlPlaneStorageError, |
| 15 | ControlPlaneUnauthorizedError, | 15 | ControlPlaneUnauthorizedError, |
| 16 | } from "../errors"; | 16 | } from "@executor/platform-sdk/errors"; |
| 17 | import { TrimmedNonEmptyStringSchema } from "../string-schemas"; | 17 | import { TrimmedNonEmptyStringSchema } from "@executor/platform-sdk/string-schemas"; |
| 18 | 18 | ||
| 19 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | 19 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 20 | 20 | ||
| @@ -3,13 +3,13 @@ import { · 2 unmodified lines | |||
| 3 | HttpServerRequest, | 3 | HttpServerRequest, |
| 4 | HttpServerResponse, | 4 | HttpServerResponse, |
| 5 | } from "@effect/platform"; | 5 | } from "@effect/platform"; |
| 6 | import type { WorkspaceId } from "#schema"; | 6 | import type { WorkspaceId } from "@executor/platform-sdk/schema"; |
| 7 | import * as Cause from "effect/Cause"; | 7 | import * as Cause from "effect/Cause"; |
| 8 | import * as Either from "effect/Either"; | 8 | import * as Either from "effect/Either"; |
| 9 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 11 | 11 | ||
| 12 | import { RuntimeSourceAuthServiceTag } from "../../runtime/sources/source-auth-service"; | 12 | import { RuntimeSourceAuthServiceTag } from "@executor/platform-sdk/runtime"; |
| 13 | import { ControlPlaneApi } from "../api"; | 13 | import { ControlPlaneApi } from "../api"; |
| 14 | import { | 14 | import { |
| 15 | ControlPlaneBadRequestError, | 15 | ControlPlaneBadRequestError, |
File renamed without changes.
| @@ -1,37 +1,31 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | import { | 2 | import { |
| 3 | LocalWorkspacePolicyApprovalModeSchema, | 3 | CreatePolicyPayloadSchema, |
| 4 | LocalWorkspacePolicyEffectSchema, | 4 | UpdatePolicyPayloadSchema, |
| 5 | LocalWorkspacePolicySchema, | 5 | } from "@executor/platform-sdk/policies/contracts"; |
| 6 | PolicyIdSchema, | ||
| 7 | WorkspaceIdSchema, | ||
| 8 | } from "#schema"; | ||
| 9 | import * as Schema from "effect/Schema"; | ||
| 10 | |||
| 11 | import { | 6 | import { |
| 12 | ControlPlaneBadRequestError, | 7 | ControlPlaneBadRequestError, |
| 13 | ControlPlaneForbiddenError, | 8 | ControlPlaneForbiddenError, |
| 14 | ControlPlaneNotFoundError, | 9 | ControlPlaneNotFoundError, |
| 15 | ControlPlaneStorageError, | 10 | ControlPlaneStorageError, |
| 16 | ControlPlaneUnauthorizedError, | 11 | ControlPlaneUnauthorizedError, |
| 17 | } from "../errors"; | 12 | } from "@executor/platform-sdk/errors"; |
| 18 | import { OptionalTrimmedNonEmptyStringSchema } from "../string-schemas"; | 13 | import { |
| 19 | 14 | LocalWorkspacePolicySchema, | |
| 20 | const LocalWorkspacePolicyPayloadSchema = Schema.Struct({ | 15 | PolicyIdSchema, |
| 21 | resourcePattern: OptionalTrimmedNonEmptyStringSchema, | 16 | WorkspaceIdSchema, |
| 22 | effect: Schema.optional(LocalWorkspacePolicyEffectSchema), | 17 | } from "@executor/platform-sdk/schema"; |
| 23 | approvalMode: Schema.optional(LocalWorkspacePolicyApprovalModeSchema), | 18 | import * as Schema from "effect/Schema"; |
| 24 | priority: Schema.optional(Schema.Number), | ||
| 25 | enabled: Schema.optional(Schema.Boolean), | ||
| 26 | }); | ||
| 27 | |||
| 28 | export const CreatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema; | ||
| 29 | |||
| 30 | export type CreatePolicyPayload = typeof CreatePolicyPayloadSchema.Type; | ||
| 31 | 19 | ||
| 32 | export const UpdatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema; | 20 | export type { |
| 21 | CreatePolicyPayload, | ||
| 22 | UpdatePolicyPayload, | ||
| 23 | } from "@executor/platform-sdk/policies/contracts"; | ||
| 33 | 24 | ||
| 34 | export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type; | 25 | export { |
| 26 | CreatePolicyPayloadSchema, | ||
| 27 | UpdatePolicyPayloadSchema, | ||
| 28 | }; | ||
| 35 | 29 | ||
| 36 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | 30 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 37 | const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema); | 31 | const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema); |
| @@ -6,7 +6,7 @@ import { · 5 unmodified lines | |||
| 6 | listPolicies, | 6 | listPolicies, |
| 7 | removePolicy, | 7 | removePolicy, |
| 8 | updatePolicy, | 8 | updatePolicy, |
| 9 | } from "../../runtime/policy/policies-operations"; | 9 | } from "@executor/platform-sdk"; |
| 10 | 10 | ||
| 11 | import { ControlPlaneApi } from "../api"; | 11 | import { ControlPlaneApi } from "../api"; |
| 12 | import { resolveRequestedLocalWorkspace } from "../local-context"; | 12 | import { resolveRequestedLocalWorkspace } from "../local-context"; |
| @@ -18,13 +18,14 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 5 unmodified lines | |||
| 18 | handlers | 18 | handlers |
| 19 | .handle("list", ({ path }) => | 19 | .handle("list", ({ path }) => |
| 20 | resolveRequestedLocalWorkspace("policies.list", path.workspaceId).pipe( | 20 | resolveRequestedLocalWorkspace("policies.list", path.workspaceId).pipe( |
| 21 | Effect.zipRight( | 21 | Effect.zipRight(listPolicies(path.workspaceId)), |
| 22 | listPolicies(path.workspaceId), | ||
| 23 | ), | ||
| 24 | ), | 22 | ), |
| 25 | ) | 23 | ) |
| 26 | .handle("create", ({ path, payload }) => | 24 | .handle("create", ({ path, payload }) => |
| 27 | resolveRequestedLocalWorkspace("policies.create", path.workspaceId).pipe( | 25 | resolveRequestedLocalWorkspace( |
| 26 | "policies.create", | ||
| 27 | path.workspaceId, | ||
| 28 | ).pipe( | ||
| 28 | Effect.zipRight( | 29 | Effect.zipRight( |
| 29 | createPolicy({ workspaceId: path.workspaceId, payload }), | 30 | createPolicy({ workspaceId: path.workspaceId, payload }), |
| 30 | ), | 31 | ), |
| @@ -41,7 +42,10 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 10 unmodified lines | |||
| 41 | ), | 42 | ), |
| 42 | ) | 43 | ) |
| 43 | .handle("update", ({ path, payload }) => | 44 | .handle("update", ({ path, payload }) => |
| 44 | resolveRequestedLocalWorkspace("policies.update", path.workspaceId).pipe( | 45 | resolveRequestedLocalWorkspace( |
| 46 | "policies.update", | ||
| 47 | path.workspaceId, | ||
| 48 | ).pipe( | ||
| 45 | Effect.zipRight( | 49 | Effect.zipRight( |
| 46 | updatePolicy({ | 50 | updatePolicy({ |
| 47 | workspaceId: path.workspaceId, | 51 | workspaceId: path.workspaceId, |
| @@ -52,7 +56,10 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 4 unmodified lines | |||
| 52 | ), | 56 | ), |
| 53 | ) | 57 | ) |
| 54 | .handle("remove", ({ path }) => | 58 | .handle("remove", ({ path }) => |
| 55 | resolveRequestedLocalWorkspace("policies.remove", path.workspaceId).pipe( | 59 | resolveRequestedLocalWorkspace( |
| 60 | "policies.remove", | ||
| 61 | path.workspaceId, | ||
| 62 | ).pipe( | ||
| 56 | Effect.zipRight( | 63 | Effect.zipRight( |
| 57 | removePolicy({ | 64 | removePolicy({ |
| 58 | workspaceId: path.workspaceId, | 65 | workspaceId: path.workspaceId, |
| @@ -1,189 +1,70 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | import { | 2 | import { |
| 3 | SourceOauthClientInputSchema, | 3 | ConnectSourceBatchPayloadSchema, |
| 4 | ExecutionInteractionIdSchema, | 4 | ConnectSourceBatchResultSchema, |
| 5 | JsonObjectSchema, | 5 | ConnectSourcePayloadSchema, |
| 6 | ConnectSourceResultSchema, | ||
| 7 | CreateSourcePayloadSchema, | ||
| 8 | CreateWorkspaceOauthClientPayloadSchema, | ||
| 9 | CredentialOauthCompleteUrlParamsSchema, | ||
| 10 | CredentialPageUrlParamsSchema, | ||
| 11 | CredentialSubmitPayloadSchema, | ||
| 12 | DiscoverSourcePayloadSchema, | ||
| 13 | UpdateSourcePayloadSchema, | ||
| 14 | WorkspaceOauthClientQuerySchema, | ||
| 15 | } from "@executor/platform-sdk/sources/contracts"; | ||
| 16 | import { | ||
| 17 | ControlPlaneBadRequestError, | ||
| 18 | ControlPlaneForbiddenError, | ||
| 19 | ControlPlaneNotFoundError, | ||
| 20 | ControlPlaneStorageError, | ||
| 21 | ControlPlaneUnauthorizedError, | ||
| 22 | } from "@executor/platform-sdk/errors"; | ||
| 23 | import { | ||
| 6 | ProviderAuthGrantIdSchema, | 24 | ProviderAuthGrantIdSchema, |
| 7 | SourceAuthSchema, | ||
| 8 | SourceAuthSessionIdSchema, | ||
| 9 | SourceDiscoveryResultSchema, | 25 | SourceDiscoveryResultSchema, |
| 10 | SourceIdSchema, | 26 | SourceIdSchema, |
| 11 | SourceInspectionDiscoverPayloadSchema, | 27 | SourceInspectionDiscoverPayloadSchema, |
| 12 | SourceInspectionDiscoverResultSchema, | 28 | SourceInspectionDiscoverResultSchema, |
| 13 | SourceInspectionSchema, | 29 | SourceInspectionSchema, |
| 14 | SourceInspectionToolDetailSchema, | 30 | SourceInspectionToolDetailSchema, |
| 15 | SourceImportAuthPolicySchema, | ||
| 16 | SourceKindSchema, | ||
| 17 | SourceProbeAuthSchema, | ||
| 18 | SourceSchema, | 31 | SourceSchema, |
| 19 | SourceStatusSchema, | ||
| 20 | WorkspaceOauthClientIdSchema, | 32 | WorkspaceOauthClientIdSchema, |
| 21 | WorkspaceOauthClientSchema, | 33 | WorkspaceOauthClientSchema, |
| 22 | WorkspaceIdSchema, | 34 | WorkspaceIdSchema, |
| 23 | } from "#schema"; | 35 | } from "@executor/platform-sdk/schema"; |
| 24 | import * as Schema from "effect/Schema"; | 36 | import * as Schema from "effect/Schema"; |
| 25 | 37 | ||
| 26 | import { | 38 | export type { |
| 27 | ControlPlaneBadRequestError, | 39 | ConnectSourceBatchPayload, |
| 28 | ControlPlaneForbiddenError, | 40 | ConnectSourceBatchResult, |
| 29 | ControlPlaneNotFoundError, | 41 | ConnectSourcePayload, |
| 30 | ControlPlaneStorageError, | 42 | ConnectSourceResult, |
| 31 | ControlPlaneUnauthorizedError, | 43 | CreateSourcePayload, |
| 32 | } from "../errors"; | 44 | CreateWorkspaceOauthClientPayload, |
| 33 | import { | 45 | DiscoverSourcePayload, |
| 34 | OptionalTrimmedNonEmptyStringSchema, | 46 | UpdateSourcePayload, |
| 35 | TrimmedNonEmptyStringSchema, | 47 | } from "@executor/platform-sdk/sources/contracts"; |
| 36 | } from "../string-schemas"; | ||
| 37 | import { | ||
| 38 | ConnectSourcePayloadSchema, | ||
| 39 | type ConnectSourcePayload, | ||
| 40 | } from "../../runtime/sources/source-adapters"; | ||
| 41 | |||
| 42 | const createSourcePayloadRequiredSchema = Schema.Struct({ | ||
| 43 | name: TrimmedNonEmptyStringSchema, | ||
| 44 | kind: SourceKindSchema, | ||
| 45 | endpoint: TrimmedNonEmptyStringSchema, | ||
| 46 | }); | ||
| 47 | |||
| 48 | const createSourcePayloadOptionalSchema = Schema.Struct({ | ||
| 49 | status: Schema.optional(SourceStatusSchema), | ||
| 50 | enabled: Schema.optional(Schema.Boolean), | ||
| 51 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 52 | binding: Schema.optional(JsonObjectSchema), | ||
| 53 | importAuthPolicy: Schema.optional(SourceImportAuthPolicySchema), | ||
| 54 | importAuth: Schema.optional(SourceAuthSchema), | ||
| 55 | auth: Schema.optional(SourceAuthSchema), | ||
| 56 | sourceHash: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 57 | lastError: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 58 | }); | ||
| 59 | |||
| 60 | export const CreateSourcePayloadSchema = Schema.extend( | ||
| 61 | createSourcePayloadRequiredSchema, | ||
| 62 | createSourcePayloadOptionalSchema, | ||
| 63 | ); | ||
| 64 | |||
| 65 | export type CreateSourcePayload = typeof CreateSourcePayloadSchema.Type; | ||
| 66 | export type CreateWorkspaceOauthClientPayload = | ||
| 67 | typeof CreateWorkspaceOauthClientPayloadSchema.Type; | ||
| 68 | |||
| 69 | export const UpdateSourcePayloadSchema = Schema.Struct({ | ||
| 70 | name: OptionalTrimmedNonEmptyStringSchema, | ||
| 71 | endpoint: OptionalTrimmedNonEmptyStringSchema, | ||
| 72 | status: Schema.optional(SourceStatusSchema), | ||
| 73 | enabled: Schema.optional(Schema.Boolean), | ||
| 74 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 75 | binding: Schema.optional(JsonObjectSchema), | ||
| 76 | importAuthPolicy: Schema.optional(SourceImportAuthPolicySchema), | ||
| 77 | importAuth: Schema.optional(SourceAuthSchema), | ||
| 78 | auth: Schema.optional(SourceAuthSchema), | ||
| 79 | sourceHash: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 80 | lastError: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 81 | }); | ||
| 82 | |||
| 83 | export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; | ||
| 84 | |||
| 85 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | ||
| 86 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); | ||
| 87 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); | ||
| 88 | |||
| 89 | const CredentialPageUrlParamsSchema = Schema.Struct({ | ||
| 90 | interactionId: ExecutionInteractionIdSchema, | ||
| 91 | }); | ||
| 92 | |||
| 93 | const CredentialSubmitPayloadSchema = Schema.Struct({ | ||
| 94 | action: Schema.optional(Schema.Literal("submit", "continue", "cancel")), | ||
| 95 | token: Schema.optional(Schema.String), | ||
| 96 | }); | ||
| 97 | |||
| 98 | const CredentialOauthCompleteUrlParamsSchema = Schema.Struct({ | ||
| 99 | state: Schema.String, | ||
| 100 | code: Schema.optional(Schema.String), | ||
| 101 | error: Schema.optional(Schema.String), | ||
| 102 | error_description: Schema.optional(Schema.String), | ||
| 103 | }); | ||
| 104 | |||
| 105 | const WorkspaceOauthClientQuerySchema = Schema.Struct({ | ||
| 106 | providerKey: Schema.String, | ||
| 107 | }); | ||
| 108 | |||
| 109 | const CreateWorkspaceOauthClientPayloadSchema = Schema.Struct({ | ||
| 110 | providerKey: Schema.String, | ||
| 111 | label: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 112 | oauthClient: SourceOauthClientInputSchema, | ||
| 113 | }); | ||
| 114 | |||
| 115 | const oauthClientIdParam = HttpApiSchema.param("oauthClientId", WorkspaceOauthClientIdSchema); | ||
| 116 | const grantIdParam = HttpApiSchema.param("grantId", ProviderAuthGrantIdSchema); | ||
| 117 | |||
| 118 | const ConnectGoogleDiscoveryBatchSourceSchema = Schema.Struct({ | ||
| 119 | service: TrimmedNonEmptyStringSchema, | ||
| 120 | version: TrimmedNonEmptyStringSchema, | ||
| 121 | discoveryUrl: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)), | ||
| 122 | scopes: Schema.optional(Schema.Array(TrimmedNonEmptyStringSchema)), | ||
| 123 | name: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 124 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 125 | }); | ||
| 126 | |||
| 127 | const ConnectSourceBatchPayloadSchema = Schema.Struct({ | ||
| 128 | workspaceOauthClientId: WorkspaceOauthClientIdSchema, | ||
| 129 | sources: Schema.Array(ConnectGoogleDiscoveryBatchSourceSchema), | ||
| 130 | }); | ||
| 131 | |||
| 132 | const ConnectSourceBatchResultSchema = Schema.Struct({ | ||
| 133 | results: Schema.Array( | ||
| 134 | Schema.Struct({ | ||
| 135 | source: SourceSchema, | ||
| 136 | status: Schema.Literal("connected", "pending_oauth"), | ||
| 137 | }), | ||
| 138 | ), | ||
| 139 | providerOauthSession: Schema.NullOr( | ||
| 140 | Schema.Struct({ | ||
| 141 | sessionId: SourceAuthSessionIdSchema, | ||
| 142 | authorizationUrl: Schema.String, | ||
| 143 | sourceIds: Schema.Array(SourceIdSchema), | ||
| 144 | }), | ||
| 145 | ), | ||
| 146 | }); | ||
| 147 | |||
| 148 | const DiscoverSourcePayloadSchema = Schema.Struct({ | ||
| 149 | url: TrimmedNonEmptyStringSchema, | ||
| 150 | probeAuth: Schema.optional(SourceProbeAuthSchema), | ||
| 151 | }); | ||
| 152 | |||
| 153 | export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type; | ||
| 154 | |||
| 155 | const ConnectSourceResultSchema = Schema.Union( | ||
| 156 | Schema.Struct({ | ||
| 157 | kind: Schema.Literal("connected"), | ||
| 158 | source: SourceSchema, | ||
| 159 | }), | ||
| 160 | Schema.Struct({ | ||
| 161 | kind: Schema.Literal("credential_required"), | ||
| 162 | source: SourceSchema, | ||
| 163 | credentialSlot: Schema.Literal("runtime", "import"), | ||
| 164 | }), | ||
| 165 | Schema.Struct({ | ||
| 166 | kind: Schema.Literal("oauth_required"), | ||
| 167 | source: SourceSchema, | ||
| 168 | sessionId: SourceAuthSessionIdSchema, | ||
| 169 | authorizationUrl: Schema.String, | ||
| 170 | }), | ||
| 171 | ); | ||
| 172 | |||
| 173 | export type ConnectSourceResult = typeof ConnectSourceResultSchema.Type; | ||
| 174 | export type ConnectSourceBatchPayload = typeof ConnectSourceBatchPayloadSchema.Type; | ||
| 175 | export type ConnectSourceBatchResult = typeof ConnectSourceBatchResultSchema.Type; | ||
| 176 | 48 | ||
| 177 | export { | 49 | export { |
| 178 | ConnectSourcePayloadSchema, | ||
| 179 | ConnectSourceBatchPayloadSchema, | 50 | ConnectSourceBatchPayloadSchema, |
| 180 | ConnectSourceBatchResultSchema, | 51 | ConnectSourceBatchResultSchema, |
| 52 | ConnectSourcePayloadSchema, | ||
| 181 | ConnectSourceResultSchema, | 53 | ConnectSourceResultSchema, |
| 54 | CreateSourcePayloadSchema, | ||
| 182 | CreateWorkspaceOauthClientPayloadSchema, | 55 | CreateWorkspaceOauthClientPayloadSchema, |
| 183 | DiscoverSourcePayloadSchema, | 56 | DiscoverSourcePayloadSchema, |
| 57 | UpdateSourcePayloadSchema, | ||
| 184 | }; | 58 | }; |
| 185 | 59 | ||
| 186 | export type { ConnectSourcePayload }; | 60 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 61 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); | ||
| 62 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); | ||
| 63 | const oauthClientIdParam = HttpApiSchema.param( | ||
| 64 | "oauthClientId", | ||
| 65 | WorkspaceOauthClientIdSchema, | ||
| 66 | ); | ||
| 67 | const grantIdParam = HttpApiSchema.param("grantId", ProviderAuthGrantIdSchema); | ||
| 187 | 68 | ||
| 188 | const HtmlSchema = HttpApiSchema.Text({ | 69 | const HtmlSchema = HttpApiSchema.Text({ |
| 189 | contentType: "text/html", | 70 | contentType: "text/html", |
| @@ -3,32 +3,32 @@ import { · 2 unmodified lines | |||
| 3 | HttpServerRequest, | 3 | HttpServerRequest, |
| 4 | HttpServerResponse, | 4 | HttpServerResponse, |
| 5 | } from "@effect/platform"; | 5 | } from "@effect/platform"; |
| 6 | import type { ExecutionInteraction, Source, WorkspaceId } from "#schema"; | 6 | import type { |
| 7 | ExecutionInteraction, | ||
| 8 | Source, | ||
| 9 | WorkspaceId, | ||
| 10 | } from "@executor/platform-sdk/schema"; | ||
| 7 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 8 | 12 | ||
| 9 | import { | 13 | import { |
| 14 | completeSourceCredentialSetup, | ||
| 10 | createSource, | 15 | createSource, |
| 11 | getSource, | 16 | discoverSource, |
| 12 | listSources, | ||
| 13 | removeSource, | ||
| 14 | updateSource, | ||
| 15 | } from "../../runtime/sources/sources-operations"; | ||
| 16 | import { | ||
| 17 | discoverSourceInspectionTools, | 17 | discoverSourceInspectionTools, |
| 18 | getSource, | ||
| 19 | getSourceCredentialInteraction, | ||
| 18 | getSourceInspection, | 20 | getSourceInspection, |
| 19 | getSourceInspectionToolDetail, | 21 | getSourceInspectionToolDetail, |
| 20 | } from "../../runtime/sources/source-inspection"; | 22 | listSources, |
| 21 | import { | 23 | removeSource, |
| 22 | completeSourceCredentialSetup, | ||
| 23 | getSourceCredentialInteraction, | ||
| 24 | submitSourceCredentialInteraction, | 24 | submitSourceCredentialInteraction, |
| 25 | } from "../../runtime/local/operations"; | 25 | updateSource, |
| 26 | import { discoverSource } from "../../runtime/sources/source-discovery"; | 26 | } from "@executor/platform-sdk"; |
| 27 | import { sourceAdapterRequiresInteractiveConnect } from "../../runtime/sources/source-adapters"; | ||
| 28 | import { | 27 | import { |
| 28 | sourceAdapterRequiresInteractiveConnect, | ||
| 29 | RuntimeSourceAuthServiceTag, | 29 | RuntimeSourceAuthServiceTag, |
| 30 | type ExecutorAddSourceInput, | 30 | type ExecutorAddSourceInput, |
| 31 | } from "../../runtime/sources/source-auth-service"; | 31 | } from "@executor/platform-sdk/runtime"; |
| 32 | 32 | ||
| 33 | import { | 33 | import { |
| 34 | ControlPlaneBadRequestError, | 34 | ControlPlaneBadRequestError, |
| @@ -58,7 +58,10 @@ const readHeader = (headers: unknown, name: string): string | null => { · 23 unmodified lines | |||
| 58 | return null; | 58 | return null; |
| 59 | }; | 59 | }; |
| 60 | 60 | ||
| 61 | const resolveRequestOrigin = (request: { url: string; headers: unknown }): string | null => { | 61 | const resolveRequestOrigin = (request: { |
| 62 | url: string; | ||
| 63 | headers: unknown; | ||
| 64 | }): string | null => { | ||
| 62 | try { | 65 | try { |
| 63 | return new URL(request.url).origin; | 66 | return new URL(request.url).origin; |
| 64 | } catch { | 67 | } catch { |
| @@ -69,7 +72,8 @@ const resolveRequestOrigin = (request: { url: string; headers: unknown }): strin · 4 unmodified lines | |||
| 69 | } | 72 | } |
| 70 | 73 | ||
| 71 | const forwardedProto = readHeader(request.headers, "x-forwarded-proto"); | 74 | const forwardedProto = readHeader(request.headers, "x-forwarded-proto"); |
| 72 | const protocol = forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http"; | 75 | const protocol = |
| 76 | forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http"; | ||
| 73 | return `${protocol}://${host}`; | 77 | return `${protocol}://${host}`; |
| 74 | } | 78 | } |
| 75 | }; | 79 | }; |
| @@ -86,7 +90,8 @@ const toBadRequestError = (operation: string, cause: unknown) => { · 10 unmodified lines | |||
| 86 | const isInteractiveConnectPayload = ( | 90 | const isInteractiveConnectPayload = ( |
| 87 | payload: ConnectSourcePayload, | 91 | payload: ConnectSourcePayload, |
| 88 | ): payload is Extract<ConnectSourcePayload, { kind?: "mcp" }> => | 92 | ): payload is Extract<ConnectSourcePayload, { kind?: "mcp" }> => |
| 89 | payload.kind === undefined || sourceAdapterRequiresInteractiveConnect(payload.kind); | 93 | payload.kind === undefined || |
| 94 | sourceAdapterRequiresInteractiveConnect(payload.kind); | ||
| 90 | 95 | ||
| 91 | const escapeHtml = (value: string): string => | 96 | const escapeHtml = (value: string): string => |
| 92 | value | 97 | value |
| @@ -415,11 +420,7 @@ const credentialPageDocument = (input: { · 322 unmodified lines | |||
| 415 | 420 | ||
| 416 | <hr /> | 421 | <hr /> |
| 417 | 422 | ||
| 418 | ${ | 423 | ${input.error ? `<p class="alert">${escapeHtml(input.error)}</p>` : ""} |
| 419 | input.error | ||
| 420 | ? `<p class="alert">${escapeHtml(input.error)}</p>` | ||
| 421 | : "" | ||
| 422 | } | ||
| 423 | ${ | 424 | ${ |
| 424 | input.state === "stored" | 425 | input.state === "stored" |
| 425 | ? `<div class="result-card success"> | 426 | ? `<div class="result-card success"> |
| @@ -431,17 +432,17 @@ const credentialPageDocument = (input: { · 5 unmodified lines | |||
| 431 | <p class="result-heading">Continuing without auth</p> | 432 | <p class="result-heading">Continuing without auth</p> |
| 432 | <p>Executor resumed the source connection without stored credentials. You can close this window.</p> | 433 | <p>Executor resumed the source connection without stored credentials. You can close this window.</p> |
| 433 | </div>` | 434 | </div>` |
| 434 | : input.state === "cancelled" | 435 | : input.state === "cancelled" |
| 435 | ? `<div class="result-card neutral"> | 436 | ? `<div class="result-card neutral"> |
| 436 | <p class="result-heading">Request cancelled</p> | 437 | <p class="result-heading">Request cancelled</p> |
| 437 | <p>Executor was notified. You can close this window.</p> | 438 | <p>Executor was notified. You can close this window.</p> |
| 438 | </div>` | 439 | </div>` |
| 439 | : input.state === "inactive" | 440 | : input.state === "inactive" |
| 440 | ? `<div class="result-card neutral"> | 441 | ? `<div class="result-card neutral"> |
| 441 | <p class="result-heading">No longer active</p> | 442 | <p class="result-heading">No longer active</p> |
| 442 | <p>This credential request has expired. Start a new source connection flow if needed.</p> | 443 | <p>This credential request has expired. Start a new source connection flow if needed.</p> |
| 443 | </div>` | 444 | </div>` |
| 444 | : `<form method="post"> | 445 | : `<form method="post"> |
| 445 | <div class="field"> | 446 | <div class="field"> |
| 446 | <label for="token">Personal access token</label> | 447 | <label for="token">Personal access token</label> |
| 447 | <input | 448 | <input |
| @@ -474,8 +475,8 @@ const sourceOAuthPopupResultDocument = (input: { · 26 unmodified lines | |||
| 474 | sessionId?: string | null; | 475 | sessionId?: string | null; |
| 475 | payload: | 476 | payload: |
| 476 | | { | 477 | | { |
| 477 | type: "executor:source-oauth-result"; | 478 | type: "executor:source-oauth-result"; |
| 478 | ok: true; | 479 | ok: true; |
| 479 | sourceId: Source["id"]; | 480 | sourceId: Source["id"]; |
| 480 | } | 481 | } |
| 481 | | { | 482 | | { |
| @@ -595,9 +596,7 @@ const sourceOAuthPopupResultDocument = (input: { · 113 unmodified lines | |||
| 595 | </html>`; | 596 | </html>`; |
| 596 | 597 | ||
| 597 | const htmlResponse = (html: string, status = 200) => | 598 | const htmlResponse = (html: string, status = 200) => |
| 598 | HttpServerResponse.html(html).pipe( | 599 | HttpServerResponse.html(html).pipe(HttpServerResponse.setStatus(status)); |
| 599 | HttpServerResponse.setStatus(status), | ||
| 600 | ); | ||
| 601 | 600 | ||
| 602 | const credentialErrorResponse = (input: { | 601 | const credentialErrorResponse = (input: { |
| 603 | title: string; | 602 | title: string; |
| @@ -619,7 +618,10 @@ const credentialErrorResponse = (input: { · 15 unmodified lines | |||
| 619 | ); | 618 | ); |
| 620 | 619 | ||
| 621 | const credentialErrorStatus = ( | 620 | const credentialErrorStatus = ( |
| 622 | error: ControlPlaneBadRequestError | ControlPlaneNotFoundError | ControlPlaneStorageError, | 621 | error: |
| 622 | | ControlPlaneBadRequestError | ||
| 623 | | ControlPlaneNotFoundError | ||
| 624 | | ControlPlaneStorageError, | ||
| 623 | ) => | 625 | ) => |
| 624 | error instanceof ControlPlaneNotFoundError | 626 | error instanceof ControlPlaneNotFoundError |
| 625 | ? 404 | 627 | ? 404 |
| @@ -679,15 +681,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 53 unmodified lines | |||
| 679 | ), | 681 | ), |
| 680 | ) | 682 | ) |
| 681 | .handle("connect", ({ path, payload }) => | 683 | .handle("connect", ({ path, payload }) => |
| 682 | resolveRequestedLocalWorkspace("sources.connect", path.workspaceId).pipe( | 684 | resolveRequestedLocalWorkspace( |
| 685 | "sources.connect", | ||
| 686 | path.workspaceId, | ||
| 687 | ).pipe( | ||
| 683 | Effect.flatMap((runtimeLocalWorkspace) => | 688 | Effect.flatMap((runtimeLocalWorkspace) => |
| 684 | Effect.gen(function* () { | 689 | Effect.gen(function* () { |
| 685 | const request = yield* HttpServerRequest.HttpServerRequest; | 690 | const request = yield* HttpServerRequest.HttpServerRequest; |
| 686 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 691 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 687 | const baseUrl = resolveRequestOrigin(request); | 692 | const baseUrl = resolveRequestOrigin(request); |
| 688 | 693 | ||
| 689 | if (isInteractiveConnectPayload(payload)) { | 694 | if (isInteractiveConnectPayload(payload)) { |
| 690 | return yield* sourceAuthService.connectMcpSource({ | 695 | return yield* sourceAuthService.connectMcpSource({ |
| 691 | workspaceId: path.workspaceId, | 696 | workspaceId: path.workspaceId, |
| 692 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | 697 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 693 | endpoint: payload.endpoint, | 698 | endpoint: payload.endpoint, |
| @@ -704,26 +709,29 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 10 unmodified lines | |||
| 704 | }); | 709 | }); |
| 705 | } | 710 | } |
| 706 | 711 | ||
| 707 | return yield* sourceAuthService.addExecutorSource( | 712 | return yield* sourceAuthService.addExecutorSource( |
| 708 | ({ | 713 | { |
| 709 | workspaceId: path.workspaceId, | 714 | workspaceId: path.workspaceId, |
| 710 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | 715 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 711 | executionId: null, | 716 | executionId: null, |
| 712 | interactionId: null, | 717 | interactionId: null, |
| 713 | ...(payload as Record<string, unknown>), | 718 | ...(payload as Record<string, unknown>), |
| 714 | } as ExecutorAddSourceInput), | 719 | } as ExecutorAddSourceInput, |
| 715 | { baseUrl }, | 720 | { baseUrl }, |
| 716 | ); | 721 | ); |
| 717 | }).pipe( | 722 | }).pipe( |
| 718 | Effect.catchAll((cause) => | 723 | Effect.catchAll((cause) => |
| 719 | Effect.fail(toBadRequestError("sources.connect", cause)), | 724 | Effect.fail(toBadRequestError("sources.connect", cause)), |
| 720 | ), | 725 | ), |
| 721 | ) | 726 | ), |
| 722 | ), | 727 | ), |
| 723 | ), | 728 | ), |
| 724 | ) | 729 | ) |
| 725 | .handle("connectBatch", ({ path, payload }) => | 730 | .handle("connectBatch", ({ path, payload }) => |
| 726 | resolveRequestedLocalWorkspace("sources.connectBatch", path.workspaceId).pipe( | 731 | resolveRequestedLocalWorkspace( |
| 732 | "sources.connectBatch", | ||
| 733 | path.workspaceId, | ||
| 734 | ).pipe( | ||
| 727 | Effect.flatMap((runtimeLocalWorkspace) => | 735 | Effect.flatMap((runtimeLocalWorkspace) => |
| 728 | Effect.gen(function* () { | 736 | Effect.gen(function* () { |
| 729 | const request = yield* HttpServerRequest.HttpServerRequest; | 737 | const request = yield* HttpServerRequest.HttpServerRequest; |
| @@ -748,7 +756,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 18 unmodified lines | |||
| 748 | ), | 756 | ), |
| 749 | ) | 757 | ) |
| 750 | .handle("listWorkspaceOauthClients", ({ path, urlParams }) => | 758 | .handle("listWorkspaceOauthClients", ({ path, urlParams }) => |
| 751 | resolveRequestedLocalWorkspace("sources.listWorkspaceOauthClients", path.workspaceId).pipe( | 759 | resolveRequestedLocalWorkspace( |
| 760 | "sources.listWorkspaceOauthClients", | ||
| 761 | path.workspaceId, | ||
| 762 | ).pipe( | ||
| 752 | Effect.flatMap(() => | 763 | Effect.flatMap(() => |
| 753 | Effect.gen(function* () { | 764 | Effect.gen(function* () { |
| 754 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 765 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| @@ -758,14 +769,19 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | |||
| 758 | }); | 769 | }); |
| 759 | }).pipe( | 770 | }).pipe( |
| 760 | Effect.catchAll((cause) => | 771 | Effect.catchAll((cause) => |
| 761 | Effect.fail(toBadRequestError("sources.listWorkspaceOauthClients", cause)), | 772 | Effect.fail( |
| 773 | toBadRequestError("sources.listWorkspaceOauthClients", cause), | ||
| 774 | ), | ||
| 762 | ), | 775 | ), |
| 763 | ), | 776 | ), |
| 764 | ), | 777 | ), |
| 765 | ), | 778 | ), |
| 766 | ) | 779 | ) |
| 767 | .handle("createWorkspaceOauthClient", ({ path, payload }) => | 780 | .handle("createWorkspaceOauthClient", ({ path, payload }) => |
| 768 | resolveRequestedLocalWorkspace("sources.createWorkspaceOauthClient", path.workspaceId).pipe( | 781 | resolveRequestedLocalWorkspace( |
| 782 | "sources.createWorkspaceOauthClient", | ||
| 783 | path.workspaceId, | ||
| 784 | ).pipe( | ||
| 769 | Effect.flatMap(() => | 785 | Effect.flatMap(() => |
| 770 | Effect.gen(function* () { | 786 | Effect.gen(function* () { |
| 771 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 787 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| @@ -777,44 +793,68 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines | |||
| 777 | }); | 793 | }); |
| 778 | }).pipe( | 794 | }).pipe( |
| 779 | Effect.catchAll((cause) => | 795 | Effect.catchAll((cause) => |
| 780 | Effect.fail(toBadRequestError("sources.createWorkspaceOauthClient", cause)), | 796 | Effect.fail( |
| 797 | toBadRequestError( | ||
| 798 | "sources.createWorkspaceOauthClient", | ||
| 799 | cause, | ||
| 800 | ), | ||
| 801 | ), | ||
| 781 | ), | 802 | ), |
| 782 | ), | 803 | ), |
| 783 | ), | 804 | ), |
| 784 | ), | 805 | ), |
| 785 | ) | 806 | ) |
| 786 | .handle("removeWorkspaceOauthClient", ({ path }) => | 807 | .handle("removeWorkspaceOauthClient", ({ path }) => |
| 787 | resolveRequestedLocalWorkspace("sources.removeWorkspaceOauthClient", path.workspaceId).pipe( | 808 | resolveRequestedLocalWorkspace( |
| 809 | "sources.removeWorkspaceOauthClient", | ||
| 810 | path.workspaceId, | ||
| 811 | ).pipe( | ||
| 788 | Effect.flatMap(() => | 812 | Effect.flatMap(() => |
| 789 | Effect.gen(function* () { | 813 | Effect.gen(function* () { |
| 790 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 814 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 791 | const removed = yield* sourceAuthService.removeWorkspaceOauthClient({ | 815 | const removed = |
| 792 | workspaceId: path.workspaceId, | 816 | yield* sourceAuthService.removeWorkspaceOauthClient({ |
| 793 | oauthClientId: path.oauthClientId, | 817 | workspaceId: path.workspaceId, |
| 794 | }); | 818 | oauthClientId: path.oauthClientId, |
| 819 | }); | ||
| 795 | 820 | ||
| 796 | return { removed }; | 821 | return { removed }; |
| 797 | }).pipe( | 822 | }).pipe( |
| 798 | Effect.catchAll((cause) => | 823 | Effect.catchAll((cause) => |
| 799 | Effect.fail(toBadRequestError("sources.removeWorkspaceOauthClient", cause)), | 824 | Effect.fail( |
| 825 | toBadRequestError( | ||
| 826 | "sources.removeWorkspaceOauthClient", | ||
| 827 | cause, | ||
| 828 | ), | ||
| 829 | ), | ||
| 800 | ), | 830 | ), |
| 801 | ), | 831 | ), |
| 802 | ), | 832 | ), |
| 803 | ), | 833 | ), |
| 804 | ) | 834 | ) |
| 805 | .handle("removeProviderAuthGrant", ({ path }) => | 835 | .handle("removeProviderAuthGrant", ({ path }) => |
| 806 | resolveRequestedLocalWorkspace("sources.removeProviderAuthGrant", path.workspaceId).pipe( | 836 | resolveRequestedLocalWorkspace( |
| 837 | "sources.removeProviderAuthGrant", | ||
| 838 | path.workspaceId, | ||
| 839 | ).pipe( | ||
| 807 | Effect.flatMap(() => | 840 | Effect.flatMap(() => |
| 808 | Effect.gen(function* () { | 841 | Effect.gen(function* () { |
| 809 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 842 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 810 | const removed = yield* sourceAuthService.removeProviderAuthGrant({ | 843 | const removed = yield* sourceAuthService |
| 811 | workspaceId: path.workspaceId, | 844 | .removeProviderAuthGrant({ |
| 812 | grantId: path.grantId, | 845 | workspaceId: path.workspaceId, |
| 813 | }).pipe( | 846 | grantId: path.grantId, |
| 814 | Effect.catchAll((cause) => | 847 | }) |
| 815 | Effect.fail(toBadRequestError("sources.removeProviderAuthGrant", cause)), | 848 | .pipe( |
| 816 | ), | 849 | Effect.catchAll((cause) => |
| 817 | ); | 850 | Effect.fail( |
| 851 | toBadRequestError( | ||
| 852 | "sources.removeProviderAuthGrant", | ||
| 853 | cause, | ||
| 854 | ), | ||
| 855 | ), | ||
| 856 | ), | ||
| 857 | ); | ||
| 818 | 858 | ||
| 819 | return { removed }; | 859 | return { removed }; |
| 820 | }), | 860 | }), |
| @@ -827,7 +867,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 6 unmodified lines | |||
| 827 | listSources({ | 867 | listSources({ |
| 828 | workspaceId: path.workspaceId, | 868 | workspaceId: path.workspaceId, |
| 829 | accountId: runtimeLocalWorkspace.installation.accountId, | 869 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 830 | }) | 870 | }), |
| 831 | ), | 871 | ), |
| 832 | ), | 872 | ), |
| 833 | ) | 873 | ) |
| @@ -838,7 +878,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | |||
| 838 | workspaceId: path.workspaceId, | 878 | workspaceId: path.workspaceId, |
| 839 | accountId: runtimeLocalWorkspace.installation.accountId, | 879 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 840 | payload, | 880 | payload, |
| 841 | }) | 881 | }), |
| 842 | ), | 882 | ), |
| 843 | ), | 883 | ), |
| 844 | ) | 884 | ) |
| @@ -849,12 +889,15 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | |||
| 849 | workspaceId: path.workspaceId, | 889 | workspaceId: path.workspaceId, |
| 850 | sourceId: path.sourceId, | 890 | sourceId: path.sourceId, |
| 851 | accountId: runtimeLocalWorkspace.installation.accountId, | 891 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 852 | }) | 892 | }), |
| 853 | ), | 893 | ), |
| 854 | ), | 894 | ), |
| 855 | ) | 895 | ) |
| 856 | .handle("inspection", ({ path }) => | 896 | .handle("inspection", ({ path }) => |
| 857 | resolveRequestedLocalWorkspace("sources.inspection", path.workspaceId).pipe( | 897 | resolveRequestedLocalWorkspace( |
| 898 | "sources.inspection", | ||
| 899 | path.workspaceId, | ||
| 900 | ).pipe( | ||
| 858 | Effect.zipRight( | 901 | Effect.zipRight( |
| 859 | getSourceInspection({ | 902 | getSourceInspection({ |
| 860 | workspaceId: path.workspaceId, | 903 | workspaceId: path.workspaceId, |
| @@ -864,7 +907,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | |||
| 864 | ), | 907 | ), |
| 865 | ) | 908 | ) |
| 866 | .handle("inspectionTool", ({ path }) => | 909 | .handle("inspectionTool", ({ path }) => |
| 867 | resolveRequestedLocalWorkspace("sources.inspection_tool", path.workspaceId).pipe( | 910 | resolveRequestedLocalWorkspace( |
| 911 | "sources.inspection_tool", | ||
| 912 | path.workspaceId, | ||
| 913 | ).pipe( | ||
| 868 | Effect.zipRight( | 914 | Effect.zipRight( |
| 869 | getSourceInspectionToolDetail({ | 915 | getSourceInspectionToolDetail({ |
| 870 | workspaceId: path.workspaceId, | 916 | workspaceId: path.workspaceId, |
| @@ -875,7 +921,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | |||
| 875 | ), | 921 | ), |
| 876 | ) | 922 | ) |
| 877 | .handle("inspectionDiscover", ({ path, payload }) => | 923 | .handle("inspectionDiscover", ({ path, payload }) => |
| 878 | resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe( | 924 | resolveRequestedLocalWorkspace( |
| 925 | "sources.inspection_discover", | ||
| 926 | path.workspaceId, | ||
| 927 | ).pipe( | ||
| 879 | Effect.zipRight( | 928 | Effect.zipRight( |
| 880 | discoverSourceInspectionTools({ | 929 | discoverSourceInspectionTools({ |
| 881 | workspaceId: path.workspaceId, | 930 | workspaceId: path.workspaceId, |
| @@ -893,7 +942,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 11 unmodified lines | |||
| 893 | sourceId: path.sourceId, | 942 | sourceId: path.sourceId, |
| 894 | accountId: runtimeLocalWorkspace.installation.accountId, | 943 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 895 | payload, | 944 | payload, |
| 896 | }) | 945 | }), |
| 897 | ), | 946 | ), |
| 898 | ), | 947 | ), |
| 899 | ) | 948 | ) |
| @@ -922,9 +971,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 22 unmodified lines | |||
| 922 | sourceLabel: interaction.sourceLabel, | 971 | sourceLabel: interaction.sourceLabel, |
| 923 | endpoint: interaction.endpoint, | 972 | endpoint: interaction.endpoint, |
| 924 | interactionId: interaction.interactionId, | 973 | interactionId: interaction.interactionId, |
| 925 | state: interaction.status === "pending" ? "pending" : "inactive", | 974 | state: |
| 975 | interaction.status === "pending" ? "pending" : "inactive", | ||
| 926 | }), | 976 | }), |
| 927 | ) | 977 | ), |
| 928 | ), | 978 | ), |
| 929 | Effect.catchTags({ | 979 | Effect.catchTags({ |
| 930 | ControlPlaneNotFoundError: (error) => | 980 | ControlPlaneNotFoundError: (error) => |
| @@ -967,14 +1017,14 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 36 unmodified lines | |||
| 967 | ? "Source Credential Stored" | 1017 | ? "Source Credential Stored" |
| 968 | : result.kind === "continued" | 1018 | : result.kind === "continued" |
| 969 | ? "Source Access Updated" | 1019 | ? "Source Access Updated" |
| 970 | : "Source Credential Request Cancelled", | 1020 | : "Source Credential Request Cancelled", |
| 971 | eyebrow: "Executor", | 1021 | eyebrow: "Executor", |
| 972 | message: | 1022 | message: |
| 973 | result.kind === "stored" | 1023 | result.kind === "stored" |
| 974 | ? "Executor stored the credential and resumed the source connection." | 1024 | ? "Executor stored the credential and resumed the source connection." |
| 975 | : result.kind === "continued" | 1025 | : result.kind === "continued" |
| 976 | ? "Executor resumed the source connection without stored credentials." | 1026 | ? "Executor resumed the source connection without stored credentials." |
| 977 | : "Executor cancelled the source credential request.", | 1027 | : "Executor cancelled the source credential request.", |
| 978 | sourceLabel: result.sourceLabel, | 1028 | sourceLabel: result.sourceLabel, |
| 979 | endpoint: result.endpoint, | 1029 | endpoint: result.endpoint, |
| 980 | interactionId: urlParams.interactionId, | 1030 | interactionId: urlParams.interactionId, |
| @@ -985,7 +1035,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | |||
| 985 | ? "continued" | 1035 | ? "continued" |
| 986 | : "cancelled", | 1036 | : "cancelled", |
| 987 | }), | 1037 | }), |
| 988 | ) | 1038 | ), |
| 989 | ), | 1039 | ), |
| 990 | Effect.catchTags({ | 1040 | Effect.catchTags({ |
| 991 | ControlPlaneBadRequestError: (error) => | 1041 | ControlPlaneBadRequestError: (error) => |
| @@ -1044,12 +1094,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 52 unmodified lines | |||
| 1044 | htmlResponse( | 1094 | htmlResponse( |
| 1045 | sourceOAuthPopupResultDocument({ | 1095 | sourceOAuthPopupResultDocument({ |
| 1046 | title: "OAuth failed", | 1096 | title: "OAuth failed", |
| 1047 | message: error instanceof Error ? error.message : "Failed completing OAuth", | 1097 | message: |
| 1098 | error instanceof Error | ||
| 1099 | ? error.message | ||
| 1100 | : "Failed completing OAuth", | ||
| 1048 | state: "cancelled", | 1101 | state: "cancelled", |
| 1049 | payload: { | 1102 | payload: { |
| 1050 | type: "executor:source-oauth-result", | 1103 | type: "executor:source-oauth-result", |
| 1051 | ok: false, | 1104 | ok: false, |
| 1052 | error: error instanceof Error ? error.message : "Failed completing OAuth", | 1105 | error: |
| 1106 | error instanceof Error | ||
| 1107 | ? error.message | ||
| 1108 | : "Failed completing OAuth", | ||
| 1053 | }, | 1109 | }, |
| 1054 | }), | 1110 | }), |
| 1055 | 500, | 1111 | 500, |
| @@ -1059,18 +1115,22 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | |||
| 1059 | ), | 1115 | ), |
| 1060 | ) | 1116 | ) |
| 1061 | .handle("providerOauthComplete", ({ path, urlParams }) => | 1117 | .handle("providerOauthComplete", ({ path, urlParams }) => |
| 1062 | resolveRequestedLocalWorkspace("sources.providerOauthComplete", path.workspaceId).pipe( | 1118 | resolveRequestedLocalWorkspace( |
| 1119 | "sources.providerOauthComplete", | ||
| 1120 | path.workspaceId, | ||
| 1121 | ).pipe( | ||
| 1063 | Effect.flatMap((runtimeLocalWorkspace) => | 1122 | Effect.flatMap((runtimeLocalWorkspace) => |
| 1064 | Effect.gen(function* () { | 1123 | Effect.gen(function* () { |
| 1065 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 1124 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 1066 | const completed = yield* sourceAuthService.completeProviderOauthCallback({ | 1125 | const completed = |
| 1067 | workspaceId: path.workspaceId, | 1126 | yield* sourceAuthService.completeProviderOauthCallback({ |
| 1068 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | 1127 | workspaceId: path.workspaceId, |
| 1069 | state: urlParams.state, | 1128 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 1070 | code: urlParams.code, | 1129 | state: urlParams.state, |
| 1071 | error: urlParams.error, | 1130 | code: urlParams.code, |
| 1072 | errorDescription: urlParams.error_description, | 1131 | error: urlParams.error, |
| 1073 | }); | 1132 | errorDescription: urlParams.error_description, |
| 1133 | }); | ||
| 1074 | const primarySource = completed.sources[0] ?? null; | 1134 | const primarySource = completed.sources[0] ?? null; |
| 1075 | 1135 | ||
| 1076 | return htmlResponse( | 1136 | return htmlResponse( |
| @@ -1082,7 +1142,9 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines | |||
| 1082 | payload: { | 1142 | payload: { |
| 1083 | type: "executor:source-oauth-result", | 1143 | type: "executor:source-oauth-result", |
| 1084 | ok: true, | 1144 | ok: true, |
| 1085 | sourceId: primarySource?.id ?? ("src_oauth_complete" as Source["id"]), | 1145 | sourceId: |
| 1146 | primarySource?.id ?? | ||
| 1147 | ("src_oauth_complete" as Source["id"]), | ||
| 1086 | }, | 1148 | }, |
| 1087 | }), | 1149 | }), |
| 1088 | ); | 1150 | ); |
| @@ -1092,12 +1154,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | |||
| 1092 | htmlResponse( | 1154 | htmlResponse( |
| 1093 | sourceOAuthPopupResultDocument({ | 1155 | sourceOAuthPopupResultDocument({ |
| 1094 | title: "OAuth failed", | 1156 | title: "OAuth failed", |
| 1095 | message: error instanceof Error ? error.message : "Failed completing OAuth", | 1157 | message: |
| 1158 | error instanceof Error | ||
| 1159 | ? error.message | ||
| 1160 | : "Failed completing OAuth", | ||
| 1096 | state: "cancelled", | 1161 | state: "cancelled", |
| 1097 | payload: { | 1162 | payload: { |
| 1098 | type: "executor:source-oauth-result", | 1163 | type: "executor:source-oauth-result", |
| 1099 | ok: false, | 1164 | ok: false, |
| 1100 | error: error instanceof Error ? error.message : "Failed completing OAuth", | 1165 | error: |
| 1166 | error instanceof Error | ||
| 1167 | ? error.message | ||
| 1168 | : "Failed completing OAuth", | ||
| 1101 | }, | 1169 | }, |
| 1102 | }), | 1170 | }), |
| 1103 | 500, | 1171 | 500, |
| @@ -1111,12 +1179,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 7 unmodified lines | |||
| 1111 | htmlResponse( | 1179 | htmlResponse( |
| 1112 | sourceOAuthPopupResultDocument({ | 1180 | sourceOAuthPopupResultDocument({ |
| 1113 | title: "OAuth failed", | 1181 | title: "OAuth failed", |
| 1114 | message: error instanceof Error ? error.message : "Failed completing OAuth", | 1182 | message: |
| 1183 | error instanceof Error | ||
| 1184 | ? error.message | ||
| 1185 | : "Failed completing OAuth", | ||
| 1115 | state: "cancelled", | 1186 | state: "cancelled", |
| 1116 | payload: { | 1187 | payload: { |
| 1117 | type: "executor:source-oauth-result", | 1188 | type: "executor:source-oauth-result", |
| 1118 | ok: false, | 1189 | ok: false, |
| 1119 | error: error instanceof Error ? error.message : "Failed completing OAuth", | 1190 | error: |
| 1191 | error instanceof Error | ||
| 1192 | ? error.message | ||
| 1193 | : "Failed completing OAuth", | ||
| 1120 | }, | 1194 | }, |
| 1121 | }), | 1195 | }), |
| 1122 | 500, | 1196 | 500, |
| @@ -0,0 +1,4 @@ | |||
| 1 | export { | ||
| 2 | OptionalTrimmedNonEmptyStringSchema, | ||
| 3 | TrimmedNonEmptyStringSchema, | ||
| 4 | } from "@executor/platform-sdk/string-schemas"; | ||
| @@ -9,12 +9,5 @@ · 8 unmodified lines | |||
| 9 | "lib": ["ES2022"], | 9 | "lib": ["ES2022"], |
| 10 | "types": ["bun-types", "node"] | 10 | "types": ["bun-types", "node"] |
| 11 | }, | 11 | }, |
| 12 | "include": [ | 12 | "include": ["src/**/*.ts"] |
| 13 | "src/**/*.ts", | ||
| 14 | "src/**/*.d.ts" | ||
| 15 | ], | ||
| 16 | "exclude": [ | ||
| 17 | "src/**/*.test.ts", | ||
| 18 | "src/persistence/**/*.ts" | ||
| 19 | ] | ||
| 20 | } | 13 | } |
File renamed without changes.
| @@ -1,13 +0,0 @@ | |||
| 1 | # @executor/control-plane | ||
| 2 | |||
| 3 | ## null | ||
| 4 | |||
| 5 | ### Patch Changes | ||
| 6 | |||
| 7 | - @executor/codemode-core@null | ||
| 8 | - @executor/source-google-discovery@null | ||
| 9 | - @executor/source-mcp@null | ||
| 10 | - @executor/source-openapi@null | ||
| 11 | - @executor/runtime-deno-subprocess@null | ||
| 12 | - @executor/runtime-quickjs@null | ||
| 13 | - @executor/runtime-ses@null | ||
| @@ -1,255 +0,0 @@ | |||
| 1 | import { | ||
| 2 | HttpApiBuilder, | ||
| 3 | } from "@effect/platform"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as Option from "effect/Option"; | ||
| 6 | |||
| 7 | import { SecretMaterialIdSchema } from "#schema"; | ||
| 8 | import { | ||
| 9 | getLocalInstallation, | ||
| 10 | } from "../../runtime/local/operations"; | ||
| 11 | import { requireRuntimeLocalWorkspace } from "../../runtime/local/runtime-context"; | ||
| 12 | import { | ||
| 13 | createDefaultSecretMaterialDeleter, | ||
| 14 | createDefaultSecretMaterialStorer, | ||
| 15 | createDefaultSecretMaterialUpdater, | ||
| 16 | ENV_SECRET_PROVIDER_ID, | ||
| 17 | KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 18 | LOCAL_SECRET_PROVIDER_ID, | ||
| 19 | parseSecretStoreProviderId, | ||
| 20 | resolveDefaultSecretStoreProviderId, | ||
| 21 | } from "../../runtime/local/secret-material-providers"; | ||
| 22 | import { RuntimeSourceStoreService } from "../../runtime/sources/source-store"; | ||
| 23 | import { ControlPlaneStore } from "../../runtime/store"; | ||
| 24 | import type { | ||
| 25 | CreateSecretResult, | ||
| 26 | InstanceConfig, | ||
| 27 | SecretProvider, | ||
| 28 | UpdateSecretResult, | ||
| 29 | } from "./api"; | ||
| 30 | |||
| 31 | import { ControlPlaneApi } from "../api"; | ||
| 32 | import { | ||
| 33 | ControlPlaneBadRequestError, | ||
| 34 | ControlPlaneNotFoundError, | ||
| 35 | ControlPlaneStorageError, | ||
| 36 | } from "../errors"; | ||
| 37 | |||
| 38 | const SECRET_STORE_PROVIDER_ENV = "EXECUTOR_SECRET_STORE_PROVIDER"; | ||
| 39 | |||
| 40 | const getInstanceConfig = (): Effect.Effect<InstanceConfig> => { | ||
| 41 | const explicitDefaultStoreProvider = | ||
| 42 | parseSecretStoreProviderId(process.env[SECRET_STORE_PROVIDER_ENV]); | ||
| 43 | const providers: SecretProvider[] = [ | ||
| 44 | { | ||
| 45 | id: LOCAL_SECRET_PROVIDER_ID, | ||
| 46 | name: "Local store", | ||
| 47 | canStore: true, | ||
| 48 | }, | ||
| 49 | ]; | ||
| 50 | |||
| 51 | if (process.platform === "darwin" || process.platform === "linux") { | ||
| 52 | providers.push({ | ||
| 53 | id: KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 54 | name: process.platform === "darwin" ? "macOS Keychain" : "Desktop Keyring", | ||
| 55 | canStore: | ||
| 56 | process.platform === "darwin" | ||
| 57 | || explicitDefaultStoreProvider === KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 58 | }); | ||
| 59 | } | ||
| 60 | |||
| 61 | providers.push({ | ||
| 62 | id: ENV_SECRET_PROVIDER_ID, | ||
| 63 | name: "Environment variable", | ||
| 64 | canStore: false, | ||
| 65 | }); | ||
| 66 | |||
| 67 | return resolveDefaultSecretStoreProviderId({ | ||
| 68 | storeProviderId: explicitDefaultStoreProvider ?? undefined, | ||
| 69 | }).pipe( | ||
| 70 | Effect.map((resolvedDefaultStoreProvider) => ({ | ||
| 71 | platform: process.platform, | ||
| 72 | secretProviders: providers, | ||
| 73 | defaultSecretStoreProvider: resolvedDefaultStoreProvider, | ||
| 74 | })), | ||
| 75 | ); | ||
| 76 | }; | ||
| 77 | |||
| 78 | const storageError = (message: string) => | ||
| 79 | new ControlPlaneStorageError({ | ||
| 80 | operation: "secrets", | ||
| 81 | message, | ||
| 82 | details: message, | ||
| 83 | }); | ||
| 84 | |||
| 85 | export const ControlPlaneLocalLive = HttpApiBuilder.group( | ||
| 86 | ControlPlaneApi, | ||
| 87 | "local", | ||
| 88 | (handlers) => | ||
| 89 | handlers | ||
| 90 | .handle("installation", () => | ||
| 91 | getLocalInstallation(), | ||
| 92 | ) | ||
| 93 | .handle("config", () => | ||
| 94 | getInstanceConfig(), | ||
| 95 | ) | ||
| 96 | .handle("listSecrets", () => | ||
| 97 | Effect.gen(function* () { | ||
| 98 | const store = yield* ControlPlaneStore; | ||
| 99 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 100 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace().pipe( | ||
| 101 | Effect.mapError(() => storageError("Failed resolving local workspace.")), | ||
| 102 | ); | ||
| 103 | const rows = yield* store.secretMaterials.listAll().pipe( | ||
| 104 | Effect.mapError(() => storageError("Failed listing secrets.")), | ||
| 105 | ); | ||
| 106 | const linkedSourcesMap = yield* sourceStore.listLinkedSecretSourcesInWorkspace( | ||
| 107 | runtimeLocalWorkspace.installation.workspaceId, | ||
| 108 | { | ||
| 109 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | ||
| 110 | }, | ||
| 111 | ).pipe( | ||
| 112 | Effect.mapError(() => storageError("Failed loading linked sources.")), | ||
| 113 | ); | ||
| 114 | return rows.map((row) => ({ | ||
| 115 | ...row, | ||
| 116 | linkedSources: linkedSourcesMap.get(row.id) ?? [], | ||
| 117 | })); | ||
| 118 | }), | ||
| 119 | ) | ||
| 120 | .handle("createSecret", ({ payload }) => | ||
| 121 | Effect.gen(function* () { | ||
| 122 | const name = payload.name.trim(); | ||
| 123 | const value = payload.value; | ||
| 124 | const purpose = payload.purpose ?? "auth_material"; | ||
| 125 | const requestedProviderId = payload.providerId === undefined | ||
| 126 | ? null | ||
| 127 | : parseSecretStoreProviderId(payload.providerId); | ||
| 128 | |||
| 129 | if (name.length === 0) { | ||
| 130 | return yield* new ControlPlaneBadRequestError({ | ||
| 131 | operation: "secrets.create", | ||
| 132 | message: "Secret name is required.", | ||
| 133 | details: "Secret name is required.", | ||
| 134 | }); | ||
| 135 | } | ||
| 136 | if (payload.providerId !== undefined && requestedProviderId === null) { | ||
| 137 | return yield* new ControlPlaneBadRequestError({ | ||
| 138 | operation: "secrets.create", | ||
| 139 | message: `Unsupported secret provider: ${payload.providerId}`, | ||
| 140 | details: `Unsupported secret provider: ${payload.providerId}`, | ||
| 141 | }); | ||
| 142 | } | ||
| 143 | |||
| 144 | const store = yield* ControlPlaneStore; | ||
| 145 | const storeSecretMaterial = createDefaultSecretMaterialStorer({ | ||
| 146 | rows: store, | ||
| 147 | ...(requestedProviderId ? { storeProviderId: requestedProviderId } : {}), | ||
| 148 | }); | ||
| 149 | const ref = yield* storeSecretMaterial({ | ||
| 150 | name, | ||
| 151 | purpose, | ||
| 152 | value, | ||
| 153 | }).pipe( | ||
| 154 | Effect.mapError((cause) => storageError( | ||
| 155 | cause instanceof Error ? cause.message : "Failed creating secret.", | ||
| 156 | )), | ||
| 157 | ); | ||
| 158 | const secretId = SecretMaterialIdSchema.make(ref.handle); | ||
| 159 | const created = yield* store.secretMaterials.getById(secretId).pipe( | ||
| 160 | Effect.mapError(() => storageError("Failed loading created secret.")), | ||
| 161 | ); | ||
| 162 | |||
| 163 | if (Option.isNone(created)) { | ||
| 164 | return yield* storageError(`Created secret not found: ${ref.handle}`); | ||
| 165 | } | ||
| 166 | |||
| 167 | return { | ||
| 168 | id: created.value.id, | ||
| 169 | name: created.value.name, | ||
| 170 | providerId: created.value.providerId, | ||
| 171 | purpose: created.value.purpose, | ||
| 172 | createdAt: created.value.createdAt, | ||
| 173 | updatedAt: created.value.updatedAt, | ||
| 174 | } satisfies CreateSecretResult; | ||
| 175 | }), | ||
| 176 | ) | ||
| 177 | .handle("updateSecret", ({ path, payload }) => | ||
| 178 | Effect.gen(function* () { | ||
| 179 | const secretId = SecretMaterialIdSchema.make(path.secretId); | ||
| 180 | const store = yield* ControlPlaneStore; | ||
| 181 | |||
| 182 | const existing = yield* store.secretMaterials.getById(secretId).pipe( | ||
| 183 | Effect.mapError(() => storageError("Failed looking up secret.")), | ||
| 184 | ); | ||
| 185 | |||
| 186 | if (Option.isNone(existing)) { | ||
| 187 | return yield* new ControlPlaneNotFoundError({ | ||
| 188 | operation: "secrets.update", | ||
| 189 | message: `Secret not found: ${path.secretId}`, | ||
| 190 | details: `Secret not found: ${path.secretId}`, | ||
| 191 | }); | ||
| 192 | } | ||
| 193 | |||
| 194 | const update: { name?: string | null; value?: string } = {}; | ||
| 195 | if (payload.name !== undefined) update.name = payload.name.trim() || null; | ||
| 196 | if (payload.value !== undefined) update.value = payload.value; | ||
| 197 | |||
| 198 | const updateSecretMaterial = createDefaultSecretMaterialUpdater({ | ||
| 199 | rows: store, | ||
| 200 | }); | ||
| 201 | const updated = yield* updateSecretMaterial({ | ||
| 202 | ref: { | ||
| 203 | providerId: existing.value.providerId, | ||
| 204 | handle: existing.value.id, | ||
| 205 | }, | ||
| 206 | ...update, | ||
| 207 | }).pipe( | ||
| 208 | Effect.mapError(() => storageError("Failed updating secret.")), | ||
| 209 | ); | ||
| 210 | |||
| 211 | return { | ||
| 212 | id: updated.id, | ||
| 213 | providerId: updated.providerId, | ||
| 214 | name: updated.name, | ||
| 215 | purpose: updated.purpose, | ||
| 216 | createdAt: updated.createdAt, | ||
| 217 | updatedAt: updated.updatedAt, | ||
| 218 | } satisfies UpdateSecretResult; | ||
| 219 | }), | ||
| 220 | ) | ||
| 221 | .handle("deleteSecret", ({ path }) => | ||
| 222 | Effect.gen(function* () { | ||
| 223 | const secretId = SecretMaterialIdSchema.make(path.secretId); | ||
| 224 | const store = yield* ControlPlaneStore; | ||
| 225 | |||
| 226 | const existing = yield* store.secretMaterials.getById(secretId).pipe( | ||
| 227 | Effect.mapError(() => storageError("Failed looking up secret.")), | ||
| 228 | ); | ||
| 229 | |||
| 230 | if (Option.isNone(existing)) { | ||
| 231 | return yield* new ControlPlaneNotFoundError({ | ||
| 232 | operation: "secrets.delete", | ||
| 233 | message: `Secret not found: ${path.secretId}`, | ||
| 234 | details: `Secret not found: ${path.secretId}`, | ||
| 235 | }); | ||
| 236 | } | ||
| 237 | |||
| 238 | const deleteSecretMaterial = createDefaultSecretMaterialDeleter({ | ||
| 239 | rows: store, | ||
| 240 | }); | ||
| 241 | const removed = yield* deleteSecretMaterial({ | ||
| 242 | providerId: existing.value.providerId, | ||
| 243 | handle: existing.value.id, | ||
| 244 | }).pipe( | ||
| 245 | Effect.mapError(() => storageError("Failed removing secret.")), | ||
| 246 | ); | ||
| 247 | |||
| 248 | if (!removed) { | ||
| 249 | return yield* storageError(`Failed removing secret: ${path.secretId}`); | ||
| 250 | } | ||
| 251 | |||
| 252 | return { removed: true }; | ||
| 253 | }), | ||
| 254 | ), | ||
| 255 | ); | ||
| @@ -1,4 +0,0 @@ | |||
| 1 | export * from "./schema/index"; | ||
| 2 | export * from "./api/index"; | ||
| 3 | export * from "./runtime/index"; | ||
| 4 | export * from "./client"; | ||
| @@ -1,1991 +0,0 @@ | |||
| 1 | import { createServer } from "node:http"; | ||
| 2 | import { join } from "node:path"; | ||
| 3 | |||
| 4 | import { FileSystem } from "@effect/platform"; | ||
| 5 | import { NodeFileSystem } from "@effect/platform-node"; | ||
| 6 | import { describe, expect, it } from "@effect/vitest"; | ||
| 7 | import { assertTrue } from "@effect/vitest/utils"; | ||
| 8 | import * as Either from "effect/Either"; | ||
| 9 | import * as Effect from "effect/Effect"; | ||
| 10 | import * as Fiber from "effect/Fiber"; | ||
| 11 | import * as Option from "effect/Option"; | ||
| 12 | |||
| 13 | import { | ||
| 14 | ExecutionIdSchema, | ||
| 15 | ExecutionInteractionIdSchema, | ||
| 16 | type ExecutionInteraction, | ||
| 17 | ProviderAuthGrantIdSchema, | ||
| 18 | SecretMaterialIdSchema, | ||
| 19 | SourceAuthSessionIdSchema, | ||
| 20 | SourceIdSchema, | ||
| 21 | WorkspaceOauthClientIdSchema, | ||
| 22 | } from "#schema"; | ||
| 23 | import type { ToolPath } from "@executor/codemode-core"; | ||
| 24 | import { createCatalogImportMetadata } from "@executor/source-core"; | ||
| 25 | import { createGraphqlCatalogFragment } from "@executor/source-graphql"; | ||
| 26 | |||
| 27 | import { | ||
| 28 | type ControlPlaneRuntime, | ||
| 29 | createControlPlaneRuntime, | ||
| 30 | LiveExecutionManagerService, | ||
| 31 | provideControlPlaneRuntime, | ||
| 32 | } from "./index"; | ||
| 33 | import { createSourceFromPayload } from "./sources/source-definitions"; | ||
| 34 | import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions"; | ||
| 35 | import { persistSource } from "./sources/source-store"; | ||
| 36 | import { withControlPlaneClient } from "./execution/test-http-client"; | ||
| 37 | import { runtimeEffectError } from "./effect-errors"; | ||
| 38 | import { | ||
| 39 | resolveLocalWorkspaceContext, | ||
| 40 | writeProjectLocalExecutorConfig, | ||
| 41 | } from "./local/config"; | ||
| 42 | import { writeLocalControlPlaneState } from "./local/control-plane-store"; | ||
| 43 | import { deriveLocalInstallation } from "./local/installation"; | ||
| 44 | import { | ||
| 45 | buildLocalSourceArtifact, | ||
| 46 | readLocalSourceArtifact, | ||
| 47 | writeLocalSourceArtifact, | ||
| 48 | } from "./local/source-artifacts"; | ||
| 49 | import { writeLocalWorkspaceState } from "./local/workspace-state"; | ||
| 50 | |||
| 51 | const makeRuntime = Effect.gen(function* () { | ||
| 52 | const fs = yield* FileSystem.FileSystem; | ||
| 53 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 54 | prefix: "executor-control-plane-runtime-", | ||
| 55 | }); | ||
| 56 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 57 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 58 | |||
| 59 | return yield* Effect.acquireRelease( | ||
| 60 | createControlPlaneRuntime({ | ||
| 61 | workspaceRoot, | ||
| 62 | homeConfigPath, | ||
| 63 | homeStateDirectory, | ||
| 64 | }), | ||
| 65 | (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie), | ||
| 66 | ); | ||
| 67 | }).pipe(Effect.provide(NodeFileSystem.layer)); | ||
| 68 | |||
| 69 | type OpenApiSpecServer = { | ||
| 70 | baseUrl: string; | ||
| 71 | specUrl: string; | ||
| 72 | close: () => Promise<void>; | ||
| 73 | }; | ||
| 74 | |||
| 75 | type GoogleWorkspaceTestServer = { | ||
| 76 | baseUrl: string; | ||
| 77 | tokenEndpoint: string; | ||
| 78 | discoveryUrl: (input: { | ||
| 79 | service: string; | ||
| 80 | version: string; | ||
| 81 | scope: string; | ||
| 82 | }) => string; | ||
| 83 | queueTokenResponse: (response: { | ||
| 84 | access_token: string; | ||
| 85 | refresh_token?: string; | ||
| 86 | expires_in?: number; | ||
| 87 | scope?: string; | ||
| 88 | }) => void; | ||
| 89 | tokenRequests: URLSearchParams[]; | ||
| 90 | discoveryAuthorizations: string[]; | ||
| 91 | close: () => Promise<void>; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const makeOpenApiSpecServer = Effect.acquireRelease( | ||
| 95 | Effect.promise<OpenApiSpecServer>( | ||
| 96 | () => | ||
| 97 | new Promise<OpenApiSpecServer>((resolve, reject) => { | ||
| 98 | const openApiDocument = JSON.stringify({ | ||
| 99 | openapi: "3.0.3", | ||
| 100 | info: { | ||
| 101 | title: "GitHub Test API", | ||
| 102 | version: "1.0.0", | ||
| 103 | }, | ||
| 104 | paths: { | ||
| 105 | "/repos/{owner}/{repo}": { | ||
| 106 | get: { | ||
| 107 | operationId: "repos/get-repo", | ||
| 108 | tags: ["repos"], | ||
| 109 | summary: "Get a repository", | ||
| 110 | responses: { | ||
| 111 | 200: { | ||
| 112 | description: "ok", | ||
| 113 | }, | ||
| 114 | }, | ||
| 115 | }, | ||
| 116 | }, | ||
| 117 | }, | ||
| 118 | }); | ||
| 119 | |||
| 120 | const server = createServer((request, response) => { | ||
| 121 | if (request.url !== "/openapi.json") { | ||
| 122 | response.statusCode = 404; | ||
| 123 | response.end(); | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | |||
| 127 | response.statusCode = 200; | ||
| 128 | response.setHeader("content-type", "application/json"); | ||
| 129 | response.end(openApiDocument); | ||
| 130 | }); | ||
| 131 | |||
| 132 | server.once("error", reject); | ||
| 133 | server.listen(0, "127.0.0.1", () => { | ||
| 134 | const address = server.address(); | ||
| 135 | if (!address || typeof address === "string") { | ||
| 136 | reject(new Error("Failed to bind OpenAPI runtime test server")); | ||
| 137 | return; | ||
| 138 | } | ||
| 139 | |||
| 140 | const baseUrl = `http://127.0.0.1:${address.port}`; | ||
| 141 | resolve({ | ||
| 142 | baseUrl, | ||
| 143 | specUrl: `${baseUrl}/openapi.json`, | ||
| 144 | close: () => | ||
| 145 | new Promise<void>((closeResolve, closeReject) => { | ||
| 146 | server.close((error) => { | ||
| 147 | if (error) { | ||
| 148 | closeReject(error); | ||
| 149 | return; | ||
| 150 | } | ||
| 151 | |||
| 152 | closeResolve(); | ||
| 153 | }); | ||
| 154 | }), | ||
| 155 | }); | ||
| 156 | }); | ||
| 157 | }), | ||
| 158 | ), | ||
| 159 | (server) => Effect.promise(() => server.close()).pipe(Effect.orDie), | ||
| 160 | ); | ||
| 161 | |||
| 162 | const makeGoogleWorkspaceTestServer = Effect.acquireRelease( | ||
| 163 | Effect.promise<GoogleWorkspaceTestServer>( | ||
| 164 | () => | ||
| 165 | new Promise<GoogleWorkspaceTestServer>((resolve, reject) => { | ||
| 166 | const discoveryScopes = new Map<string, string>(); | ||
| 167 | const tokenResponses: Array<{ | ||
| 168 | access_token: string; | ||
| 169 | refresh_token?: string; | ||
| 170 | expires_in?: number; | ||
| 171 | scope?: string; | ||
| 172 | }> = []; | ||
| 173 | const tokenRequests: URLSearchParams[] = []; | ||
| 174 | const discoveryAuthorizations: string[] = []; | ||
| 175 | let baseUrl = ""; | ||
| 176 | |||
| 177 | const server = createServer(async (request, response) => { | ||
| 178 | const requestUrl = new URL( | ||
| 179 | request.url ?? "/", | ||
| 180 | `http://${request.headers.host ?? "127.0.0.1"}`, | ||
| 181 | ); | ||
| 182 | |||
| 183 | if (request.method === "POST" && requestUrl.pathname === "/oauth/token") { | ||
| 184 | const chunks: Buffer[] = []; | ||
| 185 | for await (const chunk of request) { | ||
| 186 | chunks.push(Buffer.from(chunk)); | ||
| 187 | } | ||
| 188 | |||
| 189 | const params = new URLSearchParams(Buffer.concat(chunks).toString("utf8")); | ||
| 190 | tokenRequests.push(params); | ||
| 191 | const nextResponse = tokenResponses.shift(); | ||
| 192 | if (!nextResponse) { | ||
| 193 | response.statusCode = 500; | ||
| 194 | response.setHeader("content-type", "application/json"); | ||
| 195 | response.end(JSON.stringify({ | ||
| 196 | error: "missing_token_response", | ||
| 197 | })); | ||
| 198 | return; | ||
| 199 | } | ||
| 200 | |||
| 201 | response.statusCode = 200; | ||
| 202 | response.setHeader("content-type", "application/json"); | ||
| 203 | response.end(JSON.stringify({ | ||
| 204 | token_type: "Bearer", | ||
| 205 | expires_in: 3600, | ||
| 206 | ...nextResponse, | ||
| 207 | })); | ||
| 208 | return; | ||
| 209 | } | ||
| 210 | |||
| 211 | const discoveryMatch = /^\/([^/]+)\/\$discovery\/rest$/.exec(requestUrl.pathname); | ||
| 212 | if (request.method === "GET" && discoveryMatch) { | ||
| 213 | const service = discoveryMatch[1]!; | ||
| 214 | const version = requestUrl.searchParams.get("version") ?? "v1"; | ||
| 215 | const authorizationHeader = | ||
| 216 | typeof request.headers.authorization === "string" | ||
| 217 | ? request.headers.authorization | ||
| 218 | : Array.isArray(request.headers.authorization) | ||
| 219 | ? (request.headers.authorization[0] ?? "") | ||
| 220 | : ""; | ||
| 221 | if (authorizationHeader.length > 0) { | ||
| 222 | discoveryAuthorizations.push(authorizationHeader); | ||
| 223 | } | ||
| 224 | |||
| 225 | const scope = discoveryScopes.get(`${service}:${version}`) | ||
| 226 | ?? `https://example.test/auth/${service}`; | ||
| 227 | response.statusCode = 200; | ||
| 228 | response.setHeader("content-type", "application/json"); | ||
| 229 | response.end(JSON.stringify({ | ||
| 230 | name: service, | ||
| 231 | version, | ||
| 232 | title: `${service} API`, | ||
| 233 | description: `Test ${service} API`, | ||
| 234 | rootUrl: `${baseUrl}/`, | ||
| 235 | servicePath: `${service}/${version}/`, | ||
| 236 | auth: { | ||
| 237 | oauth2: { | ||
| 238 | scopes: { | ||
| 239 | [scope]: { | ||
| 240 | description: `${service} scope`, | ||
| 241 | }, | ||
| 242 | }, | ||
| 243 | }, | ||
| 244 | }, | ||
| 245 | methods: { | ||
| 246 | list: { | ||
| 247 | id: `${service}.list`, | ||
| 248 | path: "items", | ||
| 249 | httpMethod: "GET", | ||
| 250 | response: { | ||
| 251 | $ref: "ListResponse", | ||
| 252 | }, | ||
| 253 | }, | ||
| 254 | }, | ||
| 255 | schemas: { | ||
| 256 | ListResponse: { | ||
| 257 | id: "ListResponse", | ||
| 258 | type: "object", | ||
| 259 | properties: { | ||
| 260 | items: { | ||
| 261 | type: "array", | ||
| 262 | items: { | ||
| 263 | type: "string", | ||
| 264 | }, | ||
| 265 | }, | ||
| 266 | }, | ||
| 267 | }, | ||
| 268 | }, | ||
| 269 | })); | ||
| 270 | return; | ||
| 271 | } | ||
| 272 | |||
| 273 | response.statusCode = 404; | ||
| 274 | response.end(); | ||
| 275 | }); | ||
| 276 | |||
| 277 | server.once("error", reject); | ||
| 278 | server.listen(0, "127.0.0.1", () => { | ||
| 279 | const address = server.address(); | ||
| 280 | if (!address || typeof address === "string") { | ||
| 281 | reject(new Error("Failed to bind Google workspace test server")); | ||
| 282 | return; | ||
| 283 | } | ||
| 284 | |||
| 285 | baseUrl = `http://127.0.0.1:${address.port}`; | ||
| 286 | resolve({ | ||
| 287 | baseUrl, | ||
| 288 | tokenEndpoint: `${baseUrl}/oauth/token`, | ||
| 289 | discoveryUrl: ({ service, version, scope }) => { | ||
| 290 | discoveryScopes.set(`${service}:${version}`, scope); | ||
| 291 | return `${baseUrl}/${service}/$discovery/rest?version=${encodeURIComponent(version)}`; | ||
| 292 | }, | ||
| 293 | queueTokenResponse: (tokenResponse) => { | ||
| 294 | tokenResponses.push(tokenResponse); | ||
| 295 | }, | ||
| 296 | tokenRequests, | ||
| 297 | discoveryAuthorizations, | ||
| 298 | close: () => | ||
| 299 | new Promise<void>((closeResolve, closeReject) => { | ||
| 300 | server.close((error) => { | ||
| 301 | if (error) { | ||
| 302 | closeReject(error); | ||
| 303 | return; | ||
| 304 | } | ||
| 305 | |||
| 306 | closeResolve(); | ||
| 307 | }); | ||
| 308 | }), | ||
| 309 | }); | ||
| 310 | }); | ||
| 311 | }), | ||
| 312 | ), | ||
| 313 | (server) => Effect.promise(() => server.close()).pipe(Effect.orDie), | ||
| 314 | ); | ||
| 315 | |||
| 316 | const upsertLocalSecret = (runtime: ControlPlaneRuntime, input: { | ||
| 317 | id: string; | ||
| 318 | name: string; | ||
| 319 | purpose: "oauth_refresh_token" | "oauth_client_info" | "oauth_access_token"; | ||
| 320 | value: string; | ||
| 321 | }) => | ||
| 322 | runtime.persistence.rows.secretMaterials.upsert({ | ||
| 323 | id: SecretMaterialIdSchema.make(input.id), | ||
| 324 | providerId: "local", | ||
| 325 | handle: input.id, | ||
| 326 | name: input.name, | ||
| 327 | purpose: input.purpose, | ||
| 328 | value: input.value, | ||
| 329 | createdAt: Date.now(), | ||
| 330 | updatedAt: Date.now(), | ||
| 331 | }); | ||
| 332 | |||
| 333 | const createPersistedGoogleSource = (input: { | ||
| 334 | runtime: ControlPlaneRuntime; | ||
| 335 | name: string; | ||
| 336 | namespace: string; | ||
| 337 | service: string; | ||
| 338 | version: string; | ||
| 339 | discoveryUrl: string; | ||
| 340 | scopes: ReadonlyArray<string>; | ||
| 341 | auth: { | ||
| 342 | kind: "none"; | ||
| 343 | } | { | ||
| 344 | kind: "provider_grant_ref"; | ||
| 345 | grantId: ReturnType<typeof ProviderAuthGrantIdSchema.make>; | ||
| 346 | providerKey: string; | ||
| 347 | requiredScopes: ReadonlyArray<string>; | ||
| 348 | headerName: string; | ||
| 349 | prefix: string; | ||
| 350 | }; | ||
| 351 | }) => | ||
| 352 | Effect.gen(function* () { | ||
| 353 | const installation = input.runtime.localInstallation; | ||
| 354 | const source = yield* createSourceFromPayload({ | ||
| 355 | workspaceId: installation.workspaceId, | ||
| 356 | sourceId: SourceIdSchema.make(`src_${crypto.randomUUID()}`), | ||
| 357 | payload: { | ||
| 358 | name: input.name, | ||
| 359 | kind: "google_discovery", | ||
| 360 | endpoint: input.discoveryUrl, | ||
| 361 | namespace: input.namespace, | ||
| 362 | binding: { | ||
| 363 | service: input.service, | ||
| 364 | version: input.version, | ||
| 365 | discoveryUrl: input.discoveryUrl, | ||
| 366 | scopes: [...input.scopes], | ||
| 367 | }, | ||
| 368 | importAuthPolicy: "reuse_runtime", | ||
| 369 | importAuth: { kind: "none" }, | ||
| 370 | auth: input.auth as any, | ||
| 371 | status: "connected", | ||
| 372 | enabled: true, | ||
| 373 | }, | ||
| 374 | now: Date.now(), | ||
| 375 | }); | ||
| 376 | |||
| 377 | return yield* persistSource(input.runtime.persistence.rows, source, { | ||
| 378 | actorAccountId: installation.accountId, | ||
| 379 | }).pipe((effect) => provideControlPlaneRuntime(effect, input.runtime)); | ||
| 380 | }); | ||
| 381 | |||
| 382 | const expectLeft = <A, E>(effect: Effect.Effect<A, E, never>) => | ||
| 383 | Effect.either(effect).pipe( | ||
| 384 | Effect.flatMap((result) => | ||
| 385 | Either.isLeft(result) | ||
| 386 | ? Effect.succeed(result.left) | ||
| 387 | : Effect.fail(runtimeEffectError("control-plane-runtime.test", "Expected effect to fail")), | ||
| 388 | ), | ||
| 389 | ); | ||
| 390 | |||
| 391 | describe("control-plane-runtime", () => { | ||
| 392 | it.scoped("writes local source changes through executor.jsonc", () => | ||
| 393 | Effect.gen(function* () { | ||
| 394 | const fs = yield* FileSystem.FileSystem; | ||
| 395 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 396 | prefix: "executor-local-config-runtime-", | ||
| 397 | }); | ||
| 398 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 399 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 400 | const runtime = yield* Effect.acquireRelease( | ||
| 401 | createControlPlaneRuntime({ | ||
| 402 | workspaceRoot, | ||
| 403 | homeConfigPath, | ||
| 404 | homeStateDirectory, | ||
| 405 | }), | ||
| 406 | (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 407 | ); | ||
| 408 | const openApiServer = yield* makeOpenApiSpecServer; | ||
| 409 | const installation = runtime.localInstallation; | ||
| 410 | |||
| 411 | const createdSource = yield* withControlPlaneClient( | ||
| 412 | { runtime, accountId: installation.accountId }, | ||
| 413 | (client) => | ||
| 414 | client.sources.create({ | ||
| 415 | path: { workspaceId: installation.workspaceId }, | ||
| 416 | payload: { | ||
| 417 | name: "GitHub", | ||
| 418 | kind: "openapi", | ||
| 419 | endpoint: openApiServer.baseUrl, | ||
| 420 | namespace: "github", | ||
| 421 | binding: { | ||
| 422 | specUrl: openApiServer.specUrl, | ||
| 423 | defaultHeaders: null, | ||
| 424 | }, | ||
| 425 | auth: { kind: "none" }, | ||
| 426 | }, | ||
| 427 | }), | ||
| 428 | ); | ||
| 429 | |||
| 430 | const configPath = join(workspaceRoot, ".executor", "executor.jsonc"); | ||
| 431 | const createdConfig = JSON.parse(yield* fs.readFileString(configPath, "utf8")) as { | ||
| 432 | sources?: Record<string, { kind: string; connection: { endpoint: string } }>; | ||
| 433 | }; | ||
| 434 | expect(createdConfig.sources?.github?.kind).toBe("openapi"); | ||
| 435 | expect(createdConfig.sources?.github?.connection.endpoint).toBe(openApiServer.baseUrl); | ||
| 436 | |||
| 437 | const removed = yield* withControlPlaneClient( | ||
| 438 | { runtime, accountId: installation.accountId }, | ||
| 439 | (client) => | ||
| 440 | client.sources.remove({ | ||
| 441 | path: { | ||
| 442 | workspaceId: installation.workspaceId, | ||
| 443 | sourceId: createdSource.id, | ||
| 444 | }, | ||
| 445 | }), | ||
| 446 | ); | ||
| 447 | expect(removed.removed).toBe(true); | ||
| 448 | |||
| 449 | const removedConfig = JSON.parse(yield* fs.readFileString(configPath, "utf8")) as { | ||
| 450 | sources?: Record<string, unknown>; | ||
| 451 | }; | ||
| 452 | expect(removedConfig.sources?.github).toBeUndefined(); | ||
| 453 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 454 | 60_000, | ||
| 455 | ); | ||
| 456 | |||
| 457 | it.scoped("manages local secrets through provider-backed metadata rows", () => | ||
| 458 | Effect.gen(function* () { | ||
| 459 | const runtime = yield* makeRuntime; | ||
| 460 | |||
| 461 | const created = yield* withControlPlaneClient( | ||
| 462 | { runtime }, | ||
| 463 | (client) => | ||
| 464 | client.local.createSecret({ | ||
| 465 | payload: { | ||
| 466 | name: "GitHub PAT", | ||
| 467 | value: "ghp_test_token", | ||
| 468 | providerId: "local", | ||
| 469 | }, | ||
| 470 | }), | ||
| 471 | ); | ||
| 472 | |||
| 473 | expect(created.providerId).toBe("local"); | ||
| 474 | |||
| 475 | const listed = yield* withControlPlaneClient( | ||
| 476 | { runtime }, | ||
| 477 | (client) => client.local.listSecrets({}), | ||
| 478 | ); | ||
| 479 | expect(listed).toEqual([ | ||
| 480 | expect.objectContaining({ | ||
| 481 | id: created.id, | ||
| 482 | providerId: "local", | ||
| 483 | name: "GitHub PAT", | ||
| 484 | }), | ||
| 485 | ]); | ||
| 486 | |||
| 487 | const updated = yield* withControlPlaneClient( | ||
| 488 | { runtime }, | ||
| 489 | (client) => | ||
| 490 | client.local.updateSecret({ | ||
| 491 | path: { secretId: created.id }, | ||
| 492 | payload: { | ||
| 493 | name: "GitHub PAT Updated", | ||
| 494 | value: "ghp_test_token_v2", | ||
| 495 | }, | ||
| 496 | }), | ||
| 497 | ); | ||
| 498 | expect(updated.providerId).toBe("local"); | ||
| 499 | expect(updated.name).toBe("GitHub PAT Updated"); | ||
| 500 | |||
| 501 | const storedSecret = yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 502 | SecretMaterialIdSchema.make(created.id), | ||
| 503 | ); | ||
| 504 | assertTrue(Option.isSome(storedSecret)); | ||
| 505 | expect(storedSecret.value.providerId).toBe("local"); | ||
| 506 | expect(storedSecret.value.value).toBe("ghp_test_token_v2"); | ||
| 507 | |||
| 508 | const removed = yield* withControlPlaneClient( | ||
| 509 | { runtime }, | ||
| 510 | (client) => | ||
| 511 | client.local.deleteSecret({ | ||
| 512 | path: { secretId: created.id }, | ||
| 513 | }), | ||
| 514 | ); | ||
| 515 | expect(removed.removed).toBe(true); | ||
| 516 | }), | ||
| 517 | ); | ||
| 518 | |||
| 519 | it.scoped("reports safe secret storage options for the current platform", () => | ||
| 520 | Effect.gen(function* () { | ||
| 521 | const runtime = yield* makeRuntime; | ||
| 522 | |||
| 523 | const config = yield* withControlPlaneClient( | ||
| 524 | { runtime }, | ||
| 525 | (client) => client.local.config({}), | ||
| 526 | ); | ||
| 527 | |||
| 528 | const keychainProvider = | ||
| 529 | config.secretProviders.find((provider) => provider.id === "keychain") ?? null; | ||
| 530 | |||
| 531 | if (process.platform === "linux") { | ||
| 532 | expect(config.defaultSecretStoreProvider).toBe("local"); | ||
| 533 | expect(keychainProvider?.canStore).toBe(false); | ||
| 534 | return; | ||
| 535 | } | ||
| 536 | |||
| 537 | if (process.platform === "darwin") { | ||
| 538 | expect(keychainProvider?.canStore).toBe(true); | ||
| 539 | return; | ||
| 540 | } | ||
| 541 | |||
| 542 | expect(keychainProvider).toBeNull(); | ||
| 543 | }), | ||
| 544 | ); | ||
| 545 | |||
| 546 | it.scoped("captures credential requests through the local HTML flow without persisting raw tokens", () => | ||
| 547 | Effect.gen(function* () { | ||
| 548 | const runtime = yield* makeRuntime; | ||
| 549 | const installation = runtime.localInstallation; | ||
| 550 | const executionId = ExecutionIdSchema.make("exec_local_credential"); | ||
| 551 | const sourceId = SourceIdSchema.make("github"); | ||
| 552 | const interactionSuffix = "executor.sources.add:test"; | ||
| 553 | const interactionId = ExecutionInteractionIdSchema.make( | ||
| 554 | `${executionId}:${interactionSuffix}`, | ||
| 555 | ); | ||
| 556 | const now = Date.now(); | ||
| 557 | |||
| 558 | yield* runtime.persistence.rows.executions.insert({ | ||
| 559 | id: executionId, | ||
| 560 | workspaceId: installation.workspaceId, | ||
| 561 | createdByAccountId: installation.accountId, | ||
| 562 | status: "running", | ||
| 563 | code: "return await tools.executor.sources.add(...)", | ||
| 564 | resultJson: null, | ||
| 565 | errorText: null, | ||
| 566 | logsJson: null, | ||
| 567 | startedAt: now, | ||
| 568 | completedAt: null, | ||
| 569 | createdAt: now, | ||
| 570 | updatedAt: now, | ||
| 571 | }); | ||
| 572 | |||
| 573 | const localSource = yield* createSourceFromPayload({ | ||
| 574 | workspaceId: installation.workspaceId, | ||
| 575 | sourceId, | ||
| 576 | payload: { | ||
| 577 | name: "GitHub", | ||
| 578 | kind: "openapi", | ||
| 579 | endpoint: "https://api.github.com", | ||
| 580 | status: "auth_required", | ||
| 581 | enabled: true, | ||
| 582 | namespace: "github", | ||
| 583 | importAuthPolicy: "reuse_runtime", | ||
| 584 | binding: { | ||
| 585 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 586 | defaultHeaders: null, | ||
| 587 | }, | ||
| 588 | importAuth: { kind: "none" }, | ||
| 589 | auth: { kind: "none" }, | ||
| 590 | }, | ||
| 591 | now, | ||
| 592 | }).pipe(Effect.orDie); | ||
| 593 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 594 | actorAccountId: installation.accountId, | ||
| 595 | }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie); | ||
| 596 | |||
| 597 | const interactionFiber = yield* Effect.gen(function* () { | ||
| 598 | const liveExecutionManager = yield* LiveExecutionManagerService; | ||
| 599 | const onElicitation = liveExecutionManager.createOnElicitation({ | ||
| 600 | rows: runtime.persistence.rows, | ||
| 601 | executionId, | ||
| 602 | }); | ||
| 603 | |||
| 604 | return yield* onElicitation({ | ||
| 605 | interactionId: interactionSuffix, | ||
| 606 | path: "executor.sources.add" as ToolPath, | ||
| 607 | sourceKey: "executor", | ||
| 608 | args: { | ||
| 609 | kind: "openapi", | ||
| 610 | endpoint: "https://api.github.com", | ||
| 611 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 612 | name: "GitHub", | ||
| 613 | workspaceId: installation.workspaceId, | ||
| 614 | sourceId, | ||
| 615 | }, | ||
| 616 | elicitation: { | ||
| 617 | mode: "url", | ||
| 618 | message: "Open the secure credential page to connect GitHub", | ||
| 619 | url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`, | ||
| 620 | elicitationId: interactionSuffix, | ||
| 621 | }, | ||
| 622 | }); | ||
| 623 | }).pipe( | ||
| 624 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 625 | Effect.fork, | ||
| 626 | ); | ||
| 627 | |||
| 628 | const waitForPendingInteraction = ( | ||
| 629 | remaining: number, | ||
| 630 | ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> => | ||
| 631 | runtime.persistence.rows.executionInteractions.getPendingByExecutionId(executionId).pipe( | ||
| 632 | Effect.flatMap((pendingInteraction) => | ||
| 633 | Option.isSome(pendingInteraction) || remaining <= 0 | ||
| 634 | ? Effect.succeed(pendingInteraction) | ||
| 635 | : Effect.yieldNow().pipe( | ||
| 636 | Effect.zipRight(waitForPendingInteraction(remaining - 1)), | ||
| 637 | ), | ||
| 638 | ), | ||
| 639 | ); | ||
| 640 | |||
| 641 | const pendingInteraction = yield* waitForPendingInteraction(20); | ||
| 642 | assertTrue(Option.isSome(pendingInteraction)); | ||
| 643 | expect(pendingInteraction.value.id).toBe(interactionId); | ||
| 644 | |||
| 645 | const page = yield* withControlPlaneClient( | ||
| 646 | { runtime }, | ||
| 647 | (client) => | ||
| 648 | client.sources.credentialPage({ | ||
| 649 | path: { | ||
| 650 | workspaceId: installation.workspaceId, | ||
| 651 | sourceId, | ||
| 652 | }, | ||
| 653 | urlParams: { | ||
| 654 | interactionId: pendingInteraction.value.id, | ||
| 655 | }, | ||
| 656 | }), | ||
| 657 | ); | ||
| 658 | expect(page).toContain("Configure Source Access"); | ||
| 659 | expect(page).toContain("GitHub"); | ||
| 660 | expect(page).toContain("Continue without auth"); | ||
| 661 | |||
| 662 | const submittedPage = yield* withControlPlaneClient( | ||
| 663 | { runtime }, | ||
| 664 | (client) => | ||
| 665 | client.sources.credentialSubmit({ | ||
| 666 | path: { | ||
| 667 | workspaceId: installation.workspaceId, | ||
| 668 | sourceId, | ||
| 669 | }, | ||
| 670 | urlParams: { | ||
| 671 | interactionId: pendingInteraction.value.id, | ||
| 672 | }, | ||
| 673 | payload: { | ||
| 674 | action: "submit", | ||
| 675 | token: "ghp_local_test_token", | ||
| 676 | }, | ||
| 677 | }), | ||
| 678 | ); | ||
| 679 | expect(submittedPage).toContain("Credential Stored"); | ||
| 680 | |||
| 681 | const response = yield* Fiber.join(interactionFiber); | ||
| 682 | expect(response.action).toBe("accept"); | ||
| 683 | expect(response.content?.authKind).toBe("bearer"); | ||
| 684 | |||
| 685 | const decodedCredentialSelection = decodeSourceCredentialSelectionContent( | ||
| 686 | response.content, | ||
| 687 | ); | ||
| 688 | expect(decodedCredentialSelection.authKind).toBe("bearer"); | ||
| 689 | expect( | ||
| 690 | decodedCredentialSelection.authKind === "bearer" | ||
| 691 | ? decodedCredentialSelection.tokenRef.providerId | ||
| 692 | : null, | ||
| 693 | ).toBe("local"); | ||
| 694 | |||
| 695 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( | ||
| 696 | decodedCredentialSelection.authKind === "bearer" | ||
| 697 | ? decodedCredentialSelection.tokenRef.handle | ||
| 698 | : "", | ||
| 699 | ); | ||
| 700 | const storedSecret = yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 701 | tokenSecretMaterialId, | ||
| 702 | ); | ||
| 703 | assertTrue(Option.isSome(storedSecret)); | ||
| 704 | expect(storedSecret.value.value).toBe("ghp_local_test_token"); | ||
| 705 | |||
| 706 | const storedInteraction = yield* runtime.persistence.rows.executionInteractions.getById( | ||
| 707 | pendingInteraction.value.id, | ||
| 708 | ); | ||
| 709 | assertTrue(Option.isSome(storedInteraction)); | ||
| 710 | expect(storedInteraction.value.responseJson).toContain("\"authKind\":\"bearer\""); | ||
| 711 | expect(storedInteraction.value.responseJson).not.toContain("tokenRef"); | ||
| 712 | expect(storedInteraction.value.responseJson).not.toContain("ghp_local_test_token"); | ||
| 713 | }), | ||
| 714 | ); | ||
| 715 | |||
| 716 | it.scoped("loads a v1.2.3-style workspace artifact on startup", () => | ||
| 717 | Effect.gen(function* () { | ||
| 718 | const fs = yield* FileSystem.FileSystem; | ||
| 719 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 720 | prefix: "executor-control-plane-runtime-v123-", | ||
| 721 | }); | ||
| 722 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 723 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 724 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 725 | const configPath = join(configDirectory, "executor.jsonc"); | ||
| 726 | const sourceId = SourceIdSchema.make("graphql"); | ||
| 727 | const now = Date.now(); | ||
| 728 | |||
| 729 | yield* fs.makeDirectory(configDirectory, { recursive: true }); | ||
| 730 | yield* fs.writeFileString( | ||
| 731 | configPath, | ||
| 732 | `{ | ||
| 733 | "sources": { | ||
| 734 | "graphql": { | ||
| 735 | "kind": "graphql", | ||
| 736 | "name": "GraphQL API", | ||
| 737 | "namespace": "graphql", | ||
| 738 | "connection": { | ||
| 739 | "endpoint": "https://example.com/graphql" | ||
| 740 | }, | ||
| 741 | "binding": { | ||
| 742 | "defaultHeaders": null | ||
| 743 | } | ||
| 744 | } | ||
| 745 | } | ||
| 746 | } | ||
| 747 | `, | ||
| 748 | ); | ||
| 749 | |||
| 750 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 751 | workspaceRoot, | ||
| 752 | homeConfigPath, | ||
| 753 | homeStateDirectory, | ||
| 754 | }); | ||
| 755 | const installation = deriveLocalInstallation(context); | ||
| 756 | const source = yield* createSourceFromPayload({ | ||
| 757 | workspaceId: installation.workspaceId, | ||
| 758 | sourceId, | ||
| 759 | payload: { | ||
| 760 | name: "GraphQL API", | ||
| 761 | kind: "graphql", | ||
| 762 | endpoint: "https://example.com/graphql", | ||
| 763 | status: "connected", | ||
| 764 | enabled: true, | ||
| 765 | namespace: "graphql", | ||
| 766 | importAuthPolicy: "reuse_runtime", | ||
| 767 | binding: { | ||
| 768 | defaultHeaders: null, | ||
| 769 | }, | ||
| 770 | importAuth: { kind: "none" }, | ||
| 771 | auth: { kind: "none" }, | ||
| 772 | }, | ||
| 773 | now, | ||
| 774 | }).pipe(Effect.orDie); | ||
| 775 | |||
| 776 | const artifact = buildLocalSourceArtifact({ | ||
| 777 | source, | ||
| 778 | syncResult: { | ||
| 779 | fragment: createGraphqlCatalogFragment({ | ||
| 780 | source, | ||
| 781 | documents: [ | ||
| 782 | { | ||
| 783 | documentKind: "graphql_introspection", | ||
| 784 | documentKey: source.endpoint, | ||
| 785 | contentText: '{"__schema":{}}', | ||
| 786 | fetchedAt: now, | ||
| 787 | }, | ||
| 788 | ], | ||
| 789 | operations: [ | ||
| 790 | { | ||
| 791 | toolId: "viewer", | ||
| 792 | title: "Viewer", | ||
| 793 | description: "Load the current viewer", | ||
| 794 | effect: "read", | ||
| 795 | inputSchema: { type: "object", properties: {} }, | ||
| 796 | outputSchema: { | ||
| 797 | type: "object", | ||
| 798 | properties: { login: { type: "string" } }, | ||
| 799 | }, | ||
| 800 | providerData: { | ||
| 801 | kind: "graphql", | ||
| 802 | toolKind: "field", | ||
| 803 | toolId: "viewer", | ||
| 804 | rawToolId: "viewer", | ||
| 805 | group: "query", | ||
| 806 | leaf: "viewer", | ||
| 807 | fieldName: "viewer", | ||
| 808 | operationType: "query", | ||
| 809 | operationName: "ViewerQuery", | ||
| 810 | operationDocument: "query ViewerQuery { viewer { login } }", | ||
| 811 | queryTypeName: "Query", | ||
| 812 | mutationTypeName: null, | ||
| 813 | subscriptionTypeName: null, | ||
| 814 | }, | ||
| 815 | }, | ||
| 816 | ], | ||
| 817 | }), | ||
| 818 | importMetadata: createCatalogImportMetadata({ | ||
| 819 | source, | ||
| 820 | adapterKey: "graphql", | ||
| 821 | }), | ||
| 822 | sourceHash: source.sourceHash, | ||
| 823 | }, | ||
| 824 | }); | ||
| 825 | |||
| 826 | yield* writeLocalWorkspaceState({ | ||
| 827 | context, | ||
| 828 | state: { | ||
| 829 | version: 1, | ||
| 830 | sources: { | ||
| 831 | [sourceId]: { | ||
| 832 | status: source.status, | ||
| 833 | lastError: source.lastError, | ||
| 834 | sourceHash: source.sourceHash, | ||
| 835 | createdAt: source.createdAt, | ||
| 836 | updatedAt: source.updatedAt, | ||
| 837 | }, | ||
| 838 | }, | ||
| 839 | policies: {}, | ||
| 840 | }, | ||
| 841 | }); | ||
| 842 | yield* writeLocalControlPlaneState({ | ||
| 843 | context, | ||
| 844 | state: { | ||
| 845 | version: 1, | ||
| 846 | authArtifacts: [], | ||
| 847 | authLeases: [], | ||
| 848 | sourceOauthClients: [], | ||
| 849 | workspaceOauthClients: [], | ||
| 850 | providerAuthGrants: [], | ||
| 851 | sourceAuthSessions: [], | ||
| 852 | secretMaterials: [], | ||
| 853 | executions: [], | ||
| 854 | executionInteractions: [], | ||
| 855 | executionSteps: [], | ||
| 856 | }, | ||
| 857 | }); | ||
| 858 | yield* writeLocalSourceArtifact({ | ||
| 859 | context, | ||
| 860 | sourceId, | ||
| 861 | artifact, | ||
| 862 | }); | ||
| 863 | |||
| 864 | const artifactPath = join(context.artifactsDirectory, "sources", `${sourceId}.json`); | ||
| 865 | const legacyArtifact = JSON.parse( | ||
| 866 | yield* fs.readFileString(artifactPath, "utf8"), | ||
| 867 | ) as Record<string, unknown>; | ||
| 868 | legacyArtifact.version = 3; | ||
| 869 | yield* fs.writeFileString(artifactPath, `${JSON.stringify(legacyArtifact)}\n`); | ||
| 870 | |||
| 871 | const runtime = yield* Effect.acquireRelease( | ||
| 872 | createControlPlaneRuntime({ | ||
| 873 | workspaceRoot, | ||
| 874 | homeConfigPath, | ||
| 875 | homeStateDirectory, | ||
| 876 | }), | ||
| 877 | (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 878 | ); | ||
| 879 | |||
| 880 | const inspection = yield* withControlPlaneClient( | ||
| 881 | { runtime, accountId: installation.accountId }, | ||
| 882 | (client) => | ||
| 883 | client.sources.inspection({ | ||
| 884 | path: { | ||
| 885 | workspaceId: installation.workspaceId, | ||
| 886 | sourceId, | ||
| 887 | }, | ||
| 888 | }), | ||
| 889 | ); | ||
| 890 | |||
| 891 | expect(inspection.source.id).toBe(sourceId); | ||
| 892 | expect(inspection.source.kind).toBe("graphql"); | ||
| 893 | expect(inspection.toolCount).toBe(1); | ||
| 894 | expect(inspection.tools.map((tool) => tool.path)).toContain("graphql.viewer"); | ||
| 895 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 896 | ); | ||
| 897 | |||
| 898 | it.scoped("rebuilds missing connected source artifacts on startup", () => | ||
| 899 | Effect.gen(function* () { | ||
| 900 | const fs = yield* FileSystem.FileSystem; | ||
| 901 | const openApiServer = yield* makeOpenApiSpecServer; | ||
| 902 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 903 | prefix: "executor-control-plane-runtime-startup-rebuild-", | ||
| 904 | }); | ||
| 905 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 906 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 907 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 908 | workspaceRoot, | ||
| 909 | homeConfigPath, | ||
| 910 | homeStateDirectory, | ||
| 911 | }); | ||
| 912 | const installation = deriveLocalInstallation(context); | ||
| 913 | const sourceId = SourceIdSchema.make("github"); | ||
| 914 | const now = Date.now(); | ||
| 915 | const source = yield* createSourceFromPayload({ | ||
| 916 | workspaceId: installation.workspaceId, | ||
| 917 | sourceId, | ||
| 918 | payload: { | ||
| 919 | name: "GitHub", | ||
| 920 | kind: "openapi", | ||
| 921 | endpoint: openApiServer.baseUrl, | ||
| 922 | status: "connected", | ||
| 923 | enabled: true, | ||
| 924 | namespace: "github", | ||
| 925 | importAuthPolicy: "reuse_runtime", | ||
| 926 | binding: { | ||
| 927 | specUrl: openApiServer.specUrl, | ||
| 928 | defaultHeaders: null, | ||
| 929 | }, | ||
| 930 | importAuth: { kind: "none" }, | ||
| 931 | auth: { kind: "none" }, | ||
| 932 | }, | ||
| 933 | now, | ||
| 934 | }).pipe(Effect.orDie); | ||
| 935 | |||
| 936 | yield* writeProjectLocalExecutorConfig({ | ||
| 937 | context, | ||
| 938 | config: { | ||
| 939 | sources: { | ||
| 940 | [sourceId]: { | ||
| 941 | kind: "openapi", | ||
| 942 | name: "GitHub", | ||
| 943 | namespace: "github", | ||
| 944 | connection: { | ||
| 945 | endpoint: openApiServer.baseUrl, | ||
| 946 | }, | ||
| 947 | binding: { | ||
| 948 | specUrl: openApiServer.specUrl, | ||
| 949 | defaultHeaders: null, | ||
| 950 | }, | ||
| 951 | }, | ||
| 952 | }, | ||
| 953 | }, | ||
| 954 | }); | ||
| 955 | yield* writeLocalWorkspaceState({ | ||
| 956 | context, | ||
| 957 | state: { | ||
| 958 | version: 1, | ||
| 959 | sources: { | ||
| 960 | [sourceId]: { | ||
| 961 | status: source.status, | ||
| 962 | lastError: source.lastError, | ||
| 963 | sourceHash: source.sourceHash, | ||
| 964 | createdAt: source.createdAt, | ||
| 965 | updatedAt: source.updatedAt, | ||
| 966 | }, | ||
| 967 | }, | ||
| 968 | policies: {}, | ||
| 969 | }, | ||
| 970 | }); | ||
| 971 | |||
| 972 | const runtime = yield* Effect.acquireRelease( | ||
| 973 | createControlPlaneRuntime({ | ||
| 974 | workspaceRoot, | ||
| 975 | homeConfigPath, | ||
| 976 | homeStateDirectory, | ||
| 977 | }), | ||
| 978 | (createdRuntime) => Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 979 | ); | ||
| 980 | |||
| 981 | const rebuiltArtifact = yield* readLocalSourceArtifact({ | ||
| 982 | context, | ||
| 983 | sourceId, | ||
| 984 | }).pipe(Effect.provide(NodeFileSystem.layer)); | ||
| 985 | expect(rebuiltArtifact).not.toBeNull(); | ||
| 986 | |||
| 987 | const inspection = yield* withControlPlaneClient( | ||
| 988 | { runtime, accountId: installation.accountId }, | ||
| 989 | (client) => | ||
| 990 | client.sources.inspection({ | ||
| 991 | path: { | ||
| 992 | workspaceId: installation.workspaceId, | ||
| 993 | sourceId, | ||
| 994 | }, | ||
| 995 | }), | ||
| 996 | ); | ||
| 997 | expect(inspection.tools.length).toBeGreaterThan(0); | ||
| 998 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 999 | 60_000, | ||
| 1000 | ); | ||
| 1001 | |||
| 1002 | it.scoped("returns an empty inspection bundle for auth-required sources without a catalog artifact", () => | ||
| 1003 | Effect.gen(function* () { | ||
| 1004 | const runtime = yield* makeRuntime; | ||
| 1005 | const installation = runtime.localInstallation; | ||
| 1006 | const sourceId = SourceIdSchema.make("googleapis"); | ||
| 1007 | const now = Date.now(); | ||
| 1008 | |||
| 1009 | const localSource = yield* createSourceFromPayload({ | ||
| 1010 | workspaceId: installation.workspaceId, | ||
| 1011 | sourceId, | ||
| 1012 | payload: { | ||
| 1013 | name: "Google Drive", | ||
| 1014 | kind: "google_discovery", | ||
| 1015 | endpoint: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1016 | status: "auth_required", | ||
| 1017 | enabled: true, | ||
| 1018 | namespace: "googleapis", | ||
| 1019 | importAuthPolicy: "reuse_runtime", | ||
| 1020 | binding: { | ||
| 1021 | service: "drive", | ||
| 1022 | version: "v3", | ||
| 1023 | discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1024 | scopes: ["https://www.googleapis.com/auth/drive.readonly"], | ||
| 1025 | }, | ||
| 1026 | importAuth: { kind: "none" }, | ||
| 1027 | auth: { kind: "none" }, | ||
| 1028 | }, | ||
| 1029 | now, | ||
| 1030 | }).pipe(Effect.orDie); | ||
| 1031 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1032 | actorAccountId: installation.accountId, | ||
| 1033 | }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie); | ||
| 1034 | |||
| 1035 | const inspection = yield* withControlPlaneClient( | ||
| 1036 | { runtime, accountId: installation.accountId }, | ||
| 1037 | (client) => | ||
| 1038 | client.sources.inspection({ | ||
| 1039 | path: { | ||
| 1040 | workspaceId: installation.workspaceId, | ||
| 1041 | sourceId, | ||
| 1042 | }, | ||
| 1043 | }), | ||
| 1044 | ); | ||
| 1045 | |||
| 1046 | expect(inspection.source.id).toBe(sourceId); | ||
| 1047 | expect(inspection.source.status).toBe("auth_required"); | ||
| 1048 | expect(inspection.toolCount).toBe(0); | ||
| 1049 | expect(inspection.tools).toEqual([]); | ||
| 1050 | }), | ||
| 1051 | ); | ||
| 1052 | |||
| 1053 | it.scoped("still fails inspection when a connected source is missing its catalog artifact", () => | ||
| 1054 | Effect.gen(function* () { | ||
| 1055 | const runtime = yield* makeRuntime; | ||
| 1056 | const installation = runtime.localInstallation; | ||
| 1057 | const sourceId = SourceIdSchema.make("googleapis"); | ||
| 1058 | const now = Date.now(); | ||
| 1059 | |||
| 1060 | const localSource = yield* createSourceFromPayload({ | ||
| 1061 | workspaceId: installation.workspaceId, | ||
| 1062 | sourceId, | ||
| 1063 | payload: { | ||
| 1064 | name: "Google Drive", | ||
| 1065 | kind: "google_discovery", | ||
| 1066 | endpoint: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1067 | status: "connected", | ||
| 1068 | enabled: true, | ||
| 1069 | namespace: "googleapis", | ||
| 1070 | importAuthPolicy: "reuse_runtime", | ||
| 1071 | binding: { | ||
| 1072 | service: "drive", | ||
| 1073 | version: "v3", | ||
| 1074 | discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1075 | scopes: ["https://www.googleapis.com/auth/drive.readonly"], | ||
| 1076 | }, | ||
| 1077 | importAuth: { kind: "none" }, | ||
| 1078 | auth: { kind: "none" }, | ||
| 1079 | }, | ||
| 1080 | now, | ||
| 1081 | }).pipe(Effect.orDie); | ||
| 1082 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1083 | actorAccountId: installation.accountId, | ||
| 1084 | }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie); | ||
| 1085 | |||
| 1086 | const error = yield* expectLeft( | ||
| 1087 | withControlPlaneClient( | ||
| 1088 | { runtime, accountId: installation.accountId }, | ||
| 1089 | (client) => | ||
| 1090 | client.sources.inspection({ | ||
| 1091 | path: { | ||
| 1092 | workspaceId: installation.workspaceId, | ||
| 1093 | sourceId, | ||
| 1094 | }, | ||
| 1095 | }), | ||
| 1096 | ), | ||
| 1097 | ); | ||
| 1098 | |||
| 1099 | expect(error.message).toContain("Catalog artifact missing for source googleapis"); | ||
| 1100 | }), | ||
| 1101 | ); | ||
| 1102 | |||
| 1103 | it.scoped("allows continuing an OpenAPI source credential request without auth", () => | ||
| 1104 | Effect.gen(function* () { | ||
| 1105 | const runtime = yield* makeRuntime; | ||
| 1106 | const installation = runtime.localInstallation; | ||
| 1107 | const executionId = ExecutionIdSchema.make("exec_local_credential_continue"); | ||
| 1108 | const sourceId = SourceIdSchema.make("github"); | ||
| 1109 | const interactionSuffix = "executor.sources.add:continue"; | ||
| 1110 | const interactionId = ExecutionInteractionIdSchema.make( | ||
| 1111 | `${executionId}:${interactionSuffix}`, | ||
| 1112 | ); | ||
| 1113 | const now = Date.now(); | ||
| 1114 | |||
| 1115 | yield* runtime.persistence.rows.executions.insert({ | ||
| 1116 | id: executionId, | ||
| 1117 | workspaceId: installation.workspaceId, | ||
| 1118 | createdByAccountId: installation.accountId, | ||
| 1119 | status: "running", | ||
| 1120 | code: "return await tools.executor.sources.add(...)", | ||
| 1121 | resultJson: null, | ||
| 1122 | errorText: null, | ||
| 1123 | logsJson: null, | ||
| 1124 | startedAt: now, | ||
| 1125 | completedAt: null, | ||
| 1126 | createdAt: now, | ||
| 1127 | updatedAt: now, | ||
| 1128 | }); | ||
| 1129 | |||
| 1130 | const localSource = yield* createSourceFromPayload({ | ||
| 1131 | workspaceId: installation.workspaceId, | ||
| 1132 | sourceId, | ||
| 1133 | payload: { | ||
| 1134 | name: "GitHub", | ||
| 1135 | kind: "openapi", | ||
| 1136 | endpoint: "https://api.github.com", | ||
| 1137 | status: "auth_required", | ||
| 1138 | enabled: true, | ||
| 1139 | namespace: "github", | ||
| 1140 | importAuthPolicy: "reuse_runtime", | ||
| 1141 | binding: { | ||
| 1142 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 1143 | defaultHeaders: null, | ||
| 1144 | }, | ||
| 1145 | importAuth: { kind: "none" }, | ||
| 1146 | auth: { kind: "none" }, | ||
| 1147 | }, | ||
| 1148 | now, | ||
| 1149 | }).pipe(Effect.orDie); | ||
| 1150 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1151 | actorAccountId: installation.accountId, | ||
| 1152 | }).pipe((effect) => provideControlPlaneRuntime(effect, runtime), Effect.orDie); | ||
| 1153 | |||
| 1154 | const interactionFiber = yield* Effect.gen(function* () { | ||
| 1155 | const liveExecutionManager = yield* LiveExecutionManagerService; | ||
| 1156 | const onElicitation = liveExecutionManager.createOnElicitation({ | ||
| 1157 | rows: runtime.persistence.rows, | ||
| 1158 | executionId, | ||
| 1159 | }); | ||
| 1160 | |||
| 1161 | return yield* onElicitation({ | ||
| 1162 | interactionId: interactionSuffix, | ||
| 1163 | path: "executor.sources.add" as ToolPath, | ||
| 1164 | sourceKey: "executor", | ||
| 1165 | args: { | ||
| 1166 | kind: "openapi", | ||
| 1167 | endpoint: "https://api.github.com", | ||
| 1168 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 1169 | name: "GitHub", | ||
| 1170 | workspaceId: installation.workspaceId, | ||
| 1171 | sourceId, | ||
| 1172 | }, | ||
| 1173 | elicitation: { | ||
| 1174 | mode: "url", | ||
| 1175 | message: "Open the secure credential page to connect GitHub", | ||
| 1176 | url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`, | ||
| 1177 | elicitationId: interactionSuffix, | ||
| 1178 | }, | ||
| 1179 | }); | ||
| 1180 | }).pipe( | ||
| 1181 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 1182 | Effect.fork, | ||
| 1183 | ); | ||
| 1184 | |||
| 1185 | const waitForPendingInteraction = ( | ||
| 1186 | remaining: number, | ||
| 1187 | ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> => | ||
| 1188 | runtime.persistence.rows.executionInteractions.getPendingByExecutionId(executionId).pipe( | ||
| 1189 | Effect.flatMap((pendingInteraction) => | ||
| 1190 | Option.isSome(pendingInteraction) || remaining <= 0 | ||
| 1191 | ? Effect.succeed(pendingInteraction) | ||
| 1192 | : Effect.yieldNow().pipe( | ||
| 1193 | Effect.zipRight(waitForPendingInteraction(remaining - 1)), | ||
| 1194 | ), | ||
| 1195 | ), | ||
| 1196 | ); | ||
| 1197 | |||
| 1198 | const pendingInteraction = yield* waitForPendingInteraction(20); | ||
| 1199 | assertTrue(Option.isSome(pendingInteraction)); | ||
| 1200 | |||
| 1201 | const submittedPage = yield* withControlPlaneClient( | ||
| 1202 | { runtime }, | ||
| 1203 | (client) => | ||
| 1204 | client.sources.credentialSubmit({ | ||
| 1205 | path: { | ||
| 1206 | workspaceId: installation.workspaceId, | ||
| 1207 | sourceId, | ||
| 1208 | }, | ||
| 1209 | urlParams: { | ||
| 1210 | interactionId: pendingInteraction.value.id, | ||
| 1211 | }, | ||
| 1212 | payload: { | ||
| 1213 | action: "continue", | ||
| 1214 | }, | ||
| 1215 | }), | ||
| 1216 | ); | ||
| 1217 | |||
| 1218 | expect(submittedPage).toContain("Continuing without auth"); | ||
| 1219 | |||
| 1220 | const response = yield* Fiber.join(interactionFiber); | ||
| 1221 | expect(response.action).toBe("accept"); | ||
| 1222 | expect(response.content).toEqual({ | ||
| 1223 | authKind: "none", | ||
| 1224 | }); | ||
| 1225 | |||
| 1226 | const storedInteraction = yield* runtime.persistence.rows.executionInteractions.getById( | ||
| 1227 | pendingInteraction.value.id, | ||
| 1228 | ); | ||
| 1229 | assertTrue(Option.isSome(storedInteraction)); | ||
| 1230 | expect(storedInteraction.value.responseJson).toContain("\"authKind\":\"none\""); | ||
| 1231 | expect(storedInteraction.value.responseJson).not.toContain("tokenRef"); | ||
| 1232 | }), | ||
| 1233 | ); | ||
| 1234 | |||
| 1235 | it.scoped("reuses existing Google provider grants for single connects and clears orphan state", () => | ||
| 1236 | Effect.gen(function* () { | ||
| 1237 | const runtime = yield* makeRuntime; | ||
| 1238 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1239 | const installation = runtime.localInstallation; | ||
| 1240 | |||
| 1241 | const oauthClient = yield* withControlPlaneClient( | ||
| 1242 | { runtime }, | ||
| 1243 | (client) => | ||
| 1244 | client.sources.createWorkspaceOauthClient({ | ||
| 1245 | path: { | ||
| 1246 | workspaceId: installation.workspaceId, | ||
| 1247 | }, | ||
| 1248 | payload: { | ||
| 1249 | providerKey: "google_workspace", | ||
| 1250 | label: "Local Google Workspace Client", | ||
| 1251 | oauthClient: { | ||
| 1252 | clientId: "google-client-id", | ||
| 1253 | clientSecret: "google-client-secret", | ||
| 1254 | }, | ||
| 1255 | }, | ||
| 1256 | }), | ||
| 1257 | ); | ||
| 1258 | |||
| 1259 | const refreshSecretId = "sec_google_refresh_single_reuse"; | ||
| 1260 | yield* upsertLocalSecret(runtime, { | ||
| 1261 | id: refreshSecretId, | ||
| 1262 | name: "Google Refresh Token", | ||
| 1263 | purpose: "oauth_refresh_token", | ||
| 1264 | value: "refresh-token-reuse", | ||
| 1265 | }); | ||
| 1266 | |||
| 1267 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1268 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1269 | id: grantId, | ||
| 1270 | workspaceId: installation.workspaceId, | ||
| 1271 | actorAccountId: installation.accountId, | ||
| 1272 | providerKey: "google_workspace", | ||
| 1273 | oauthClientId: oauthClient.id, | ||
| 1274 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1275 | clientAuthentication: "client_secret_post", | ||
| 1276 | headerName: "Authorization", | ||
| 1277 | prefix: "Bearer ", | ||
| 1278 | refreshToken: { | ||
| 1279 | providerId: "local", | ||
| 1280 | handle: refreshSecretId, | ||
| 1281 | }, | ||
| 1282 | grantedScopes: [ | ||
| 1283 | "scope:drive.readonly", | ||
| 1284 | ], | ||
| 1285 | lastRefreshedAt: null, | ||
| 1286 | orphanedAt: 123, | ||
| 1287 | createdAt: Date.now(), | ||
| 1288 | updatedAt: Date.now(), | ||
| 1289 | }); | ||
| 1290 | |||
| 1291 | googleServer.queueTokenResponse({ | ||
| 1292 | access_token: "reuse-access-token", | ||
| 1293 | scope: "scope:drive.readonly", | ||
| 1294 | }); | ||
| 1295 | |||
| 1296 | const result = yield* withControlPlaneClient( | ||
| 1297 | { runtime }, | ||
| 1298 | (client) => | ||
| 1299 | client.sources.connect({ | ||
| 1300 | path: { | ||
| 1301 | workspaceId: installation.workspaceId, | ||
| 1302 | }, | ||
| 1303 | payload: { | ||
| 1304 | kind: "google_discovery", | ||
| 1305 | workspaceOauthClientId: oauthClient.id, | ||
| 1306 | service: "drive", | ||
| 1307 | version: "v3", | ||
| 1308 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1309 | service: "drive", | ||
| 1310 | version: "v3", | ||
| 1311 | scope: "scope:drive.readonly", | ||
| 1312 | }), | ||
| 1313 | scopes: ["scope:drive.readonly"], | ||
| 1314 | name: "Drive", | ||
| 1315 | namespace: "google.drive", | ||
| 1316 | }, | ||
| 1317 | }), | ||
| 1318 | ); | ||
| 1319 | |||
| 1320 | expect(result.kind).toBe("connected"); | ||
| 1321 | if (result.kind !== "connected") { | ||
| 1322 | throw new Error(`Expected connected result, received ${result.kind}`); | ||
| 1323 | } | ||
| 1324 | expect(result.source.auth).toEqual({ | ||
| 1325 | kind: "provider_grant_ref", | ||
| 1326 | grantId, | ||
| 1327 | providerKey: "google_workspace", | ||
| 1328 | requiredScopes: ["scope:drive.readonly"], | ||
| 1329 | headerName: "Authorization", | ||
| 1330 | prefix: "Bearer ", | ||
| 1331 | }); | ||
| 1332 | |||
| 1333 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1334 | assertTrue(Option.isSome(storedGrant)); | ||
| 1335 | expect(storedGrant.value.orphanedAt).toBeNull(); | ||
| 1336 | expect(googleServer.discoveryAuthorizations).toContain("Bearer reuse-access-token"); | ||
| 1337 | }), | ||
| 1338 | 60_000, | ||
| 1339 | ); | ||
| 1340 | |||
| 1341 | it.scoped("uses the app callback redirect for browser-started Google single-source OAuth", () => | ||
| 1342 | Effect.gen(function* () { | ||
| 1343 | const runtime = yield* makeRuntime; | ||
| 1344 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1345 | const installation = runtime.localInstallation; | ||
| 1346 | |||
| 1347 | const oauthClient = yield* withControlPlaneClient( | ||
| 1348 | { runtime }, | ||
| 1349 | (client) => | ||
| 1350 | client.sources.createWorkspaceOauthClient({ | ||
| 1351 | path: { | ||
| 1352 | workspaceId: installation.workspaceId, | ||
| 1353 | }, | ||
| 1354 | payload: { | ||
| 1355 | providerKey: "google_workspace", | ||
| 1356 | label: "Local Google Workspace Client", | ||
| 1357 | oauthClient: { | ||
| 1358 | clientId: "google-client-id", | ||
| 1359 | clientSecret: "google-client-secret", | ||
| 1360 | }, | ||
| 1361 | }, | ||
| 1362 | }), | ||
| 1363 | ); | ||
| 1364 | |||
| 1365 | const result = yield* withControlPlaneClient( | ||
| 1366 | { runtime }, | ||
| 1367 | (client) => | ||
| 1368 | client.sources.connect({ | ||
| 1369 | path: { | ||
| 1370 | workspaceId: installation.workspaceId, | ||
| 1371 | }, | ||
| 1372 | payload: { | ||
| 1373 | kind: "google_discovery", | ||
| 1374 | workspaceOauthClientId: oauthClient.id, | ||
| 1375 | service: "gmail", | ||
| 1376 | version: "v1", | ||
| 1377 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1378 | service: "gmail", | ||
| 1379 | version: "v1", | ||
| 1380 | scope: "scope:gmail.readonly", | ||
| 1381 | }), | ||
| 1382 | scopes: ["scope:gmail.readonly"], | ||
| 1383 | name: "Gmail", | ||
| 1384 | namespace: "google.gmail", | ||
| 1385 | }, | ||
| 1386 | }), | ||
| 1387 | ); | ||
| 1388 | |||
| 1389 | expect(result.kind).toBe("oauth_required"); | ||
| 1390 | if (result.kind !== "oauth_required") { | ||
| 1391 | throw new Error(`Expected oauth_required result, received ${result.kind}`); | ||
| 1392 | } | ||
| 1393 | |||
| 1394 | const redirectUri = new URL(result.authorizationUrl).searchParams.get("redirect_uri"); | ||
| 1395 | expect(redirectUri).not.toBeNull(); | ||
| 1396 | expect(new URL(redirectUri!).pathname).toBe( | ||
| 1397 | `/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/oauth/provider/callback`, | ||
| 1398 | ); | ||
| 1399 | expect(result.source.status).toBe("auth_required"); | ||
| 1400 | }), | ||
| 1401 | 60_000, | ||
| 1402 | ); | ||
| 1403 | |||
| 1404 | it.scoped("reuses existing Google provider grants for batch connects and clears orphan state", () => | ||
| 1405 | Effect.gen(function* () { | ||
| 1406 | const runtime = yield* makeRuntime; | ||
| 1407 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1408 | const installation = runtime.localInstallation; | ||
| 1409 | |||
| 1410 | const oauthClient = yield* withControlPlaneClient( | ||
| 1411 | { runtime }, | ||
| 1412 | (client) => | ||
| 1413 | client.sources.createWorkspaceOauthClient({ | ||
| 1414 | path: { | ||
| 1415 | workspaceId: installation.workspaceId, | ||
| 1416 | }, | ||
| 1417 | payload: { | ||
| 1418 | providerKey: "google_workspace", | ||
| 1419 | label: "Local Google Workspace Client", | ||
| 1420 | oauthClient: { | ||
| 1421 | clientId: "google-client-id", | ||
| 1422 | clientSecret: "google-client-secret", | ||
| 1423 | }, | ||
| 1424 | }, | ||
| 1425 | }), | ||
| 1426 | ); | ||
| 1427 | |||
| 1428 | const refreshSecretId = "sec_google_refresh_reuse"; | ||
| 1429 | yield* upsertLocalSecret(runtime, { | ||
| 1430 | id: refreshSecretId, | ||
| 1431 | name: "Google Refresh Token", | ||
| 1432 | purpose: "oauth_refresh_token", | ||
| 1433 | value: "refresh-token-reuse", | ||
| 1434 | }); | ||
| 1435 | |||
| 1436 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1437 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1438 | id: grantId, | ||
| 1439 | workspaceId: installation.workspaceId, | ||
| 1440 | actorAccountId: installation.accountId, | ||
| 1441 | providerKey: "google_workspace", | ||
| 1442 | oauthClientId: oauthClient.id, | ||
| 1443 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1444 | clientAuthentication: "client_secret_post", | ||
| 1445 | headerName: "Authorization", | ||
| 1446 | prefix: "Bearer ", | ||
| 1447 | refreshToken: { | ||
| 1448 | providerId: "local", | ||
| 1449 | handle: refreshSecretId, | ||
| 1450 | }, | ||
| 1451 | grantedScopes: [ | ||
| 1452 | "scope:gmail.readonly", | ||
| 1453 | "scope:calendar.readonly", | ||
| 1454 | ], | ||
| 1455 | lastRefreshedAt: null, | ||
| 1456 | orphanedAt: 123, | ||
| 1457 | createdAt: Date.now(), | ||
| 1458 | updatedAt: Date.now(), | ||
| 1459 | }); | ||
| 1460 | |||
| 1461 | googleServer.queueTokenResponse({ | ||
| 1462 | access_token: "reuse-access-token", | ||
| 1463 | scope: "scope:gmail.readonly scope:calendar.readonly", | ||
| 1464 | }); | ||
| 1465 | googleServer.queueTokenResponse({ | ||
| 1466 | access_token: "reuse-access-token", | ||
| 1467 | scope: "scope:gmail.readonly scope:calendar.readonly", | ||
| 1468 | }); | ||
| 1469 | |||
| 1470 | const result = yield* withControlPlaneClient( | ||
| 1471 | { runtime }, | ||
| 1472 | (client) => | ||
| 1473 | client.sources.connectBatch({ | ||
| 1474 | path: { | ||
| 1475 | workspaceId: installation.workspaceId, | ||
| 1476 | }, | ||
| 1477 | payload: { | ||
| 1478 | workspaceOauthClientId: oauthClient.id, | ||
| 1479 | sources: [ | ||
| 1480 | { | ||
| 1481 | service: "gmail", | ||
| 1482 | version: "v1", | ||
| 1483 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1484 | service: "gmail", | ||
| 1485 | version: "v1", | ||
| 1486 | scope: "scope:gmail.readonly", | ||
| 1487 | }), | ||
| 1488 | scopes: ["scope:gmail.readonly"], | ||
| 1489 | name: "Gmail", | ||
| 1490 | namespace: "google.gmail", | ||
| 1491 | }, | ||
| 1492 | { | ||
| 1493 | service: "calendar", | ||
| 1494 | version: "v3", | ||
| 1495 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1496 | service: "calendar", | ||
| 1497 | version: "v3", | ||
| 1498 | scope: "scope:calendar.readonly", | ||
| 1499 | }), | ||
| 1500 | scopes: ["scope:calendar.readonly"], | ||
| 1501 | name: "Calendar", | ||
| 1502 | namespace: "google.calendar", | ||
| 1503 | }, | ||
| 1504 | ], | ||
| 1505 | }, | ||
| 1506 | }), | ||
| 1507 | ); | ||
| 1508 | |||
| 1509 | expect(result.providerOauthSession).toBeNull(); | ||
| 1510 | expect(result.results).toHaveLength(2); | ||
| 1511 | expect(result.results.every((entry) => entry.status === "connected")).toBe(true); | ||
| 1512 | expect(result.results.every((entry) => entry.source.auth.kind === "provider_grant_ref")).toBe(true); | ||
| 1513 | |||
| 1514 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1515 | assertTrue(Option.isSome(storedGrant)); | ||
| 1516 | expect(storedGrant.value.orphanedAt).toBeNull(); | ||
| 1517 | expect(googleServer.tokenRequests.length).toBeGreaterThanOrEqual(1); | ||
| 1518 | expect( | ||
| 1519 | googleServer.discoveryAuthorizations.every( | ||
| 1520 | (authorization) => authorization === "Bearer reuse-access-token", | ||
| 1521 | ), | ||
| 1522 | ).toBe(true); | ||
| 1523 | }), | ||
| 1524 | 60_000, | ||
| 1525 | ); | ||
| 1526 | |||
| 1527 | it.scoped("uses the app callback redirect for browser-started Google batch OAuth", () => | ||
| 1528 | Effect.gen(function* () { | ||
| 1529 | const runtime = yield* makeRuntime; | ||
| 1530 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1531 | const installation = runtime.localInstallation; | ||
| 1532 | |||
| 1533 | const oauthClient = yield* withControlPlaneClient( | ||
| 1534 | { runtime }, | ||
| 1535 | (client) => | ||
| 1536 | client.sources.createWorkspaceOauthClient({ | ||
| 1537 | path: { | ||
| 1538 | workspaceId: installation.workspaceId, | ||
| 1539 | }, | ||
| 1540 | payload: { | ||
| 1541 | providerKey: "google_workspace", | ||
| 1542 | label: "Local Google Workspace Client", | ||
| 1543 | oauthClient: { | ||
| 1544 | clientId: "google-client-id", | ||
| 1545 | clientSecret: "google-client-secret", | ||
| 1546 | }, | ||
| 1547 | }, | ||
| 1548 | }), | ||
| 1549 | ); | ||
| 1550 | |||
| 1551 | const result = yield* withControlPlaneClient( | ||
| 1552 | { runtime }, | ||
| 1553 | (client) => | ||
| 1554 | client.sources.connectBatch({ | ||
| 1555 | path: { | ||
| 1556 | workspaceId: installation.workspaceId, | ||
| 1557 | }, | ||
| 1558 | payload: { | ||
| 1559 | workspaceOauthClientId: oauthClient.id, | ||
| 1560 | sources: [{ | ||
| 1561 | service: "gmail", | ||
| 1562 | version: "v1", | ||
| 1563 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1564 | service: "gmail", | ||
| 1565 | version: "v1", | ||
| 1566 | scope: "scope:gmail.readonly", | ||
| 1567 | }), | ||
| 1568 | scopes: ["scope:gmail.readonly"], | ||
| 1569 | name: "Gmail", | ||
| 1570 | namespace: "google.gmail", | ||
| 1571 | }], | ||
| 1572 | }, | ||
| 1573 | }), | ||
| 1574 | ); | ||
| 1575 | |||
| 1576 | expect(result.providerOauthSession).not.toBeNull(); | ||
| 1577 | const redirectUri = new URL( | ||
| 1578 | result.providerOauthSession!.authorizationUrl, | ||
| 1579 | ).searchParams.get("redirect_uri"); | ||
| 1580 | |||
| 1581 | expect(redirectUri).not.toBeNull(); | ||
| 1582 | expect(new URL(redirectUri!).pathname).toBe( | ||
| 1583 | `/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/oauth/provider/callback`, | ||
| 1584 | ); | ||
| 1585 | }), | ||
| 1586 | 60_000, | ||
| 1587 | ); | ||
| 1588 | |||
| 1589 | it.scoped("preserves the existing provider refresh token when callback expansion omits refresh_token", () => | ||
| 1590 | Effect.gen(function* () { | ||
| 1591 | const runtime = yield* makeRuntime; | ||
| 1592 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1593 | const installation = runtime.localInstallation; | ||
| 1594 | |||
| 1595 | const oauthClient = yield* withControlPlaneClient( | ||
| 1596 | { runtime }, | ||
| 1597 | (client) => | ||
| 1598 | client.sources.createWorkspaceOauthClient({ | ||
| 1599 | path: { | ||
| 1600 | workspaceId: installation.workspaceId, | ||
| 1601 | }, | ||
| 1602 | payload: { | ||
| 1603 | providerKey: "google_workspace", | ||
| 1604 | label: "Local Google Workspace Client", | ||
| 1605 | oauthClient: { | ||
| 1606 | clientId: "google-client-id", | ||
| 1607 | clientSecret: "google-client-secret", | ||
| 1608 | }, | ||
| 1609 | }, | ||
| 1610 | }), | ||
| 1611 | ); | ||
| 1612 | |||
| 1613 | const preservedRefreshSecretId = "sec_google_refresh_preserved"; | ||
| 1614 | yield* upsertLocalSecret(runtime, { | ||
| 1615 | id: preservedRefreshSecretId, | ||
| 1616 | name: "Preserved Google Refresh Token", | ||
| 1617 | purpose: "oauth_refresh_token", | ||
| 1618 | value: "refresh-token-preserved", | ||
| 1619 | }); | ||
| 1620 | |||
| 1621 | const source = yield* createPersistedGoogleSource({ | ||
| 1622 | runtime, | ||
| 1623 | name: "Drive", | ||
| 1624 | namespace: "google.drive", | ||
| 1625 | service: "drive", | ||
| 1626 | version: "v3", | ||
| 1627 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1628 | service: "drive", | ||
| 1629 | version: "v3", | ||
| 1630 | scope: "scope:drive.readonly", | ||
| 1631 | }), | ||
| 1632 | scopes: ["scope:drive.readonly"], | ||
| 1633 | auth: { | ||
| 1634 | kind: "none", | ||
| 1635 | }, | ||
| 1636 | }); | ||
| 1637 | |||
| 1638 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1639 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1640 | id: grantId, | ||
| 1641 | workspaceId: installation.workspaceId, | ||
| 1642 | actorAccountId: installation.accountId, | ||
| 1643 | providerKey: "google_workspace", | ||
| 1644 | oauthClientId: oauthClient.id, | ||
| 1645 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1646 | clientAuthentication: "client_secret_post", | ||
| 1647 | headerName: "Authorization", | ||
| 1648 | prefix: "Bearer ", | ||
| 1649 | refreshToken: { | ||
| 1650 | providerId: "local", | ||
| 1651 | handle: preservedRefreshSecretId, | ||
| 1652 | }, | ||
| 1653 | grantedScopes: [ | ||
| 1654 | "scope:gmail.readonly", | ||
| 1655 | ], | ||
| 1656 | lastRefreshedAt: null, | ||
| 1657 | orphanedAt: null, | ||
| 1658 | createdAt: Date.now(), | ||
| 1659 | updatedAt: Date.now(), | ||
| 1660 | }); | ||
| 1661 | |||
| 1662 | const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`); | ||
| 1663 | const state = `provider-state-${crypto.randomUUID()}`; | ||
| 1664 | yield* runtime.persistence.rows.sourceAuthSessions.upsert({ | ||
| 1665 | id: sessionId, | ||
| 1666 | workspaceId: installation.workspaceId, | ||
| 1667 | sourceId: SourceIdSchema.make(`oauth_provider_${crypto.randomUUID()}`), | ||
| 1668 | actorAccountId: installation.accountId, | ||
| 1669 | credentialSlot: "runtime", | ||
| 1670 | executionId: null, | ||
| 1671 | interactionId: null, | ||
| 1672 | providerKind: "oauth2_provider_batch", | ||
| 1673 | status: "pending", | ||
| 1674 | state, | ||
| 1675 | sessionDataJson: JSON.stringify({ | ||
| 1676 | kind: "provider_oauth_batch", | ||
| 1677 | providerKey: "google_workspace", | ||
| 1678 | authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth", | ||
| 1679 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1680 | redirectUri: `${googleServer.baseUrl}/oauth/callback`, | ||
| 1681 | oauthClientId: oauthClient.id, | ||
| 1682 | clientAuthentication: "client_secret_post", | ||
| 1683 | scopes: [ | ||
| 1684 | "scope:gmail.readonly", | ||
| 1685 | "scope:drive.readonly", | ||
| 1686 | ], | ||
| 1687 | headerName: "Authorization", | ||
| 1688 | prefix: "Bearer ", | ||
| 1689 | authorizationParams: { | ||
| 1690 | access_type: "offline", | ||
| 1691 | prompt: "consent", | ||
| 1692 | include_granted_scopes: "true", | ||
| 1693 | }, | ||
| 1694 | targetSources: [{ | ||
| 1695 | sourceId: source.id, | ||
| 1696 | requiredScopes: ["scope:drive.readonly"], | ||
| 1697 | }], | ||
| 1698 | codeVerifier: "pkce-verifier", | ||
| 1699 | authorizationUrl: `https://accounts.google.com/o/oauth2/v2/auth?state=${encodeURIComponent(state)}`, | ||
| 1700 | }), | ||
| 1701 | errorText: null, | ||
| 1702 | completedAt: null, | ||
| 1703 | createdAt: Date.now(), | ||
| 1704 | updatedAt: Date.now(), | ||
| 1705 | }); | ||
| 1706 | |||
| 1707 | googleServer.queueTokenResponse({ | ||
| 1708 | access_token: "authorization-code-access-token", | ||
| 1709 | scope: "scope:gmail.readonly scope:drive.readonly", | ||
| 1710 | }); | ||
| 1711 | googleServer.queueTokenResponse({ | ||
| 1712 | access_token: "post-callback-sync-access-token", | ||
| 1713 | scope: "scope:gmail.readonly scope:drive.readonly", | ||
| 1714 | }); | ||
| 1715 | |||
| 1716 | const callbackPage = yield* withControlPlaneClient( | ||
| 1717 | { runtime }, | ||
| 1718 | (client) => | ||
| 1719 | client.sources.providerOauthComplete({ | ||
| 1720 | path: { | ||
| 1721 | workspaceId: installation.workspaceId, | ||
| 1722 | }, | ||
| 1723 | urlParams: { | ||
| 1724 | state, | ||
| 1725 | code: "authorization-code", | ||
| 1726 | }, | ||
| 1727 | }), | ||
| 1728 | ); | ||
| 1729 | |||
| 1730 | expect(callbackPage).toContain("Connected 1 source"); | ||
| 1731 | |||
| 1732 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1733 | assertTrue(Option.isSome(storedGrant)); | ||
| 1734 | expect(storedGrant.value.refreshToken).toEqual({ | ||
| 1735 | providerId: "local", | ||
| 1736 | handle: preservedRefreshSecretId, | ||
| 1737 | }); | ||
| 1738 | expect(storedGrant.value.grantedScopes).toEqual([ | ||
| 1739 | "scope:gmail.readonly", | ||
| 1740 | "scope:drive.readonly", | ||
| 1741 | ]); | ||
| 1742 | |||
| 1743 | const refreshedSource = yield* withControlPlaneClient( | ||
| 1744 | { runtime }, | ||
| 1745 | (client) => | ||
| 1746 | client.sources.get({ | ||
| 1747 | path: { | ||
| 1748 | workspaceId: installation.workspaceId, | ||
| 1749 | sourceId: source.id, | ||
| 1750 | }, | ||
| 1751 | }), | ||
| 1752 | ); | ||
| 1753 | |||
| 1754 | expect(refreshedSource.status).toBe("connected"); | ||
| 1755 | expect(refreshedSource.auth).toEqual({ | ||
| 1756 | kind: "provider_grant_ref", | ||
| 1757 | grantId, | ||
| 1758 | providerKey: "google_workspace", | ||
| 1759 | requiredScopes: ["scope:drive.readonly"], | ||
| 1760 | headerName: "Authorization", | ||
| 1761 | prefix: "Bearer ", | ||
| 1762 | }); | ||
| 1763 | |||
| 1764 | expect(googleServer.tokenRequests.map((request) => request.get("grant_type"))).toEqual([ | ||
| 1765 | "authorization_code", | ||
| 1766 | "refresh_token", | ||
| 1767 | ]); | ||
| 1768 | expect(googleServer.tokenRequests[1]?.get("refresh_token")).toBe("refresh-token-preserved"); | ||
| 1769 | expect(googleServer.discoveryAuthorizations).toContain("Bearer post-callback-sync-access-token"); | ||
| 1770 | }), | ||
| 1771 | 60_000, | ||
| 1772 | ); | ||
| 1773 | |||
| 1774 | it.scoped("marks provider grants as orphaned when the last referencing source is removed", () => | ||
| 1775 | Effect.gen(function* () { | ||
| 1776 | const runtime = yield* makeRuntime; | ||
| 1777 | const installation = runtime.localInstallation; | ||
| 1778 | const oauthClientId = WorkspaceOauthClientIdSchema.make(`ws_oauth_client_${crypto.randomUUID()}`); | ||
| 1779 | const refreshSecretId = "sec_google_refresh_orphan"; | ||
| 1780 | |||
| 1781 | yield* runtime.persistence.rows.workspaceOauthClients.upsert({ | ||
| 1782 | id: oauthClientId, | ||
| 1783 | workspaceId: installation.workspaceId, | ||
| 1784 | providerKey: "google_workspace", | ||
| 1785 | label: "Local Google Workspace Client", | ||
| 1786 | clientId: "google-client-id", | ||
| 1787 | clientSecretProviderId: null, | ||
| 1788 | clientSecretHandle: null, | ||
| 1789 | clientMetadataJson: null, | ||
| 1790 | createdAt: Date.now(), | ||
| 1791 | updatedAt: Date.now(), | ||
| 1792 | }); | ||
| 1793 | yield* upsertLocalSecret(runtime, { | ||
| 1794 | id: refreshSecretId, | ||
| 1795 | name: "Google Refresh Token", | ||
| 1796 | purpose: "oauth_refresh_token", | ||
| 1797 | value: "refresh-token-orphan", | ||
| 1798 | }); | ||
| 1799 | |||
| 1800 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1801 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1802 | id: grantId, | ||
| 1803 | workspaceId: installation.workspaceId, | ||
| 1804 | actorAccountId: installation.accountId, | ||
| 1805 | providerKey: "google_workspace", | ||
| 1806 | oauthClientId, | ||
| 1807 | tokenEndpoint: "https://example.test/oauth/token", | ||
| 1808 | clientAuthentication: "client_secret_post", | ||
| 1809 | headerName: "Authorization", | ||
| 1810 | prefix: "Bearer ", | ||
| 1811 | refreshToken: { | ||
| 1812 | providerId: "local", | ||
| 1813 | handle: refreshSecretId, | ||
| 1814 | }, | ||
| 1815 | grantedScopes: ["scope:drive.readonly"], | ||
| 1816 | lastRefreshedAt: null, | ||
| 1817 | orphanedAt: null, | ||
| 1818 | createdAt: Date.now(), | ||
| 1819 | updatedAt: Date.now(), | ||
| 1820 | }); | ||
| 1821 | |||
| 1822 | const source = yield* createPersistedGoogleSource({ | ||
| 1823 | runtime, | ||
| 1824 | name: "Drive", | ||
| 1825 | namespace: "google.drive", | ||
| 1826 | service: "drive", | ||
| 1827 | version: "v3", | ||
| 1828 | discoveryUrl: "https://example.test/drive/$discovery/rest?version=v3", | ||
| 1829 | scopes: ["scope:drive.readonly"], | ||
| 1830 | auth: { | ||
| 1831 | kind: "provider_grant_ref", | ||
| 1832 | grantId, | ||
| 1833 | providerKey: "google_workspace", | ||
| 1834 | requiredScopes: ["scope:drive.readonly"], | ||
| 1835 | headerName: "Authorization", | ||
| 1836 | prefix: "Bearer ", | ||
| 1837 | }, | ||
| 1838 | }); | ||
| 1839 | |||
| 1840 | const removed = yield* withControlPlaneClient( | ||
| 1841 | { runtime }, | ||
| 1842 | (client) => | ||
| 1843 | client.sources.remove({ | ||
| 1844 | path: { | ||
| 1845 | workspaceId: installation.workspaceId, | ||
| 1846 | sourceId: source.id, | ||
| 1847 | }, | ||
| 1848 | }), | ||
| 1849 | ); | ||
| 1850 | |||
| 1851 | expect(removed.removed).toBe(true); | ||
| 1852 | |||
| 1853 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1854 | assertTrue(Option.isSome(storedGrant)); | ||
| 1855 | expect(storedGrant.value.orphanedAt).not.toBeNull(); | ||
| 1856 | }), | ||
| 1857 | ); | ||
| 1858 | |||
| 1859 | it.scoped("revokes shared provider grants through the API and disconnects linked sources", () => | ||
| 1860 | Effect.gen(function* () { | ||
| 1861 | const runtime = yield* makeRuntime; | ||
| 1862 | const installation = runtime.localInstallation; | ||
| 1863 | const oauthClientId = WorkspaceOauthClientIdSchema.make(`ws_oauth_client_${crypto.randomUUID()}`); | ||
| 1864 | const refreshSecretId = "sec_google_refresh_revoke"; | ||
| 1865 | |||
| 1866 | yield* runtime.persistence.rows.workspaceOauthClients.upsert({ | ||
| 1867 | id: oauthClientId, | ||
| 1868 | workspaceId: installation.workspaceId, | ||
| 1869 | providerKey: "google_workspace", | ||
| 1870 | label: "Local Google Workspace Client", | ||
| 1871 | clientId: "google-client-id", | ||
| 1872 | clientSecretProviderId: null, | ||
| 1873 | clientSecretHandle: null, | ||
| 1874 | clientMetadataJson: null, | ||
| 1875 | createdAt: Date.now(), | ||
| 1876 | updatedAt: Date.now(), | ||
| 1877 | }); | ||
| 1878 | yield* upsertLocalSecret(runtime, { | ||
| 1879 | id: refreshSecretId, | ||
| 1880 | name: "Google Refresh Token", | ||
| 1881 | purpose: "oauth_refresh_token", | ||
| 1882 | value: "refresh-token-revoke", | ||
| 1883 | }); | ||
| 1884 | |||
| 1885 | const grantId = ProviderAuthGrantIdSchema.make(`provider_grant_${crypto.randomUUID()}`); | ||
| 1886 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1887 | id: grantId, | ||
| 1888 | workspaceId: installation.workspaceId, | ||
| 1889 | actorAccountId: installation.accountId, | ||
| 1890 | providerKey: "google_workspace", | ||
| 1891 | oauthClientId, | ||
| 1892 | tokenEndpoint: "https://example.test/oauth/token", | ||
| 1893 | clientAuthentication: "client_secret_post", | ||
| 1894 | headerName: "Authorization", | ||
| 1895 | prefix: "Bearer ", | ||
| 1896 | refreshToken: { | ||
| 1897 | providerId: "local", | ||
| 1898 | handle: refreshSecretId, | ||
| 1899 | }, | ||
| 1900 | grantedScopes: ["scope:drive.readonly", "scope:gmail.readonly"], | ||
| 1901 | lastRefreshedAt: null, | ||
| 1902 | orphanedAt: null, | ||
| 1903 | createdAt: Date.now(), | ||
| 1904 | updatedAt: Date.now(), | ||
| 1905 | }); | ||
| 1906 | |||
| 1907 | const driveSource = yield* createPersistedGoogleSource({ | ||
| 1908 | runtime, | ||
| 1909 | name: "Drive", | ||
| 1910 | namespace: "google.drive", | ||
| 1911 | service: "drive", | ||
| 1912 | version: "v3", | ||
| 1913 | discoveryUrl: "https://example.test/drive/$discovery/rest?version=v3", | ||
| 1914 | scopes: ["scope:drive.readonly"], | ||
| 1915 | auth: { | ||
| 1916 | kind: "provider_grant_ref", | ||
| 1917 | grantId, | ||
| 1918 | providerKey: "google_workspace", | ||
| 1919 | requiredScopes: ["scope:drive.readonly"], | ||
| 1920 | headerName: "Authorization", | ||
| 1921 | prefix: "Bearer ", | ||
| 1922 | }, | ||
| 1923 | }); | ||
| 1924 | const gmailSource = yield* createPersistedGoogleSource({ | ||
| 1925 | runtime, | ||
| 1926 | name: "Gmail", | ||
| 1927 | namespace: "google.gmail", | ||
| 1928 | service: "gmail", | ||
| 1929 | version: "v1", | ||
| 1930 | discoveryUrl: "https://example.test/gmail/$discovery/rest?version=v1", | ||
| 1931 | scopes: ["scope:gmail.readonly"], | ||
| 1932 | auth: { | ||
| 1933 | kind: "provider_grant_ref", | ||
| 1934 | grantId, | ||
| 1935 | providerKey: "google_workspace", | ||
| 1936 | requiredScopes: ["scope:gmail.readonly"], | ||
| 1937 | headerName: "Authorization", | ||
| 1938 | prefix: "Bearer ", | ||
| 1939 | }, | ||
| 1940 | }); | ||
| 1941 | |||
| 1942 | const removed = yield* withControlPlaneClient( | ||
| 1943 | { runtime }, | ||
| 1944 | (client) => | ||
| 1945 | client.sources.removeProviderAuthGrant({ | ||
| 1946 | path: { | ||
| 1947 | workspaceId: installation.workspaceId, | ||
| 1948 | grantId, | ||
| 1949 | }, | ||
| 1950 | }), | ||
| 1951 | ); | ||
| 1952 | |||
| 1953 | expect(removed.removed).toBe(true); | ||
| 1954 | |||
| 1955 | const storedGrant = yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1956 | assertTrue(Option.isNone(storedGrant)); | ||
| 1957 | |||
| 1958 | const storedRefreshSecret = yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 1959 | SecretMaterialIdSchema.make(refreshSecretId), | ||
| 1960 | ); | ||
| 1961 | assertTrue(Option.isNone(storedRefreshSecret)); | ||
| 1962 | |||
| 1963 | const disconnectedDrive = yield* withControlPlaneClient( | ||
| 1964 | { runtime }, | ||
| 1965 | (client) => | ||
| 1966 | client.sources.get({ | ||
| 1967 | path: { | ||
| 1968 | workspaceId: installation.workspaceId, | ||
| 1969 | sourceId: driveSource.id, | ||
| 1970 | }, | ||
| 1971 | }), | ||
| 1972 | ); | ||
| 1973 | const disconnectedGmail = yield* withControlPlaneClient( | ||
| 1974 | { runtime }, | ||
| 1975 | (client) => | ||
| 1976 | client.sources.get({ | ||
| 1977 | path: { | ||
| 1978 | workspaceId: installation.workspaceId, | ||
| 1979 | sourceId: gmailSource.id, | ||
| 1980 | }, | ||
| 1981 | }), | ||
| 1982 | ); | ||
| 1983 | |||
| 1984 | expect(disconnectedDrive.status).toBe("auth_required"); | ||
| 1985 | expect(disconnectedDrive.auth).toEqual({ kind: "none" }); | ||
| 1986 | expect(disconnectedGmail.status).toBe("auth_required"); | ||
| 1987 | expect(disconnectedGmail.auth).toEqual({ kind: "none" }); | ||
| 1988 | }), | ||
| 1989 | ); | ||
| 1990 | |||
| 1991 | }); | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/platform-internal", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts" | ||
| 7 | }, | ||
| 8 | "scripts": { | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | ||
| 10 | }, | ||
| 11 | "dependencies": { | ||
| 12 | "@executor/codemode-core": "workspace:*", | ||
| 13 | "@executor/platform-sdk": "workspace:*", | ||
| 14 | "effect": "catalog:" | ||
| 15 | }, | ||
| 16 | "devDependencies": { | ||
| 17 | "@types/node": "catalog:", | ||
| 18 | "bun-types": "catalog:", | ||
| 19 | "typescript": "^5.9.3" | ||
| 20 | }, | ||
| 21 | "version": "0.0.0" | ||
| 22 | } | ||
| @@ -0,0 +1 @@ | |||
| 1 | export * from "./tools/executor-admin"; | ||
| @@ -0,0 +1,692 @@ | |||
| 1 | import { toTool, type ToolMap } from "@executor/codemode-core"; | ||
| 2 | import { | ||
| 3 | type AccountId, | ||
| 4 | LocalInstallationSchema, | ||
| 5 | LocalWorkspacePolicySchema, | ||
| 6 | SourceDiscoveryResultSchema, | ||
| 7 | SourceIdSchema, | ||
| 8 | SourceInspectionDiscoverPayloadSchema, | ||
| 9 | SourceInspectionDiscoverResultSchema, | ||
| 10 | SourceInspectionSchema, | ||
| 11 | SourceInspectionToolDetailSchema, | ||
| 12 | SourceSchema, | ||
| 13 | type WorkspaceId, | ||
| 14 | WorkspaceOauthClientSchema, | ||
| 15 | } from "@executor/platform-sdk/schema"; | ||
| 16 | import * as Effect from "effect/Effect"; | ||
| 17 | import * as Layer from "effect/Layer"; | ||
| 18 | import * as Schema from "effect/Schema"; | ||
| 19 | |||
| 20 | import { | ||
| 21 | ControlPlaneStore, | ||
| 22 | type ControlPlaneStoreShape, | ||
| 23 | type WorkspaceInternalToolContext, | ||
| 24 | RuntimeLocalWorkspaceState, | ||
| 25 | RuntimeSourceAuthService, | ||
| 26 | RuntimeSourceCatalogSyncService, | ||
| 27 | RuntimeSourceStore, | ||
| 28 | RuntimeSourceStoreService, | ||
| 29 | SourceArtifactStore, | ||
| 30 | type SourceArtifactStoreShape, | ||
| 31 | WorkspaceConfigStore, | ||
| 32 | type WorkspaceConfigStoreShape, | ||
| 33 | WorkspaceStateStore, | ||
| 34 | type WorkspaceStateStoreShape, | ||
| 35 | makeWorkspaceStorageLayer, | ||
| 36 | provideOptionalRuntimeLocalWorkspace, | ||
| 37 | } from "@executor/platform-sdk/runtime"; | ||
| 38 | import { | ||
| 39 | CreateSecretPayloadSchema, | ||
| 40 | CreateSecretResultSchema, | ||
| 41 | type CreateSecretPayload, | ||
| 42 | DeleteSecretResultSchema, | ||
| 43 | InstanceConfigSchema, | ||
| 44 | SecretListItemSchema, | ||
| 45 | UpdateSecretPayloadSchema, | ||
| 46 | UpdateSecretResultSchema, | ||
| 47 | type UpdateSecretPayload, | ||
| 48 | } from "@executor/platform-sdk/local/contracts"; | ||
| 49 | import { | ||
| 50 | createLocalSecret, | ||
| 51 | deleteLocalSecret, | ||
| 52 | getLocalInstanceConfig, | ||
| 53 | listLocalSecrets, | ||
| 54 | updateLocalSecret, | ||
| 55 | } from "@executor/platform-sdk/local/secrets"; | ||
| 56 | import { | ||
| 57 | CreatePolicyPayloadSchema, | ||
| 58 | type CreatePolicyPayload, | ||
| 59 | type UpdatePolicyPayload, | ||
| 60 | UpdatePolicyPayloadSchema, | ||
| 61 | } from "@executor/platform-sdk/policies/contracts"; | ||
| 62 | import { | ||
| 63 | createPolicy, | ||
| 64 | getPolicy, | ||
| 65 | listPolicies, | ||
| 66 | removePolicy, | ||
| 67 | updatePolicy, | ||
| 68 | } from "@executor/platform-sdk/policies/operations"; | ||
| 69 | import { | ||
| 70 | CreateWorkspaceOauthClientPayloadSchema, | ||
| 71 | DiscoverSourcePayloadSchema, | ||
| 72 | type CreateWorkspaceOauthClientPayload, | ||
| 73 | UpdateSourcePayloadSchema, | ||
| 74 | } from "@executor/platform-sdk/sources/contracts"; | ||
| 75 | import { | ||
| 76 | discoverSource, | ||
| 77 | } from "@executor/platform-sdk/sources/discovery"; | ||
| 78 | import { | ||
| 79 | discoverSourceInspectionTools, | ||
| 80 | getSourceInspection, | ||
| 81 | getSourceInspectionToolDetail, | ||
| 82 | } from "@executor/platform-sdk/sources/inspection"; | ||
| 83 | import { | ||
| 84 | getSource, | ||
| 85 | listSources, | ||
| 86 | removeSource, | ||
| 87 | updateSource, | ||
| 88 | } from "@executor/platform-sdk/sources/operations"; | ||
| 89 | |||
| 90 | const emptyInputSchema = Schema.standardSchemaV1(Schema.Struct({})); | ||
| 91 | const localInstallationOutputSchema = Schema.standardSchemaV1( | ||
| 92 | LocalInstallationSchema, | ||
| 93 | ); | ||
| 94 | const instanceConfigOutputSchema = | ||
| 95 | Schema.standardSchemaV1(InstanceConfigSchema); | ||
| 96 | const secretListOutputSchema = Schema.standardSchemaV1( | ||
| 97 | Schema.Array(SecretListItemSchema), | ||
| 98 | ); | ||
| 99 | const createSecretInputSchema = Schema.standardSchemaV1( | ||
| 100 | CreateSecretPayloadSchema, | ||
| 101 | ); | ||
| 102 | const updateSecretInputSchema = Schema.standardSchemaV1( | ||
| 103 | Schema.Struct({ | ||
| 104 | secretId: Schema.String, | ||
| 105 | payload: UpdateSecretPayloadSchema, | ||
| 106 | }), | ||
| 107 | ); | ||
| 108 | const removeSecretInputSchema = Schema.standardSchemaV1( | ||
| 109 | Schema.Struct({ | ||
| 110 | secretId: Schema.String, | ||
| 111 | }), | ||
| 112 | ); | ||
| 113 | const removeResultSchema = Schema.standardSchemaV1( | ||
| 114 | Schema.Struct({ | ||
| 115 | removed: Schema.Boolean, | ||
| 116 | }), | ||
| 117 | ); | ||
| 118 | const listSourcesOutputSchema = Schema.standardSchemaV1( | ||
| 119 | Schema.Array(SourceSchema), | ||
| 120 | ); | ||
| 121 | const getSourceInputSchema = Schema.standardSchemaV1( | ||
| 122 | Schema.Struct({ | ||
| 123 | sourceId: SourceIdSchema, | ||
| 124 | }), | ||
| 125 | ); | ||
| 126 | const updateSourceInputSchema = Schema.standardSchemaV1( | ||
| 127 | Schema.Struct({ | ||
| 128 | sourceId: SourceIdSchema, | ||
| 129 | payload: UpdateSourcePayloadSchema, | ||
| 130 | }), | ||
| 131 | ); | ||
| 132 | const discoverSourceInputSchema = Schema.standardSchemaV1( | ||
| 133 | DiscoverSourcePayloadSchema, | ||
| 134 | ); | ||
| 135 | const inspectToolInputSchema = Schema.standardSchemaV1( | ||
| 136 | Schema.Struct({ | ||
| 137 | sourceId: SourceIdSchema, | ||
| 138 | toolPath: Schema.String, | ||
| 139 | }), | ||
| 140 | ); | ||
| 141 | const inspectDiscoverInputSchema = Schema.standardSchemaV1( | ||
| 142 | Schema.Struct({ | ||
| 143 | sourceId: SourceIdSchema, | ||
| 144 | payload: SourceInspectionDiscoverPayloadSchema, | ||
| 145 | }), | ||
| 146 | ); | ||
| 147 | const listPoliciesOutputSchema = Schema.standardSchemaV1( | ||
| 148 | Schema.Array(LocalWorkspacePolicySchema), | ||
| 149 | ); | ||
| 150 | const policyIdInputSchema = Schema.standardSchemaV1( | ||
| 151 | Schema.Struct({ | ||
| 152 | policyId: Schema.String, | ||
| 153 | }), | ||
| 154 | ); | ||
| 155 | const createPolicyInputSchema = Schema.standardSchemaV1( | ||
| 156 | CreatePolicyPayloadSchema, | ||
| 157 | ); | ||
| 158 | const updatePolicyInputSchema = Schema.standardSchemaV1( | ||
| 159 | Schema.Struct({ | ||
| 160 | policyId: Schema.String, | ||
| 161 | payload: UpdatePolicyPayloadSchema, | ||
| 162 | }), | ||
| 163 | ); | ||
| 164 | const workspaceOauthClientListInputSchema = Schema.standardSchemaV1( | ||
| 165 | Schema.Struct({ | ||
| 166 | providerKey: Schema.String, | ||
| 167 | }), | ||
| 168 | ); | ||
| 169 | const workspaceOauthClientListOutputSchema = Schema.standardSchemaV1( | ||
| 170 | Schema.Array(WorkspaceOauthClientSchema), | ||
| 171 | ); | ||
| 172 | const createWorkspaceOauthClientInputSchema = Schema.standardSchemaV1( | ||
| 173 | CreateWorkspaceOauthClientPayloadSchema, | ||
| 174 | ); | ||
| 175 | const removeWorkspaceOauthClientInputSchema = Schema.standardSchemaV1( | ||
| 176 | Schema.Struct({ | ||
| 177 | oauthClientId: Schema.String, | ||
| 178 | }), | ||
| 179 | ); | ||
| 180 | const removeProviderGrantInputSchema = Schema.standardSchemaV1( | ||
| 181 | Schema.Struct({ | ||
| 182 | grantId: Schema.String, | ||
| 183 | }), | ||
| 184 | ); | ||
| 185 | const sourceOutputSchema = Schema.standardSchemaV1(SourceSchema); | ||
| 186 | const sourceInspectionOutputSchema = Schema.standardSchemaV1( | ||
| 187 | SourceInspectionSchema, | ||
| 188 | ); | ||
| 189 | const sourceInspectionToolOutputSchema = Schema.standardSchemaV1( | ||
| 190 | SourceInspectionToolDetailSchema, | ||
| 191 | ); | ||
| 192 | const sourceInspectionDiscoverOutputSchema = Schema.standardSchemaV1( | ||
| 193 | SourceInspectionDiscoverResultSchema, | ||
| 194 | ); | ||
| 195 | const sourceDiscoveryOutputSchema = Schema.standardSchemaV1( | ||
| 196 | SourceDiscoveryResultSchema, | ||
| 197 | ); | ||
| 198 | const localWorkspacePolicyOutputSchema = Schema.standardSchemaV1( | ||
| 199 | LocalWorkspacePolicySchema, | ||
| 200 | ); | ||
| 201 | |||
| 202 | const makeRuntimeLayer = (input: { | ||
| 203 | controlPlaneStore: ControlPlaneStoreShape; | ||
| 204 | sourceStore: RuntimeSourceStore; | ||
| 205 | sourceCatalogSyncService: Effect.Effect.Success< | ||
| 206 | typeof RuntimeSourceCatalogSyncService | ||
| 207 | >; | ||
| 208 | workspaceConfigStore: WorkspaceConfigStoreShape; | ||
| 209 | workspaceStateStore: WorkspaceStateStoreShape; | ||
| 210 | sourceArtifactStore: SourceArtifactStoreShape; | ||
| 211 | }) => | ||
| 212 | Layer.mergeAll( | ||
| 213 | Layer.succeed(ControlPlaneStore, input.controlPlaneStore), | ||
| 214 | Layer.succeed(RuntimeSourceStoreService, input.sourceStore), | ||
| 215 | Layer.succeed( | ||
| 216 | RuntimeSourceCatalogSyncService, | ||
| 217 | input.sourceCatalogSyncService, | ||
| 218 | ), | ||
| 219 | makeWorkspaceStorageLayer({ | ||
| 220 | workspaceConfigStore: input.workspaceConfigStore, | ||
| 221 | workspaceStateStore: input.workspaceStateStore, | ||
| 222 | sourceArtifactStore: input.sourceArtifactStore, | ||
| 223 | }), | ||
| 224 | ); | ||
| 225 | |||
| 226 | const runRuntimeEffect = <A, E, R>(input: { | ||
| 227 | effect: Effect.Effect<A, E, R>; | ||
| 228 | runtimeLayer: Layer.Layer< | ||
| 229 | | ControlPlaneStore | ||
| 230 | | RuntimeSourceStoreService | ||
| 231 | | RuntimeSourceCatalogSyncService | ||
| 232 | | WorkspaceConfigStore | ||
| 233 | | WorkspaceStateStore | ||
| 234 | | SourceArtifactStore, | ||
| 235 | never, | ||
| 236 | never | ||
| 237 | >; | ||
| 238 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | ||
| 239 | }) => | ||
| 240 | Effect.runPromise( | ||
| 241 | provideOptionalRuntimeLocalWorkspace( | ||
| 242 | input.effect.pipe(Effect.provide(input.runtimeLayer)), | ||
| 243 | input.runtimeLocalWorkspace, | ||
| 244 | ) as Effect.Effect<A, E, never>, | ||
| 245 | ); | ||
| 246 | |||
| 247 | const runWorkspaceStorageEffect = <A, E, R>(input: { | ||
| 248 | effect: Effect.Effect<A, E, R>; | ||
| 249 | workspaceStorageLayer: Layer.Layer< | ||
| 250 | WorkspaceConfigStore | WorkspaceStateStore | SourceArtifactStore, | ||
| 251 | never, | ||
| 252 | never | ||
| 253 | >; | ||
| 254 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | ||
| 255 | }) => | ||
| 256 | Effect.runPromise( | ||
| 257 | provideOptionalRuntimeLocalWorkspace( | ||
| 258 | input.effect.pipe(Effect.provide(input.workspaceStorageLayer)), | ||
| 259 | input.runtimeLocalWorkspace, | ||
| 260 | ) as Effect.Effect<A, E, never>, | ||
| 261 | ); | ||
| 262 | |||
| 263 | export const createExecutorAdminToolMap = ( | ||
| 264 | input: WorkspaceInternalToolContext, | ||
| 265 | ): ToolMap => { | ||
| 266 | const runtimeLayer = makeRuntimeLayer({ | ||
| 267 | controlPlaneStore: input.controlPlaneStore, | ||
| 268 | sourceStore: input.sourceStore, | ||
| 269 | sourceCatalogSyncService: input.sourceCatalogSyncService, | ||
| 270 | workspaceConfigStore: input.workspaceConfigStore, | ||
| 271 | workspaceStateStore: input.workspaceStateStore, | ||
| 272 | sourceArtifactStore: input.sourceArtifactStore, | ||
| 273 | }); | ||
| 274 | const workspaceStorageLayer = makeWorkspaceStorageLayer({ | ||
| 275 | workspaceConfigStore: input.workspaceConfigStore, | ||
| 276 | workspaceStateStore: input.workspaceStateStore, | ||
| 277 | sourceArtifactStore: input.sourceArtifactStore, | ||
| 278 | }); | ||
| 279 | |||
| 280 | const metadata = { | ||
| 281 | sourceKey: "executor", | ||
| 282 | interaction: "auto" as const, | ||
| 283 | }; | ||
| 284 | |||
| 285 | return { | ||
| 286 | "executor.local.installation.get": toTool({ | ||
| 287 | tool: { | ||
| 288 | description: | ||
| 289 | "Get the active local executor installation account and workspace ids.", | ||
| 290 | inputSchema: emptyInputSchema, | ||
| 291 | outputSchema: localInstallationOutputSchema, | ||
| 292 | execute: async () => ({ | ||
| 293 | workspaceId: input.workspaceId, | ||
| 294 | accountId: input.accountId, | ||
| 295 | }), | ||
| 296 | }, | ||
| 297 | metadata, | ||
| 298 | }), | ||
| 299 | "executor.local.config.get": toTool({ | ||
| 300 | tool: { | ||
| 301 | description: | ||
| 302 | "Get local instance config such as supported secret providers.", | ||
| 303 | inputSchema: emptyInputSchema, | ||
| 304 | outputSchema: instanceConfigOutputSchema, | ||
| 305 | execute: () => Effect.runPromise(getLocalInstanceConfig()), | ||
| 306 | }, | ||
| 307 | metadata, | ||
| 308 | }), | ||
| 309 | "executor.secrets.list": toTool({ | ||
| 310 | tool: { | ||
| 311 | description: | ||
| 312 | "List locally stored secrets and the sources linked to them.", | ||
| 313 | inputSchema: emptyInputSchema, | ||
| 314 | outputSchema: secretListOutputSchema, | ||
| 315 | execute: () => | ||
| 316 | runRuntimeEffect({ | ||
| 317 | effect: listLocalSecrets(), | ||
| 318 | runtimeLayer, | ||
| 319 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 320 | }), | ||
| 321 | }, | ||
| 322 | metadata, | ||
| 323 | }), | ||
| 324 | "executor.secrets.create": toTool({ | ||
| 325 | tool: { | ||
| 326 | description: | ||
| 327 | "Create a local secret without putting the raw value into source config.", | ||
| 328 | inputSchema: createSecretInputSchema, | ||
| 329 | outputSchema: Schema.standardSchemaV1(CreateSecretResultSchema), | ||
| 330 | execute: (payload: CreateSecretPayload) => | ||
| 331 | runRuntimeEffect({ | ||
| 332 | effect: createLocalSecret(payload), | ||
| 333 | runtimeLayer, | ||
| 334 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 335 | }), | ||
| 336 | }, | ||
| 337 | metadata, | ||
| 338 | }), | ||
| 339 | "executor.secrets.update": toTool({ | ||
| 340 | tool: { | ||
| 341 | description: | ||
| 342 | "Update a stored secret name and optionally rotate its value.", | ||
| 343 | inputSchema: updateSecretInputSchema, | ||
| 344 | outputSchema: Schema.standardSchemaV1(UpdateSecretResultSchema), | ||
| 345 | execute: (payload: { | ||
| 346 | secretId: string; | ||
| 347 | payload: UpdateSecretPayload; | ||
| 348 | }) => | ||
| 349 | runRuntimeEffect({ | ||
| 350 | effect: updateLocalSecret(payload), | ||
| 351 | runtimeLayer, | ||
| 352 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 353 | }), | ||
| 354 | }, | ||
| 355 | metadata, | ||
| 356 | }), | ||
| 357 | "executor.secrets.remove": toTool({ | ||
| 358 | tool: { | ||
| 359 | description: "Remove a stored local secret.", | ||
| 360 | inputSchema: removeSecretInputSchema, | ||
| 361 | outputSchema: Schema.standardSchemaV1(DeleteSecretResultSchema), | ||
| 362 | execute: ({ secretId }: { secretId: string }) => | ||
| 363 | runRuntimeEffect({ | ||
| 364 | effect: deleteLocalSecret(secretId), | ||
| 365 | runtimeLayer, | ||
| 366 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 367 | }), | ||
| 368 | }, | ||
| 369 | metadata, | ||
| 370 | }), | ||
| 371 | "executor.sources.discover": toTool({ | ||
| 372 | tool: { | ||
| 373 | description: | ||
| 374 | "Probe a URL and infer whether it looks like MCP, OpenAPI, GraphQL, or another supported source.", | ||
| 375 | inputSchema: discoverSourceInputSchema, | ||
| 376 | outputSchema: sourceDiscoveryOutputSchema, | ||
| 377 | execute: (payload: { url: string; probeAuth?: unknown }) => | ||
| 378 | Effect.runPromise( | ||
| 379 | discoverSource({ | ||
| 380 | url: payload.url, | ||
| 381 | probeAuth: payload.probeAuth as never, | ||
| 382 | }), | ||
| 383 | ), | ||
| 384 | }, | ||
| 385 | metadata, | ||
| 386 | }), | ||
| 387 | "executor.sources.list": toTool({ | ||
| 388 | tool: { | ||
| 389 | description: "List sources connected in the current workspace.", | ||
| 390 | inputSchema: emptyInputSchema, | ||
| 391 | outputSchema: listSourcesOutputSchema, | ||
| 392 | execute: () => | ||
| 393 | runRuntimeEffect({ | ||
| 394 | effect: listSources({ | ||
| 395 | workspaceId: input.workspaceId, | ||
| 396 | accountId: input.accountId as never, | ||
| 397 | }), | ||
| 398 | runtimeLayer, | ||
| 399 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 400 | }), | ||
| 401 | }, | ||
| 402 | metadata, | ||
| 403 | }), | ||
| 404 | "executor.sources.get": toTool({ | ||
| 405 | tool: { | ||
| 406 | description: "Get one source by id.", | ||
| 407 | inputSchema: getSourceInputSchema, | ||
| 408 | outputSchema: sourceOutputSchema, | ||
| 409 | execute: ({ sourceId }: { sourceId: string }) => | ||
| 410 | runRuntimeEffect({ | ||
| 411 | effect: getSource({ | ||
| 412 | workspaceId: input.workspaceId, | ||
| 413 | sourceId: sourceId as never, | ||
| 414 | accountId: input.accountId as never, | ||
| 415 | }), | ||
| 416 | runtimeLayer, | ||
| 417 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 418 | }), | ||
| 419 | }, | ||
| 420 | metadata, | ||
| 421 | }), | ||
| 422 | "executor.sources.update": toTool({ | ||
| 423 | tool: { | ||
| 424 | description: "Update a source definition in the current workspace.", | ||
| 425 | inputSchema: updateSourceInputSchema, | ||
| 426 | outputSchema: sourceOutputSchema, | ||
| 427 | execute: (payload: { | ||
| 428 | sourceId: string; | ||
| 429 | payload: Record<string, unknown>; | ||
| 430 | }) => | ||
| 431 | runRuntimeEffect({ | ||
| 432 | effect: updateSource({ | ||
| 433 | workspaceId: input.workspaceId, | ||
| 434 | sourceId: payload.sourceId as never, | ||
| 435 | accountId: input.accountId as never, | ||
| 436 | payload: payload.payload as never, | ||
| 437 | }), | ||
| 438 | runtimeLayer, | ||
| 439 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 440 | }), | ||
| 441 | }, | ||
| 442 | metadata, | ||
| 443 | }), | ||
| 444 | "executor.sources.remove": toTool({ | ||
| 445 | tool: { | ||
| 446 | description: "Remove a source from the current workspace.", | ||
| 447 | inputSchema: getSourceInputSchema, | ||
| 448 | outputSchema: removeResultSchema, | ||
| 449 | execute: ({ sourceId }: { sourceId: string }) => | ||
| 450 | runRuntimeEffect({ | ||
| 451 | effect: removeSource({ | ||
| 452 | workspaceId: input.workspaceId, | ||
| 453 | sourceId: sourceId as never, | ||
| 454 | }), | ||
| 455 | runtimeLayer, | ||
| 456 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 457 | }), | ||
| 458 | }, | ||
| 459 | metadata, | ||
| 460 | }), | ||
| 461 | "executor.sources.inspect.get": toTool({ | ||
| 462 | tool: { | ||
| 463 | description: "Inspect the tool model for one connected source.", | ||
| 464 | inputSchema: getSourceInputSchema, | ||
| 465 | outputSchema: sourceInspectionOutputSchema, | ||
| 466 | execute: ({ sourceId }: { sourceId: string }) => | ||
| 467 | runRuntimeEffect({ | ||
| 468 | effect: getSourceInspection({ | ||
| 469 | workspaceId: input.workspaceId, | ||
| 470 | sourceId: sourceId as never, | ||
| 471 | }), | ||
| 472 | runtimeLayer, | ||
| 473 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 474 | }), | ||
| 475 | }, | ||
| 476 | metadata, | ||
| 477 | }), | ||
| 478 | "executor.sources.inspect.tool": toTool({ | ||
| 479 | tool: { | ||
| 480 | description: "Inspect one tool inside a connected source.", | ||
| 481 | inputSchema: inspectToolInputSchema, | ||
| 482 | outputSchema: sourceInspectionToolOutputSchema, | ||
| 483 | execute: ({ | ||
| 484 | sourceId, | ||
| 485 | toolPath, | ||
| 486 | }: { | ||
| 487 | sourceId: string; | ||
| 488 | toolPath: string; | ||
| 489 | }) => | ||
| 490 | runRuntimeEffect({ | ||
| 491 | effect: getSourceInspectionToolDetail({ | ||
| 492 | workspaceId: input.workspaceId, | ||
| 493 | sourceId: sourceId as never, | ||
| 494 | toolPath, | ||
| 495 | }), | ||
| 496 | runtimeLayer, | ||
| 497 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 498 | }), | ||
| 499 | }, | ||
| 500 | metadata, | ||
| 501 | }), | ||
| 502 | "executor.sources.inspect.discover": toTool({ | ||
| 503 | tool: { | ||
| 504 | description: "Search within a single source's inspected tools.", | ||
| 505 | inputSchema: inspectDiscoverInputSchema, | ||
| 506 | outputSchema: sourceInspectionDiscoverOutputSchema, | ||
| 507 | execute: ({ | ||
| 508 | sourceId, | ||
| 509 | payload, | ||
| 510 | }: { | ||
| 511 | sourceId: string; | ||
| 512 | payload: { query: string; limit?: number }; | ||
| 513 | }) => | ||
| 514 | runRuntimeEffect({ | ||
| 515 | effect: discoverSourceInspectionTools({ | ||
| 516 | workspaceId: input.workspaceId, | ||
| 517 | sourceId: sourceId as never, | ||
| 518 | payload: payload as never, | ||
| 519 | }), | ||
| 520 | runtimeLayer, | ||
| 521 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 522 | }), | ||
| 523 | }, | ||
| 524 | metadata, | ||
| 525 | }), | ||
| 526 | "executor.sources.oauthClients.list": toTool({ | ||
| 527 | tool: { | ||
| 528 | description: "List workspace OAuth clients for a provider key.", | ||
| 529 | inputSchema: workspaceOauthClientListInputSchema, | ||
| 530 | outputSchema: workspaceOauthClientListOutputSchema, | ||
| 531 | execute: ({ providerKey }: { providerKey: string }) => | ||
| 532 | runWorkspaceStorageEffect({ | ||
| 533 | effect: input.sourceAuthService.listWorkspaceOauthClients({ | ||
| 534 | workspaceId: input.workspaceId, | ||
| 535 | providerKey, | ||
| 536 | }), | ||
| 537 | workspaceStorageLayer, | ||
| 538 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 539 | }), | ||
| 540 | }, | ||
| 541 | metadata, | ||
| 542 | }), | ||
| 543 | "executor.sources.oauthClients.create": toTool({ | ||
| 544 | tool: { | ||
| 545 | description: | ||
| 546 | "Create a workspace OAuth client used for shared provider auth flows.", | ||
| 547 | inputSchema: createWorkspaceOauthClientInputSchema, | ||
| 548 | outputSchema: Schema.standardSchemaV1(WorkspaceOauthClientSchema), | ||
| 549 | execute: (payload: CreateWorkspaceOauthClientPayload) => | ||
| 550 | runWorkspaceStorageEffect({ | ||
| 551 | effect: input.sourceAuthService.createWorkspaceOauthClient({ | ||
| 552 | workspaceId: input.workspaceId, | ||
| 553 | providerKey: payload.providerKey, | ||
| 554 | label: payload.label, | ||
| 555 | oauthClient: payload.oauthClient, | ||
| 556 | }), | ||
| 557 | workspaceStorageLayer, | ||
| 558 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 559 | }), | ||
| 560 | }, | ||
| 561 | metadata, | ||
| 562 | }), | ||
| 563 | "executor.sources.oauthClients.remove": toTool({ | ||
| 564 | tool: { | ||
| 565 | description: "Remove a workspace OAuth client.", | ||
| 566 | inputSchema: removeWorkspaceOauthClientInputSchema, | ||
| 567 | outputSchema: removeResultSchema, | ||
| 568 | execute: ({ oauthClientId }: { oauthClientId: string }) => | ||
| 569 | runWorkspaceStorageEffect({ | ||
| 570 | effect: input.sourceAuthService | ||
| 571 | .removeWorkspaceOauthClient({ | ||
| 572 | workspaceId: input.workspaceId, | ||
| 573 | oauthClientId: oauthClientId as never, | ||
| 574 | }) | ||
| 575 | .pipe(Effect.map((removed) => ({ removed }))), | ||
| 576 | workspaceStorageLayer, | ||
| 577 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 578 | }), | ||
| 579 | }, | ||
| 580 | metadata, | ||
| 581 | }), | ||
| 582 | "executor.sources.providerGrants.remove": toTool({ | ||
| 583 | tool: { | ||
| 584 | description: | ||
| 585 | "Revoke a shared provider grant reference from the local workspace.", | ||
| 586 | inputSchema: removeProviderGrantInputSchema, | ||
| 587 | outputSchema: removeResultSchema, | ||
| 588 | execute: ({ grantId }: { grantId: string }) => | ||
| 589 | runWorkspaceStorageEffect({ | ||
| 590 | effect: input.sourceAuthService | ||
| 591 | .removeProviderAuthGrant({ | ||
| 592 | workspaceId: input.workspaceId, | ||
| 593 | grantId: grantId as never, | ||
| 594 | }) | ||
| 595 | .pipe(Effect.map((removed) => ({ removed }))), | ||
| 596 | workspaceStorageLayer, | ||
| 597 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 598 | }), | ||
| 599 | }, | ||
| 600 | metadata, | ||
| 601 | }), | ||
| 602 | "executor.policies.list": toTool({ | ||
| 603 | tool: { | ||
| 604 | description: "List local workspace policies.", | ||
| 605 | inputSchema: emptyInputSchema, | ||
| 606 | outputSchema: listPoliciesOutputSchema, | ||
| 607 | execute: () => | ||
| 608 | runRuntimeEffect({ | ||
| 609 | effect: listPolicies(input.workspaceId), | ||
| 610 | runtimeLayer, | ||
| 611 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 612 | }), | ||
| 613 | }, | ||
| 614 | metadata, | ||
| 615 | }), | ||
| 616 | "executor.policies.create": toTool({ | ||
| 617 | tool: { | ||
| 618 | description: "Create a local workspace policy.", | ||
| 619 | inputSchema: createPolicyInputSchema, | ||
| 620 | outputSchema: localWorkspacePolicyOutputSchema, | ||
| 621 | execute: (payload: CreatePolicyPayload) => | ||
| 622 | runRuntimeEffect({ | ||
| 623 | effect: createPolicy({ | ||
| 624 | workspaceId: input.workspaceId, | ||
| 625 | payload, | ||
| 626 | }), | ||
| 627 | runtimeLayer, | ||
| 628 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 629 | }), | ||
| 630 | }, | ||
| 631 | metadata, | ||
| 632 | }), | ||
| 633 | "executor.policies.get": toTool({ | ||
| 634 | tool: { | ||
| 635 | description: "Get one local workspace policy by id.", | ||
| 636 | inputSchema: policyIdInputSchema, | ||
| 637 | outputSchema: localWorkspacePolicyOutputSchema, | ||
| 638 | execute: ({ policyId }: { policyId: string }) => | ||
| 639 | runRuntimeEffect({ | ||
| 640 | effect: getPolicy({ | ||
| 641 | workspaceId: input.workspaceId, | ||
| 642 | policyId: policyId as never, | ||
| 643 | }), | ||
| 644 | runtimeLayer, | ||
| 645 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 646 | }), | ||
| 647 | }, | ||
| 648 | metadata, | ||
| 649 | }), | ||
| 650 | "executor.policies.update": toTool({ | ||
| 651 | tool: { | ||
| 652 | description: "Update a local workspace policy.", | ||
| 653 | inputSchema: updatePolicyInputSchema, | ||
| 654 | outputSchema: localWorkspacePolicyOutputSchema, | ||
| 655 | execute: ({ | ||
| 656 | policyId, | ||
| 657 | payload, | ||
| 658 | }: { | ||
| 659 | policyId: string; | ||
| 660 | payload: UpdatePolicyPayload; | ||
| 661 | }) => | ||
| 662 | runRuntimeEffect({ | ||
| 663 | effect: updatePolicy({ | ||
| 664 | workspaceId: input.workspaceId, | ||
| 665 | policyId: policyId as never, | ||
| 666 | payload, | ||
| 667 | }), | ||
| 668 | runtimeLayer, | ||
| 669 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 670 | }), | ||
| 671 | }, | ||
| 672 | metadata, | ||
| 673 | }), | ||
| 674 | "executor.policies.remove": toTool({ | ||
| 675 | tool: { | ||
| 676 | description: "Remove a local workspace policy.", | ||
| 677 | inputSchema: policyIdInputSchema, | ||
| 678 | outputSchema: removeResultSchema, | ||
| 679 | execute: ({ policyId }: { policyId: string }) => | ||
| 680 | runRuntimeEffect({ | ||
| 681 | effect: removePolicy({ | ||
| 682 | workspaceId: input.workspaceId, | ||
| 683 | policyId: policyId as never, | ||
| 684 | }), | ||
| 685 | runtimeLayer, | ||
| 686 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 687 | }), | ||
| 688 | }, | ||
| 689 | metadata, | ||
| 690 | }), | ||
| 691 | }; | ||
| 692 | }; | ||
| @@ -0,0 +1,13 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": ["src/**/*.ts"] | ||
| 13 | } | ||
| @@ -1,54 +1,65 @@ | |||
| 1 | { | 1 | { |
| 2 | "name": "@executor/control-plane", | 2 | "name": "@executor/platform-sdk", |
| 3 | "private": true, | 3 | "private": true, |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "exports": { | 5 | "exports": { |
| 6 | ".": "./src/index.ts", | 6 | ".": "./src/index.ts", |
| 7 | "./client": "./src/client.ts", | 7 | "./errors": "./src/errors.ts", |
| 8 | "./schema": "./src/schema/index.ts" | 8 | "./string-schemas": "./src/string-schemas.ts", |
| 9 | "./executions/contracts": "./src/executions/contracts.ts", | ||
| 10 | "./schema": "./src/schema/index.ts", | ||
| 11 | "./runtime": "./src/runtime/index.ts", | ||
| 12 | "./local/contracts": "./src/local/contracts.ts", | ||
| 13 | "./local/operations": "./src/local/operations.ts", | ||
| 14 | "./local/secrets": "./src/local/secrets.ts", | ||
| 15 | "./policies/contracts": "./src/policies/contracts.ts", | ||
| 16 | "./policies/operations": "./src/policies/operations.ts", | ||
| 17 | "./sources/contracts": "./src/sources/contracts.ts", | ||
| 18 | "./sources/discovery": "./src/sources/discovery.ts", | ||
| 19 | "./sources/inspection": "./src/sources/inspection.ts", | ||
| 20 | "./sources/operations": "./src/sources/operations.ts" | ||
| 9 | }, | 21 | }, |
| 10 | "imports": { | 22 | "imports": { |
| 11 | "#schema": "./src/schema/index.ts", | 23 | "#runtime": "./src/runtime/index.ts", |
| 12 | "#domain": "./src/domain/index.ts", | 24 | "#schema": "./src/schema/index.ts" |
| 13 | "#api": "./src/api/index.ts", | ||
| 14 | "#runtime": "./src/runtime/index.ts" | ||
| 15 | }, | 25 | }, |
| 16 | "scripts": { | 26 | "scripts": { |
| 17 | "fixture:release:capture": "bun run ./src/runtime/local/capture-release-workspace-fixture.ts", | 27 | "fixture:release:capture": "bun run ./src/runtime/local/capture-release-workspace-fixture.ts", |
| 18 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", | ||
| 19 | "test": "vitest run", | 28 | "test": "vitest run", |
| 20 | "test:watch": "vitest" | 29 | "test:watch": "vitest", |
| 30 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | ||
| 21 | }, | 31 | }, |
| 22 | "dependencies": { | 32 | "dependencies": { |
| 23 | "@effect/platform": "catalog:", | 33 | "@effect/platform": "catalog:", |
| 24 | "@executor/source-core": "workspace:*", | 34 | "@effect/platform-node": "catalog:", |
| 25 | "@executor/source-builtins": "workspace:*", | ||
| 26 | "@executor/auth-mcp-oauth": "workspace:*", | 35 | "@executor/auth-mcp-oauth": "workspace:*", |
| 27 | "@executor/auth-oauth2": "workspace:*", | 36 | "@executor/auth-oauth2": "workspace:*", |
| 28 | "@executor/codemode-core": "workspace:*", | 37 | "@executor/codemode-core": "workspace:*", |
| 29 | "@executor/source-google-discovery": "workspace:*", | ||
| 30 | "@executor/source-graphql": "workspace:*", | ||
| 31 | "@executor/ir": "workspace:*", | 38 | "@executor/ir": "workspace:*", |
| 32 | "@executor/source-mcp": "workspace:*", | ||
| 33 | "@executor/source-openapi": "workspace:*", | ||
| 34 | "@executor/runtime-deno-subprocess": "workspace:*", | 39 | "@executor/runtime-deno-subprocess": "workspace:*", |
| 35 | "@executor/runtime-quickjs": "workspace:*", | 40 | "@executor/runtime-quickjs": "workspace:*", |
| 36 | "@executor/runtime-ses": "workspace:*", | 41 | "@executor/runtime-ses": "workspace:*", |
| 37 | "effect": "catalog:", | 42 | "@executor/source-core": "workspace:*", |
| 38 | "graphql": "^16.12.0", | 43 | "@executor/source-builtins": "workspace:*", |
| 39 | "jsonc-parser": "3.3.1", | 44 | "@executor/source-google-discovery": "workspace:*", |
| 40 | "postgres": "^3.4.8", | 45 | "@executor/source-graphql": "workspace:*", |
| 41 | "prettier": "^3.8.1", | 46 | "@executor/source-mcp": "workspace:*", |
| 42 | "typescript": "^5.9.3" | 47 | "@executor/source-openapi": "workspace:*", |
| 48 | "effect": "catalog:" | ||
| 43 | }, | 49 | }, |
| 44 | "devDependencies": { | 50 | "devDependencies": { |
| 45 | "@effect/vitest": "catalog:", | 51 | "@effect/vitest": "catalog:", |
| 46 | "@types/node": "catalog:", | 52 | "@executor/platform-api": "workspace:*", |
| 47 | "@effect/platform-node": "catalog:", | 53 | "@executor/platform-internal": "workspace:*", |
| 48 | "@modelcontextprotocol/sdk": "^1.26.0", | 54 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 55 | "@types/node": "catalog:", | ||
| 49 | "bun-types": "catalog:", | 56 | "bun-types": "catalog:", |
| 50 | "vitest": "catalog:", | 57 | "graphql": "^16.12.0", |
| 51 | "zod": "catalog:" | 58 | "jsonc-parser": "3.3.1", |
| 59 | "postgres": "^3.4.8", | ||
| 60 | "prettier": "^3.8.1", | ||
| 61 | "typescript": "^5.9.3", | ||
| 62 | "vitest": "catalog:" | ||
| 52 | }, | 63 | }, |
| 53 | "version": "0.0.0" | 64 | "version": "0.0.0" |
| 54 | } | 65 | } |
File renamed without changes.
| @@ -0,0 +1,21 @@ | |||
| 1 | import * as Schema from "effect/Schema"; | ||
| 2 | |||
| 3 | import { TrimmedNonEmptyStringSchema } from "../string-schemas"; | ||
| 4 | |||
| 5 | export const CreateExecutionPayloadSchema = Schema.Struct({ | ||
| 6 | code: TrimmedNonEmptyStringSchema, | ||
| 7 | interactionMode: Schema.optional( | ||
| 8 | Schema.Literal("live", "live_form", "detach"), | ||
| 9 | ), | ||
| 10 | }); | ||
| 11 | |||
| 12 | export type CreateExecutionPayload = typeof CreateExecutionPayloadSchema.Type; | ||
| 13 | |||
| 14 | export const ResumeExecutionPayloadSchema = Schema.Struct({ | ||
| 15 | responseJson: Schema.optional(Schema.String), | ||
| 16 | interactionMode: Schema.optional( | ||
| 17 | Schema.Literal("live", "live_form", "detach"), | ||
| 18 | ), | ||
| 19 | }); | ||
| 20 | |||
| 21 | export type ResumeExecutionPayload = typeof ResumeExecutionPayloadSchema.Type; | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | export * from "./errors"; | ||
| 2 | export * from "./string-schemas"; | ||
| 3 | export * from "./executions/contracts"; | ||
| 4 | export * from "./local/contracts"; | ||
| 5 | export * from "./local/operations"; | ||
| 6 | export * from "./local/secrets"; | ||
| 7 | export * from "./policies/contracts"; | ||
| 8 | export * from "./policies/operations"; | ||
| 9 | export * from "./sources/contracts"; | ||
| 10 | export * from "./sources/discovery"; | ||
| 11 | export * from "./sources/inspection"; | ||
| 12 | export * from "./sources/operations"; | ||
| @@ -1,13 +1,6 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | 1 | import { SecretMaterialPurposeSchema } from "../schema"; |
| 2 | import { LocalInstallationSchema, SecretMaterialPurposeSchema } from "#schema"; | ||
| 3 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 4 | 3 | ||
| 5 | import { | ||
| 6 | ControlPlaneBadRequestError, | ||
| 7 | ControlPlaneNotFoundError, | ||
| 8 | ControlPlaneStorageError, | ||
| 9 | } from "../errors"; | ||
| 10 | |||
| 11 | export const SecretProviderSchema = Schema.Struct({ | 4 | export const SecretProviderSchema = Schema.Struct({ |
| 12 | id: Schema.String, | 5 | id: Schema.String, |
| 13 | name: Schema.String, | 6 | name: Schema.String, |
| @@ -23,8 +16,6 @@ export const InstanceConfigSchema = Schema.Struct({ · 9 unmodified lines | |||
| 23 | export type SecretProvider = typeof SecretProviderSchema.Type; | 16 | export type SecretProvider = typeof SecretProviderSchema.Type; |
| 24 | export type InstanceConfig = typeof InstanceConfigSchema.Type; | 17 | export type InstanceConfig = typeof InstanceConfigSchema.Type; |
| 25 | 18 | ||
| 26 | // -- Secrets CRUD schemas --------------------------------------------------- | ||
| 27 | |||
| 28 | export const SecretLinkedSourceSchema = Schema.Struct({ | 19 | export const SecretLinkedSourceSchema = Schema.Struct({ |
| 29 | sourceId: Schema.String, | 20 | sourceId: Schema.String, |
| 30 | sourceName: Schema.String, | 21 | sourceName: Schema.String, |
| @@ -87,45 +78,3 @@ export const DeleteSecretResultSchema = Schema.Struct({ · 56 unmodified lines | |||
| 87 | }); | 78 | }); |
| 88 | 79 | ||
| 89 | export type DeleteSecretResult = typeof DeleteSecretResultSchema.Type; | 80 | export type DeleteSecretResult = typeof DeleteSecretResultSchema.Type; |
| 90 | |||
| 91 | // -- API group -------------------------------------------------------------- | ||
| 92 | |||
| 93 | export class LocalApi extends HttpApiGroup.make("local") | ||
| 94 | .add( | ||
| 95 | HttpApiEndpoint.get("installation")`/local/installation` | ||
| 96 | .addSuccess(LocalInstallationSchema) | ||
| 97 | .addError(ControlPlaneBadRequestError) | ||
| 98 | .addError(ControlPlaneNotFoundError) | ||
| 99 | .addError(ControlPlaneStorageError), | ||
| 100 | ) | ||
| 101 | .add( | ||
| 102 | HttpApiEndpoint.get("config")`/local/config` | ||
| 103 | .addSuccess(InstanceConfigSchema), | ||
| 104 | ) | ||
| 105 | .add( | ||
| 106 | HttpApiEndpoint.get("listSecrets")`/local/secrets` | ||
| 107 | .addSuccess(Schema.Array(SecretListItemSchema)) | ||
| 108 | .addError(ControlPlaneStorageError), | ||
| 109 | ) | ||
| 110 | .add( | ||
| 111 | HttpApiEndpoint.post("createSecret")`/local/secrets` | ||
| 112 | .setPayload(CreateSecretPayloadSchema) | ||
| 113 | .addSuccess(CreateSecretResultSchema) | ||
| 114 | .addError(ControlPlaneBadRequestError) | ||
| 115 | .addError(ControlPlaneStorageError), | ||
| 116 | ) | ||
| 117 | .add( | ||
| 118 | HttpApiEndpoint.patch("updateSecret")`/local/secrets/${HttpApiSchema.param("secretId", Schema.String)}` | ||
| 119 | .setPayload(UpdateSecretPayloadSchema) | ||
| 120 | .addSuccess(UpdateSecretResultSchema) | ||
| 121 | .addError(ControlPlaneBadRequestError) | ||
| 122 | .addError(ControlPlaneNotFoundError) | ||
| 123 | .addError(ControlPlaneStorageError), | ||
| 124 | ) | ||
| 125 | .add( | ||
| 126 | HttpApiEndpoint.del("deleteSecret")`/local/secrets/${HttpApiSchema.param("secretId", Schema.String)}` | ||
| 127 | .addSuccess(DeleteSecretResultSchema) | ||
| 128 | .addError(ControlPlaneNotFoundError) | ||
| 129 | .addError(ControlPlaneStorageError), | ||
| 130 | ) | ||
| 131 | .prefix("/v1") {} | ||
| @@ -3,28 +3,29 @@ import type { · 2 unmodified lines | |||
| 3 | ExecutionInteraction, | 3 | ExecutionInteraction, |
| 4 | Source, | 4 | Source, |
| 5 | WorkspaceId, | 5 | WorkspaceId, |
| 6 | } from "#schema"; | 6 | } from "../schema"; |
| 7 | import { | 7 | import { |
| 8 | SourceIdSchema, | 8 | SourceIdSchema, |
| 9 | WorkspaceIdSchema, | 9 | WorkspaceIdSchema, |
| 10 | } from "#schema"; | 10 | } from "../schema"; |
| 11 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Option from "effect/Option"; | 12 | import * as Option from "effect/Option"; |
| 13 | 13 | ||
| 14 | import { submitExecutionInteractionResponse } from "../execution/service"; | 14 | import { submitExecutionInteractionResponse } from "../runtime/execution/service"; |
| 15 | import { LiveExecutionManagerService } from "../execution/live"; | 15 | import { LiveExecutionManagerService } from "../runtime/execution/live"; |
| 16 | import { operationErrors } from "../runtime/policy/operation-errors"; | ||
| 17 | import { resolveLocalWorkspaceContext } from "../runtime/local/config"; | ||
| 16 | import { | 18 | import { |
| 17 | operationErrors, | 19 | InstallationStore, |
| 18 | } from "../policy/operation-errors"; | 20 | LocalInstallationStore, |
| 19 | import { resolveLocalWorkspaceContext } from "./config"; | 21 | } from "../runtime/local/storage"; |
| 20 | import { InstallationStore, LocalInstallationStore } from "./storage"; | 22 | import { getRuntimeLocalWorkspaceOption } from "../runtime/local/runtime-context"; |
| 21 | import { getRuntimeLocalWorkspaceOption } from "./runtime-context"; | 23 | import { RuntimeSourceAuthServiceTag } from "../runtime/sources/source-auth-service"; |
| 22 | import { RuntimeSourceAuthServiceTag } from "../sources/source-auth-service"; | ||
| 23 | import { | 24 | import { |
| 24 | createSourceCredentialSelectionBearerContent, | 25 | createSourceCredentialSelectionBearerContent, |
| 25 | createSourceCredentialSelectionNoneContent, | 26 | createSourceCredentialSelectionNoneContent, |
| 26 | } from "../sources/source-credential-interactions"; | 27 | } from "../runtime/sources/source-credential-interactions"; |
| 27 | import { ControlPlaneStore } from "../store"; | 28 | import { ControlPlaneStore } from "../runtime/store"; |
| 28 | 29 | ||
| 29 | const localOps = { | 30 | const localOps = { |
| 30 | installation: operationErrors("local.installation.get"), | 31 | installation: operationErrors("local.installation.get"), |
| @@ -59,13 +60,15 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 28 unmodified lines | |||
| 59 | return trimmed.length > 0 ? trimmed : null; | 60 | return trimmed.length > 0 ? trimmed : null; |
| 60 | }; | 61 | }; |
| 61 | 62 | ||
| 62 | const decodeSourceCredentialInteraction = ( | 63 | export const decodeSourceCredentialInteraction = ( |
| 63 | interaction: ExecutionInteraction, | 64 | interaction: ExecutionInteraction, |
| 64 | ): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => { | 65 | ): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => { |
| 65 | try { | 66 | try { |
| 66 | if (interaction.purpose !== "source_connect_oauth2" | 67 | if ( |
| 67 | && interaction.purpose !== "source_connect_secret" | 68 | interaction.purpose !== "source_connect_oauth2" && |
| 68 | && interaction.purpose !== "elicitation") { | 69 | interaction.purpose !== "source_connect_secret" && |
| 70 | interaction.purpose !== "elicitation" | ||
| 71 | ) { | ||
| 69 | return null; | 72 | return null; |
| 70 | } | 73 | } |
| 71 | 74 | ||
| @@ -80,18 +83,25 @@ const decodeSourceCredentialInteraction = ( · 8 unmodified lines | |||
| 80 | return null; | 83 | return null; |
| 81 | } | 84 | } |
| 82 | 85 | ||
| 83 | if (payload.path !== "executor.sources.add" || args.kind !== "openapi") { | 86 | if (payload.path !== "executor.sources.add") { |
| 84 | return null; | 87 | return null; |
| 85 | } | 88 | } |
| 86 | const effectivePurpose = interaction.purpose === "elicitation" | 89 | const effectivePurpose = |
| 87 | ? elicitation.mode === "url" | 90 | interaction.purpose === "elicitation" |
| 88 | ? "source_connect_oauth2" | 91 | ? elicitation.mode === "url" |
| 89 | : "source_connect_secret" | 92 | ? "source_connect_oauth2" |
| 90 | : interaction.purpose; | 93 | : "source_connect_secret" |
| 91 | if (effectivePurpose === "source_connect_oauth2" && elicitation.mode !== "url") { | 94 | : interaction.purpose; |
| 95 | if ( | ||
| 96 | effectivePurpose === "source_connect_oauth2" && | ||
| 97 | elicitation.mode !== "url" | ||
| 98 | ) { | ||
| 92 | return null; | 99 | return null; |
| 93 | } | 100 | } |
| 94 | if (effectivePurpose === "source_connect_secret" && elicitation.mode !== "form") { | 101 | if ( |
| 102 | effectivePurpose === "source_connect_secret" && | ||
| 103 | elicitation.mode !== "form" | ||
| 104 | ) { | ||
| 95 | return null; | 105 | return null; |
| 96 | } | 106 | } |
| 97 | 107 | ||
| @@ -127,45 +137,49 @@ const loadSourceCredentialInteraction = (input: { · 29 unmodified lines | |||
| 127 | const store = yield* ControlPlaneStore; | 137 | const store = yield* ControlPlaneStore; |
| 128 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 138 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 129 | 139 | ||
| 130 | const stored = yield* store.executionInteractions.getById(input.interactionId).pipe( | 140 | const stored = yield* store.executionInteractions |
| 131 | Effect.mapError((error) => | 141 | .getById(input.interactionId) |
| 132 | input.operation.unknownStorage( | 142 | .pipe( |
| 133 | error, | 143 | Effect.mapError((error) => |
| 134 | `Failed loading execution interaction ${input.interactionId}`, | 144 | input.operation.unknownStorage( |
| 145 | error, | ||
| 146 | `Failed loading execution interaction ${input.interactionId}`, | ||
| 147 | ), | ||
| 135 | ), | 148 | ), |
| 136 | ), | 149 | ); |
| 137 | ); | ||
| 138 | 150 | ||
| 139 | if (Option.isNone(stored)) { | 151 | if (Option.isNone(stored)) { |
| 140 | return yield* input.operation.notFound( | 152 | return yield* input.operation.notFound( |
| 141 | "Source credential request not found", | 153 | "Source credential request not found", |
| 142 | `interactionId=${input.interactionId}`, | 154 | `interactionId=${input.interactionId}`, |
| 143 | ); | 155 | ); |
| 144 | } | 156 | } |
| 145 | 157 | ||
| 146 | const decoded = decodeSourceCredentialInteraction(stored.value); | 158 | const decoded = decodeSourceCredentialInteraction(stored.value); |
| 147 | if ( | 159 | if ( |
| 148 | decoded === null | 160 | decoded === null || |
| 149 | || decoded.workspaceId !== input.workspaceId | 161 | decoded.workspaceId !== input.workspaceId || |
| 150 | || decoded.sourceId !== input.sourceId | 162 | decoded.sourceId !== input.sourceId |
| 151 | ) { | 163 | ) { |
| 152 | return yield* input.operation.notFound( | 164 | return yield* input.operation.notFound( |
| 153 | "Source credential request not found", | 165 | "Source credential request not found", |
| 154 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`, | 166 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`, |
| 155 | ); | 167 | ); |
| 156 | } | 168 | } |
| 157 | 169 | ||
| 158 | const source = yield* sourceAuthService.getSourceById({ | 170 | const source = yield* sourceAuthService |
| 159 | workspaceId: input.workspaceId, | 171 | .getSourceById({ |
| 160 | sourceId: input.sourceId, | 172 | workspaceId: input.workspaceId, |
| 161 | }).pipe( | 173 | sourceId: input.sourceId, |
| 162 | Effect.mapError((error) => | 174 | }) |
| 163 | input.operation.unknownStorage( | 175 | .pipe( |
| 164 | error, | 176 | Effect.mapError((error) => |
| 165 | `Failed loading source ${input.sourceId}`, | 177 | input.operation.unknownStorage( |
| 178 | error, | ||
| 179 | `Failed loading source ${input.sourceId}`, | ||
| 180 | ), | ||
| 166 | ), | 181 | ), |
| 167 | ), | 182 | ); |
| 168 | ); | ||
| 169 | 183 | ||
| 170 | return { | 184 | return { |
| 171 | ...decoded, | 185 | ...decoded, |
| @@ -177,23 +191,23 @@ const loadSourceCredentialInteraction = (input: { · 5 unmodified lines | |||
| 177 | export const getLocalInstallation = () => | 191 | export const getLocalInstallation = () => |
| 178 | Effect.gen(function* () { | 192 | Effect.gen(function* () { |
| 179 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | 193 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 180 | const context = runtimeLocalWorkspace?.context | 194 | const context = |
| 181 | ?? (yield* resolveLocalWorkspaceContext().pipe( | 195 | runtimeLocalWorkspace?.context ?? |
| 196 | (yield* resolveLocalWorkspaceContext().pipe( | ||
| 182 | Effect.mapError((error) => | 197 | Effect.mapError((error) => |
| 183 | localOps.installation.unknownStorage( | 198 | localOps.installation.unknownStorage( |
| 184 | error, | 199 | error, |
| 185 | "Failed resolving local workspace context", | 200 | "Failed resolving local workspace context", |
| 186 | ), | 201 | ), |
| 187 | ), | 202 | ), |
| 188 | )); | 203 | )); |
| 189 | 204 | ||
| 190 | const installationStore = runtimeLocalWorkspace !== null | 205 | const installationStore = |
| 191 | ? yield* InstallationStore | 206 | runtimeLocalWorkspace !== null |
| 192 | : LocalInstallationStore; | 207 | ? yield* InstallationStore |
| 208 | : LocalInstallationStore; | ||
| 193 | 209 | ||
| 194 | return yield* installationStore.load( | 210 | return yield* installationStore.load(context); |
| 195 | context, | ||
| 196 | ); | ||
| 197 | }); | 211 | }); |
| 198 | 212 | ||
| 199 | export const getSourceCredentialInteraction = (input: { | 213 | export const getSourceCredentialInteraction = (input: { |
| @@ -223,9 +237,9 @@ export const submitSourceCredentialInteraction = (input: { · 23 unmodified lines | |||
| 223 | 237 | ||
| 224 | if (interaction.status !== "pending") { | 238 | if (interaction.status !== "pending") { |
| 225 | return yield* localOps.sourceCredentialSubmit.badRequest( | 239 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 226 | "Source credential request is no longer active", | 240 | "Source credential request is no longer active", |
| 227 | `interactionId=${interaction.interactionId} status=${interaction.status}`, | 241 | `interactionId=${interaction.interactionId} status=${interaction.status}`, |
| 228 | ); | 242 | ); |
| 229 | } | 243 | } |
| 230 | 244 | ||
| 231 | const liveExecutionManager = yield* LiveExecutionManagerService; | 245 | const liveExecutionManager = yield* LiveExecutionManagerService; |
| @@ -249,15 +263,15 @@ export const submitSourceCredentialInteraction = (input: { · 17 unmodified lines | |||
| 249 | localOps.sourceCredentialSubmit.unknownStorage( | 263 | localOps.sourceCredentialSubmit.unknownStorage( |
| 250 | error, | 264 | error, |
| 251 | `Failed resuming execution for interaction ${interaction.interactionId}`, | 265 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 252 | ) | 266 | ), |
| 253 | ), | 267 | ), |
| 254 | ); | 268 | ); |
| 255 | 269 | ||
| 256 | if (!persisted) { | 270 | if (!persisted) { |
| 257 | return yield* localOps.sourceCredentialSubmit.badRequest( | 271 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 258 | "Source credential request is no longer resumable", | 272 | "Source credential request is no longer resumable", |
| 259 | `interactionId=${interaction.interactionId}`, | 273 | `interactionId=${interaction.interactionId}`, |
| 260 | ); | 274 | ); |
| 261 | } | 275 | } |
| 262 | } | 276 | } |
| 263 | 277 | ||
| @@ -289,15 +303,15 @@ export const submitSourceCredentialInteraction = (input: { · 25 unmodified lines | |||
| 289 | localOps.sourceCredentialSubmit.unknownStorage( | 303 | localOps.sourceCredentialSubmit.unknownStorage( |
| 290 | error, | 304 | error, |
| 291 | `Failed resuming execution for interaction ${interaction.interactionId}`, | 305 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 292 | ) | 306 | ), |
| 293 | ), | 307 | ), |
| 294 | ); | 308 | ); |
| 295 | 309 | ||
| 296 | if (!persisted) { | 310 | if (!persisted) { |
| 297 | return yield* localOps.sourceCredentialSubmit.badRequest( | 311 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 298 | "Source credential request is no longer resumable", | 312 | "Source credential request is no longer resumable", |
| 299 | `interactionId=${interaction.interactionId}`, | 313 | `interactionId=${interaction.interactionId}`, |
| 300 | ); | 314 | ); |
| 301 | } | 315 | } |
| 302 | } | 316 | } |
| 303 | 317 | ||
| @@ -311,23 +325,25 @@ export const submitSourceCredentialInteraction = (input: { · 7 unmodified lines | |||
| 311 | const token = trimOrNull(input.token); | 325 | const token = trimOrNull(input.token); |
| 312 | if (token === null) { | 326 | if (token === null) { |
| 313 | return yield* localOps.sourceCredentialSubmit.badRequest( | 327 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 314 | "Missing token", | 328 | "Missing token", |
| 315 | `interactionId=${interaction.interactionId}`, | 329 | `interactionId=${interaction.interactionId}`, |
| 316 | ); | 330 | ); |
| 317 | } | 331 | } |
| 318 | 332 | ||
| 319 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 333 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 320 | const tokenRef = yield* sourceAuthService.storeSecretMaterial({ | 334 | const tokenRef = yield* sourceAuthService |
| 321 | purpose: "auth_material", | 335 | .storeSecretMaterial({ |
| 322 | value: token, | 336 | purpose: "auth_material", |
| 323 | }).pipe( | 337 | value: token, |
| 324 | Effect.mapError((error) => | 338 | }) |
| 325 | localOps.sourceCredentialSubmit.unknownStorage( | 339 | .pipe( |
| 326 | error, | 340 | Effect.mapError((error) => |
| 327 | `Failed storing credential material for interaction ${interaction.interactionId}`, | 341 | localOps.sourceCredentialSubmit.unknownStorage( |
| 342 | error, | ||
| 343 | `Failed storing credential material for interaction ${interaction.interactionId}`, | ||
| 344 | ), | ||
| 328 | ), | 345 | ), |
| 329 | ), | 346 | ); |
| 330 | ); | ||
| 331 | const resumed = yield* liveExecutionManager.resolveInteraction({ | 347 | const resumed = yield* liveExecutionManager.resolveInteraction({ |
| 332 | executionId: interaction.executionId, | 348 | executionId: interaction.executionId, |
| 333 | response: { | 349 | response: { |
| @@ -348,15 +364,15 @@ export const submitSourceCredentialInteraction = (input: { · 14 unmodified lines | |||
| 348 | localOps.sourceCredentialSubmit.unknownStorage( | 364 | localOps.sourceCredentialSubmit.unknownStorage( |
| 349 | error, | 365 | error, |
| 350 | `Failed resuming execution for interaction ${interaction.interactionId}`, | 366 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 351 | ) | 367 | ), |
| 352 | ), | 368 | ), |
| 353 | ); | 369 | ); |
| 354 | 370 | ||
| 355 | if (!persisted) { | 371 | if (!persisted) { |
| 356 | return yield* localOps.sourceCredentialSubmit.badRequest( | 372 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 357 | "Source credential request is no longer resumable", | 373 | "Source credential request is no longer resumable", |
| 358 | `interactionId=${interaction.interactionId}`, | 374 | `interactionId=${interaction.interactionId}`, |
| 359 | ); | 375 | ); |
| 360 | } | 376 | } |
| 361 | } | 377 | } |
| 362 | 378 | ||
| @@ -378,12 +394,14 @@ export const completeSourceCredentialSetup = (input: { · 15 unmodified lines | |||
| 378 | Effect.gen(function* () { | 394 | Effect.gen(function* () { |
| 379 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 395 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 380 | 396 | ||
| 381 | return yield* sourceAuthService.completeSourceCredentialSetup(input).pipe( | 397 | return yield* sourceAuthService |
| 382 | Effect.mapError((error) => | 398 | .completeSourceCredentialSetup(input) |
| 383 | localOps.sourceCredentialComplete.unknownStorage( | 399 | .pipe( |
| 384 | error, | 400 | Effect.mapError((error) => |
| 385 | "Failed completing source credential setup", | 401 | localOps.sourceCredentialComplete.unknownStorage( |
| 402 | error, | ||
| 403 | "Failed completing source credential setup", | ||
| 404 | ), | ||
| 386 | ), | 405 | ), |
| 387 | ), | 406 | ); |
| 388 | ); | ||
| 389 | }); | 407 | }); |
| @@ -0,0 +1,283 @@ | |||
| 1 | import * as Effect from "effect/Effect"; | ||
| 2 | import * as Option from "effect/Option"; | ||
| 3 | |||
| 4 | import { SecretMaterialIdSchema } from "../schema"; | ||
| 5 | import { | ||
| 6 | type CreateSecretPayload, | ||
| 7 | type CreateSecretResult, | ||
| 8 | type DeleteSecretResult, | ||
| 9 | type InstanceConfig, | ||
| 10 | type SecretListItem, | ||
| 11 | type UpdateSecretPayload, | ||
| 12 | type UpdateSecretResult, | ||
| 13 | } from "./contracts"; | ||
| 14 | import { | ||
| 15 | ControlPlaneBadRequestError, | ||
| 16 | ControlPlaneNotFoundError, | ||
| 17 | ControlPlaneStorageError, | ||
| 18 | } from "../errors"; | ||
| 19 | import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context"; | ||
| 20 | import { | ||
| 21 | createDefaultSecretMaterialDeleter, | ||
| 22 | createDefaultSecretMaterialStorer, | ||
| 23 | createDefaultSecretMaterialUpdater, | ||
| 24 | ENV_SECRET_PROVIDER_ID, | ||
| 25 | KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 26 | LOCAL_SECRET_PROVIDER_ID, | ||
| 27 | parseSecretStoreProviderId, | ||
| 28 | resolveDefaultSecretStoreProviderId, | ||
| 29 | } from "../runtime/local/secret-material-providers"; | ||
| 30 | import { RuntimeSourceStoreService } from "../runtime/sources/source-store"; | ||
| 31 | import { ControlPlaneStore } from "../runtime/store"; | ||
| 32 | |||
| 33 | const SECRET_STORE_PROVIDER_ENV = "EXECUTOR_SECRET_STORE_PROVIDER"; | ||
| 34 | |||
| 35 | const secretStorageError = (operation: string, message: string) => | ||
| 36 | new ControlPlaneStorageError({ | ||
| 37 | operation, | ||
| 38 | message, | ||
| 39 | details: message, | ||
| 40 | }); | ||
| 41 | |||
| 42 | export const getLocalInstanceConfig = (): Effect.Effect<InstanceConfig> => { | ||
| 43 | const explicitDefaultStoreProvider = parseSecretStoreProviderId( | ||
| 44 | process.env[SECRET_STORE_PROVIDER_ENV], | ||
| 45 | ); | ||
| 46 | const providers = [ | ||
| 47 | { | ||
| 48 | id: LOCAL_SECRET_PROVIDER_ID, | ||
| 49 | name: "Local store", | ||
| 50 | canStore: true, | ||
| 51 | }, | ||
| 52 | ]; | ||
| 53 | |||
| 54 | if (process.platform === "darwin" || process.platform === "linux") { | ||
| 55 | providers.push({ | ||
| 56 | id: KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 57 | name: | ||
| 58 | process.platform === "darwin" ? "macOS Keychain" : "Desktop Keyring", | ||
| 59 | canStore: | ||
| 60 | process.platform === "darwin" || | ||
| 61 | explicitDefaultStoreProvider === KEYCHAIN_SECRET_PROVIDER_ID, | ||
| 62 | }); | ||
| 63 | } | ||
| 64 | |||
| 65 | providers.push({ | ||
| 66 | id: ENV_SECRET_PROVIDER_ID, | ||
| 67 | name: "Environment variable", | ||
| 68 | canStore: false, | ||
| 69 | }); | ||
| 70 | |||
| 71 | return resolveDefaultSecretStoreProviderId({ | ||
| 72 | storeProviderId: explicitDefaultStoreProvider ?? undefined, | ||
| 73 | }).pipe( | ||
| 74 | Effect.map((resolvedDefaultStoreProvider) => ({ | ||
| 75 | platform: process.platform, | ||
| 76 | secretProviders: providers, | ||
| 77 | defaultSecretStoreProvider: resolvedDefaultStoreProvider, | ||
| 78 | })), | ||
| 79 | ); | ||
| 80 | }; | ||
| 81 | |||
| 82 | export const listLocalSecrets = () => | ||
| 83 | Effect.gen(function* () { | ||
| 84 | const store = yield* ControlPlaneStore; | ||
| 85 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 86 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace().pipe( | ||
| 87 | Effect.mapError(() => | ||
| 88 | secretStorageError("secrets.list", "Failed resolving local workspace."), | ||
| 89 | ), | ||
| 90 | ); | ||
| 91 | const rows = yield* store.secretMaterials | ||
| 92 | .listAll() | ||
| 93 | .pipe( | ||
| 94 | Effect.mapError(() => | ||
| 95 | secretStorageError("secrets.list", "Failed listing secrets."), | ||
| 96 | ), | ||
| 97 | ); | ||
| 98 | const linkedSourcesMap = yield* sourceStore | ||
| 99 | .listLinkedSecretSourcesInWorkspace( | ||
| 100 | runtimeLocalWorkspace.installation.workspaceId, | ||
| 101 | { | ||
| 102 | actorAccountId: runtimeLocalWorkspace.installation.accountId, | ||
| 103 | }, | ||
| 104 | ) | ||
| 105 | .pipe( | ||
| 106 | Effect.mapError(() => | ||
| 107 | secretStorageError("secrets.list", "Failed loading linked sources."), | ||
| 108 | ), | ||
| 109 | ); | ||
| 110 | |||
| 111 | return rows.map((row) => ({ | ||
| 112 | ...row, | ||
| 113 | linkedSources: linkedSourcesMap.get(row.id) ?? [], | ||
| 114 | })); | ||
| 115 | }); | ||
| 116 | |||
| 117 | export const createLocalSecret = (payload: CreateSecretPayload) => | ||
| 118 | Effect.gen(function* () { | ||
| 119 | const name = payload.name.trim(); | ||
| 120 | const value = payload.value; | ||
| 121 | const purpose = payload.purpose ?? "auth_material"; | ||
| 122 | const requestedProviderId = | ||
| 123 | payload.providerId === undefined | ||
| 124 | ? null | ||
| 125 | : parseSecretStoreProviderId(payload.providerId); | ||
| 126 | |||
| 127 | if (name.length === 0) { | ||
| 128 | return yield* new ControlPlaneBadRequestError({ | ||
| 129 | operation: "secrets.create", | ||
| 130 | message: "Secret name is required.", | ||
| 131 | details: "Secret name is required.", | ||
| 132 | }); | ||
| 133 | } | ||
| 134 | if (payload.providerId !== undefined && requestedProviderId === null) { | ||
| 135 | return yield* new ControlPlaneBadRequestError({ | ||
| 136 | operation: "secrets.create", | ||
| 137 | message: `Unsupported secret provider: ${payload.providerId}`, | ||
| 138 | details: `Unsupported secret provider: ${payload.providerId}`, | ||
| 139 | }); | ||
| 140 | } | ||
| 141 | |||
| 142 | const store = yield* ControlPlaneStore; | ||
| 143 | const storeSecretMaterial = createDefaultSecretMaterialStorer({ | ||
| 144 | rows: store, | ||
| 145 | ...(requestedProviderId ? { storeProviderId: requestedProviderId } : {}), | ||
| 146 | }); | ||
| 147 | const ref = yield* storeSecretMaterial({ | ||
| 148 | name, | ||
| 149 | purpose, | ||
| 150 | value, | ||
| 151 | }).pipe( | ||
| 152 | Effect.mapError((cause) => | ||
| 153 | secretStorageError( | ||
| 154 | "secrets.create", | ||
| 155 | cause instanceof Error ? cause.message : "Failed creating secret.", | ||
| 156 | ), | ||
| 157 | ), | ||
| 158 | ); | ||
| 159 | const secretId = SecretMaterialIdSchema.make(ref.handle); | ||
| 160 | const created = yield* store.secretMaterials | ||
| 161 | .getById(secretId) | ||
| 162 | .pipe( | ||
| 163 | Effect.mapError(() => | ||
| 164 | secretStorageError( | ||
| 165 | "secrets.create", | ||
| 166 | "Failed loading created secret.", | ||
| 167 | ), | ||
| 168 | ), | ||
| 169 | ); | ||
| 170 | |||
| 171 | if (Option.isNone(created)) { | ||
| 172 | return yield* secretStorageError( | ||
| 173 | "secrets.create", | ||
| 174 | `Created secret not found: ${ref.handle}`, | ||
| 175 | ); | ||
| 176 | } | ||
| 177 | |||
| 178 | return { | ||
| 179 | id: created.value.id, | ||
| 180 | name: created.value.name, | ||
| 181 | providerId: created.value.providerId, | ||
| 182 | purpose: created.value.purpose, | ||
| 183 | createdAt: created.value.createdAt, | ||
| 184 | updatedAt: created.value.updatedAt, | ||
| 185 | } satisfies CreateSecretResult; | ||
| 186 | }); | ||
| 187 | |||
| 188 | export const updateLocalSecret = (input: { | ||
| 189 | secretId: string; | ||
| 190 | payload: UpdateSecretPayload; | ||
| 191 | }) => | ||
| 192 | Effect.gen(function* () { | ||
| 193 | const secretId = SecretMaterialIdSchema.make(input.secretId); | ||
| 194 | const store = yield* ControlPlaneStore; | ||
| 195 | |||
| 196 | const existing = yield* store.secretMaterials | ||
| 197 | .getById(secretId) | ||
| 198 | .pipe( | ||
| 199 | Effect.mapError(() => | ||
| 200 | secretStorageError("secrets.update", "Failed looking up secret."), | ||
| 201 | ), | ||
| 202 | ); | ||
| 203 | |||
| 204 | if (Option.isNone(existing)) { | ||
| 205 | return yield* new ControlPlaneNotFoundError({ | ||
| 206 | operation: "secrets.update", | ||
| 207 | message: `Secret not found: ${input.secretId}`, | ||
| 208 | details: `Secret not found: ${input.secretId}`, | ||
| 209 | }); | ||
| 210 | } | ||
| 211 | |||
| 212 | const update: { name?: string | null; value?: string } = {}; | ||
| 213 | if (input.payload.name !== undefined) | ||
| 214 | update.name = input.payload.name.trim() || null; | ||
| 215 | if (input.payload.value !== undefined) update.value = input.payload.value; | ||
| 216 | |||
| 217 | const updateSecretMaterial = createDefaultSecretMaterialUpdater({ | ||
| 218 | rows: store, | ||
| 219 | }); | ||
| 220 | const updated = yield* updateSecretMaterial({ | ||
| 221 | ref: { | ||
| 222 | providerId: existing.value.providerId, | ||
| 223 | handle: existing.value.id, | ||
| 224 | }, | ||
| 225 | ...update, | ||
| 226 | }).pipe( | ||
| 227 | Effect.mapError(() => | ||
| 228 | secretStorageError("secrets.update", "Failed updating secret."), | ||
| 229 | ), | ||
| 230 | ); | ||
| 231 | |||
| 232 | return { | ||
| 233 | id: updated.id, | ||
| 234 | providerId: updated.providerId, | ||
| 235 | name: updated.name, | ||
| 236 | purpose: updated.purpose, | ||
| 237 | createdAt: updated.createdAt, | ||
| 238 | updatedAt: updated.updatedAt, | ||
| 239 | } satisfies UpdateSecretResult; | ||
| 240 | }); | ||
| 241 | |||
| 242 | export const deleteLocalSecret = (secretId: string) => | ||
| 243 | Effect.gen(function* () { | ||
| 244 | const parsedSecretId = SecretMaterialIdSchema.make(secretId); | ||
| 245 | const store = yield* ControlPlaneStore; | ||
| 246 | |||
| 247 | const existing = yield* store.secretMaterials | ||
| 248 | .getById(parsedSecretId) | ||
| 249 | .pipe( | ||
| 250 | Effect.mapError(() => | ||
| 251 | secretStorageError("secrets.delete", "Failed looking up secret."), | ||
| 252 | ), | ||
| 253 | ); | ||
| 254 | |||
| 255 | if (Option.isNone(existing)) { | ||
| 256 | return yield* new ControlPlaneNotFoundError({ | ||
| 257 | operation: "secrets.delete", | ||
| 258 | message: `Secret not found: ${secretId}`, | ||
| 259 | details: `Secret not found: ${secretId}`, | ||
| 260 | }); | ||
| 261 | } | ||
| 262 | |||
| 263 | const deleteSecretMaterial = createDefaultSecretMaterialDeleter({ | ||
| 264 | rows: store, | ||
| 265 | }); | ||
| 266 | const removed = yield* deleteSecretMaterial({ | ||
| 267 | providerId: existing.value.providerId, | ||
| 268 | handle: existing.value.id, | ||
| 269 | }).pipe( | ||
| 270 | Effect.mapError(() => | ||
| 271 | secretStorageError("secrets.delete", "Failed removing secret."), | ||
| 272 | ), | ||
| 273 | ); | ||
| 274 | |||
| 275 | if (!removed) { | ||
| 276 | return yield* secretStorageError( | ||
| 277 | "secrets.delete", | ||
| 278 | `Failed removing secret: ${secretId}`, | ||
| 279 | ); | ||
| 280 | } | ||
| 281 | |||
| 282 | return { removed: true } satisfies DeleteSecretResult; | ||
| 283 | }); | ||
| @@ -0,0 +1,23 @@ | |||
| 1 | import { | ||
| 2 | LocalWorkspacePolicyApprovalModeSchema, | ||
| 3 | LocalWorkspacePolicyEffectSchema, | ||
| 4 | } from "../schema"; | ||
| 5 | import * as Schema from "effect/Schema"; | ||
| 6 | |||
| 7 | import { OptionalTrimmedNonEmptyStringSchema } from "../string-schemas"; | ||
| 8 | |||
| 9 | const LocalWorkspacePolicyPayloadSchema = Schema.Struct({ | ||
| 10 | resourcePattern: OptionalTrimmedNonEmptyStringSchema, | ||
| 11 | effect: Schema.optional(LocalWorkspacePolicyEffectSchema), | ||
| 12 | approvalMode: Schema.optional(LocalWorkspacePolicyApprovalModeSchema), | ||
| 13 | priority: Schema.optional(Schema.Number), | ||
| 14 | enabled: Schema.optional(Schema.Boolean), | ||
| 15 | }); | ||
| 16 | |||
| 17 | export const CreatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema; | ||
| 18 | |||
| 19 | export type CreatePolicyPayload = typeof CreatePolicyPayloadSchema.Type; | ||
| 20 | |||
| 21 | export const UpdatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema; | ||
| 22 | |||
| 23 | export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type; | ||
| @@ -3,38 +3,32 @@ import { createHash } from "node:crypto"; · 2 unmodified lines | |||
| 3 | import type { | 3 | import type { |
| 4 | CreatePolicyPayload, | 4 | CreatePolicyPayload, |
| 5 | UpdatePolicyPayload, | 5 | UpdatePolicyPayload, |
| 6 | } from "../../api/policies/api"; | 6 | } from "./contracts"; |
| 7 | import { | 7 | import { |
| 8 | PolicyIdSchema, | 8 | PolicyIdSchema, |
| 9 | type LocalExecutorConfig, | 9 | type LocalExecutorConfig, |
| 10 | type LocalWorkspacePolicy, | 10 | type LocalWorkspacePolicy, |
| 11 | type PolicyId, | 11 | type PolicyId, |
| 12 | type WorkspaceId, | 12 | type WorkspaceId, |
| 13 | } from "#schema"; | 13 | } from "../schema"; |
| 14 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 15 | 15 | ||
| 16 | import { | 16 | import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context"; |
| 17 | requireRuntimeLocalWorkspace, | ||
| 18 | } from "../local/runtime-context"; | ||
| 19 | import type { | 17 | import type { |
| 20 | WorkspaceConfigStoreShape, | 18 | WorkspaceConfigStoreShape, |
| 21 | WorkspaceStateStoreShape, | 19 | WorkspaceStateStoreShape, |
| 22 | } from "../local/storage"; | 20 | } from "../runtime/local/storage"; |
| 23 | import { | 21 | import { |
| 24 | WorkspaceConfigStore, | 22 | WorkspaceConfigStore, |
| 25 | WorkspaceStateStore, | 23 | WorkspaceStateStore, |
| 26 | } from "../local/storage"; | 24 | } from "../runtime/local/storage"; |
| 27 | import type { ResolvedLocalWorkspaceContext } from "../local/config"; | 25 | import type { ResolvedLocalWorkspaceContext } from "../runtime/local/config"; |
| 28 | import { | 26 | import { type LocalWorkspaceState } from "../runtime/local/workspace-state"; |
| 29 | type LocalWorkspaceState, | 27 | import { derivePolicyConfigKey } from "../runtime/local/workspace-sync"; |
| 30 | } from "../local/workspace-state"; | ||
| 31 | import { | ||
| 32 | derivePolicyConfigKey, | ||
| 33 | } from "../local/workspace-sync"; | ||
| 34 | import { | 28 | import { |
| 35 | type OperationErrors, | 29 | type OperationErrors, |
| 36 | operationErrors, | 30 | operationErrors, |
| 37 | } from "./operation-errors"; | 31 | } from "../runtime/policy/operation-errors"; |
| 38 | 32 | ||
| 39 | const policyOps = { | 33 | const policyOps = { |
| 40 | list: operationErrors("policies.list"), | 34 | list: operationErrors("policies.list"), |
| @@ -44,8 +38,7 @@ const policyOps = { · 3 unmodified lines | |||
| 44 | remove: operationErrors("policies.remove"), | 38 | remove: operationErrors("policies.remove"), |
| 45 | } as const; | 39 | } as const; |
| 46 | 40 | ||
| 47 | const cloneJson = <T>(value: T): T => | 41 | const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T; |
| 48 | JSON.parse(JSON.stringify(value)) as T; | ||
| 49 | 42 | ||
| 50 | const localPolicyIdForKey = (input: { | 43 | const localPolicyIdForKey = (input: { |
| 51 | workspaceRoot: string; | 44 | workspaceRoot: string; |
| @@ -62,10 +55,12 @@ const toLocalWorkspacePolicy = (input: { · 10 unmodified lines | |||
| 62 | policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string]; | 55 | policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string]; |
| 63 | state: LocalWorkspaceState["policies"][string] | undefined; | 56 | state: LocalWorkspaceState["policies"][string] | undefined; |
| 64 | }): LocalWorkspacePolicy => ({ | 57 | }): LocalWorkspacePolicy => ({ |
| 65 | id: input.state?.id ?? localPolicyIdForKey({ | 58 | id: |
| 66 | workspaceRoot: input.workspaceRoot, | 59 | input.state?.id ?? |
| 67 | key: input.key, | 60 | localPolicyIdForKey({ |
| 68 | }), | 61 | workspaceRoot: input.workspaceRoot, |
| 62 | key: input.key, | ||
| 63 | }), | ||
| 69 | key: input.key, | 64 | key: input.key, |
| 70 | workspaceId: input.workspaceId, | 65 | workspaceId: input.workspaceId, |
| 71 | resourcePattern: input.policyConfig.match.trim(), | 66 | resourcePattern: input.policyConfig.match.trim(), |
| @@ -79,7 +74,8 @@ const toLocalWorkspacePolicy = (input: { · 7 unmodified lines | |||
| 79 | 74 | ||
| 80 | export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => | 75 | export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => |
| 81 | Effect.gen(function* () { | 76 | Effect.gen(function* () { |
| 82 | const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); | 77 | const runtimeLocalWorkspace = |
| 78 | yield* requireRuntimeLocalWorkspace(workspaceId); | ||
| 83 | const workspaceConfigStore = yield* WorkspaceConfigStore; | 79 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 84 | const workspaceStateStore = yield* WorkspaceStateStore; | 80 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 85 | const loadedConfig = yield* workspaceConfigStore.load( | 81 | const loadedConfig = yield* workspaceConfigStore.load( |
| @@ -89,14 +85,16 @@ export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => · 3 unmodified lines | |||
| 89 | runtimeLocalWorkspace.context, | 85 | runtimeLocalWorkspace.context, |
| 90 | ); | 86 | ); |
| 91 | 87 | ||
| 92 | const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(([key, policyConfig]) => | 88 | const policies = Object.entries(loadedConfig.config?.policies ?? {}).map( |
| 93 | toLocalWorkspacePolicy({ | 89 | ([key, policyConfig]) => |
| 94 | workspaceId, | 90 | toLocalWorkspacePolicy({ |
| 95 | workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | 91 | workspaceId, |
| 96 | key, | 92 | workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, |
| 97 | policyConfig, | 93 | key, |
| 98 | state: workspaceState.policies[key], | 94 | policyConfig, |
| 99 | })); | 95 | state: workspaceState.policies[key], |
| 96 | }), | ||
| 97 | ); | ||
| 100 | 98 | ||
| 101 | return { | 99 | return { |
| 102 | runtimeLocalWorkspace, | 100 | runtimeLocalWorkspace, |
| @@ -114,16 +112,19 @@ const writeLocalPolicyFiles = (input: { · 11 unmodified lines | |||
| 114 | projectConfig: LocalExecutorConfig; | 112 | projectConfig: LocalExecutorConfig; |
| 115 | workspaceState: LocalWorkspaceState; | 113 | workspaceState: LocalWorkspaceState; |
| 116 | }) => | 114 | }) => |
| 117 | Effect.all([ | 115 | Effect.all( |
| 118 | input.workspaceConfigStore.writeProject({ | 116 | [ |
| 119 | context: input.context, | 117 | input.workspaceConfigStore.writeProject({ |
| 120 | config: input.projectConfig, | 118 | context: input.context, |
| 121 | }), | 119 | config: input.projectConfig, |
| 122 | input.workspaceStateStore.write({ | 120 | }), |
| 123 | context: input.context, | 121 | input.workspaceStateStore.write({ |
| 124 | state: input.workspaceState, | 122 | context: input.context, |
| 125 | }), | 123 | state: input.workspaceState, |
| 126 | ], { discard: true }).pipe( | 124 | }), |
| 125 | ], | ||
| 126 | { discard: true }, | ||
| 127 | ).pipe( | ||
| 127 | Effect.mapError((cause) => | 128 | Effect.mapError((cause) => |
| 128 | input.operation.unknownStorage( | 129 | input.operation.unknownStorage( |
| 129 | cause, | 130 | cause, |
| @@ -148,7 +149,9 @@ const loadWorkspacePolicyContext = ( · 18 unmodified lines | |||
| 148 | export const listPolicies = (workspaceId: WorkspaceId) => | 149 | export const listPolicies = (workspaceId: WorkspaceId) => |
| 149 | Effect.gen(function* () { | 150 | Effect.gen(function* () { |
| 150 | yield* loadWorkspacePolicyContext(policyOps.list, workspaceId); | 151 | yield* loadWorkspacePolicyContext(policyOps.list, workspaceId); |
| 151 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(workspaceId).pipe( | 152 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( |
| 153 | workspaceId, | ||
| 154 | ).pipe( | ||
| 152 | Effect.mapError((cause) => | 155 | Effect.mapError((cause) => |
| 153 | policyOps.list.unknownStorage( | 156 | policyOps.list.unknownStorage( |
| 154 | cause, | 157 | cause, |
| @@ -164,10 +167,15 @@ export const createPolicy = (input: { · 9 unmodified lines | |||
| 164 | payload: CreatePolicyPayload; | 167 | payload: CreatePolicyPayload; |
| 165 | }) => | 168 | }) => |
| 166 | Effect.gen(function* () { | 169 | Effect.gen(function* () { |
| 167 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId); | 170 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext( |
| 171 | policyOps.create, | ||
| 172 | input.workspaceId, | ||
| 173 | ); | ||
| 168 | const workspaceConfigStore = yield* WorkspaceConfigStore; | 174 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 169 | const workspaceStateStore = yield* WorkspaceStateStore; | 175 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 170 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | 176 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( |
| 177 | input.workspaceId, | ||
| 178 | ).pipe( | ||
| 171 | Effect.mapError((cause) => | 179 | Effect.mapError((cause) => |
| 172 | policyOps.create.unknownStorage( | 180 | policyOps.create.unknownStorage( |
| 173 | cause, | 181 | cause, |
| @@ -176,7 +184,9 @@ export const createPolicy = (input: { · 2 unmodified lines | |||
| 176 | ), | 184 | ), |
| 177 | ); | 185 | ); |
| 178 | const now = Date.now(); | 186 | const now = Date.now(); |
| 179 | const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | 187 | const projectConfig = cloneJson( |
| 188 | localWorkspace.loadedConfig.projectConfig ?? {}, | ||
| 189 | ); | ||
| 180 | const policies = { ...projectConfig.policies }; | 190 | const policies = { ...projectConfig.policies }; |
| 181 | const key = derivePolicyConfigKey( | 191 | const key = derivePolicyConfigKey( |
| 182 | { | 192 | { |
| @@ -190,9 +200,14 @@ export const createPolicy = (input: { · 7 unmodified lines | |||
| 190 | policies[key] = { | 200 | policies[key] = { |
| 191 | match: input.payload.resourcePattern ?? "*", | 201 | match: input.payload.resourcePattern ?? "*", |
| 192 | action: input.payload.effect ?? "allow", | 202 | action: input.payload.effect ?? "allow", |
| 193 | approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto", | 203 | approval: |
| 204 | (input.payload.approvalMode ?? "auto") === "required" | ||
| 205 | ? "manual" | ||
| 206 | : "auto", | ||
| 194 | ...(input.payload.enabled === false ? { enabled: false } : {}), | 207 | ...(input.payload.enabled === false ? { enabled: false } : {}), |
| 195 | ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}), | 208 | ...((input.payload.priority ?? 0) !== 0 |
| 209 | ? { priority: input.payload.priority ?? 0 } | ||
| 210 | : {}), | ||
| 196 | }; | 211 | }; |
| 197 | 212 | ||
| 198 | const existingState = localWorkspace.workspaceState.policies[key]; | 213 | const existingState = localWorkspace.workspaceState.policies[key]; |
| @@ -201,10 +216,12 @@ export const createPolicy = (input: { · 2 unmodified lines | |||
| 201 | policies: { | 216 | policies: { |
| 202 | ...localWorkspace.workspaceState.policies, | 217 | ...localWorkspace.workspaceState.policies, |
| 203 | [key]: { | 218 | [key]: { |
| 204 | id: existingState?.id ?? localPolicyIdForKey({ | 219 | id: |
| 205 | workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | 220 | existingState?.id ?? |
| 206 | key, | 221 | localPolicyIdForKey({ |
| 207 | }), | 222 | workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, |
| 223 | key, | ||
| 224 | }), | ||
| 208 | createdAt: existingState?.createdAt ?? now, | 225 | createdAt: existingState?.createdAt ?? now, |
| 209 | updatedAt: now, | 226 | updatedAt: now, |
| 210 | }, | 227 | }, |
| @@ -238,7 +255,9 @@ export const getPolicy = (input: { · 27 unmodified lines | |||
| 238 | }) => | 255 | }) => |
| 239 | Effect.gen(function* () { | 256 | Effect.gen(function* () { |
| 240 | yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId); | 257 | yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId); |
| 241 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | 258 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( |
| 259 | input.workspaceId, | ||
| 260 | ).pipe( | ||
| 242 | Effect.mapError((cause) => | 261 | Effect.mapError((cause) => |
| 243 | policyOps.get.unknownStorage( | 262 | policyOps.get.unknownStorage( |
| 244 | cause, | 263 | cause, |
| @@ -246,12 +265,15 @@ export const getPolicy = (input: { · 1 unmodified line | |||
| 246 | ), | 265 | ), |
| 247 | ), | 266 | ), |
| 248 | ); | 267 | ); |
| 249 | const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | 268 | const policy = |
| 269 | localWorkspace.policies.find( | ||
| 270 | (candidate) => candidate.id === input.policyId, | ||
| 271 | ) ?? null; | ||
| 250 | if (policy === null) { | 272 | if (policy === null) { |
| 251 | return yield* policyOps.get.notFound( | 273 | return yield* policyOps.get.notFound( |
| 252 | "Policy not found", | 274 | "Policy not found", |
| 253 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | 275 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, |
| 254 | ); | 276 | ); |
| 255 | } | 277 | } |
| 256 | return policy; | 278 | return policy; |
| 257 | }); | 279 | }); |
| @@ -262,10 +284,15 @@ export const updatePolicy = (input: { · 4 unmodified lines | |||
| 262 | payload: UpdatePolicyPayload; | 284 | payload: UpdatePolicyPayload; |
| 263 | }) => | 285 | }) => |
| 264 | Effect.gen(function* () { | 286 | Effect.gen(function* () { |
| 265 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId); | 287 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext( |
| 288 | policyOps.update, | ||
| 289 | input.workspaceId, | ||
| 290 | ); | ||
| 266 | const workspaceConfigStore = yield* WorkspaceConfigStore; | 291 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 267 | const workspaceStateStore = yield* WorkspaceStateStore; | 292 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 268 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | 293 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( |
| 294 | input.workspaceId, | ||
| 295 | ).pipe( | ||
| 269 | Effect.mapError((cause) => | 296 | Effect.mapError((cause) => |
| 270 | policyOps.update.unknownStorage( | 297 | policyOps.update.unknownStorage( |
| 271 | cause, | 298 | cause, |
| @@ -273,27 +300,43 @@ export const updatePolicy = (input: { · 1 unmodified line | |||
| 273 | ), | 300 | ), |
| 274 | ), | 301 | ), |
| 275 | ); | 302 | ); |
| 276 | const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | 303 | const existing = |
| 304 | localWorkspace.policies.find( | ||
| 305 | (candidate) => candidate.id === input.policyId, | ||
| 306 | ) ?? null; | ||
| 277 | if (existing === null) { | 307 | if (existing === null) { |
| 278 | return yield* policyOps.update.notFound( | 308 | return yield* policyOps.update.notFound( |
| 279 | "Policy not found", | 309 | "Policy not found", |
| 280 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | 310 | `workspaceId=${input.workspaceId} policyId=${input.policyId}`, |
| 281 | ); | 311 | ); |
| 282 | } | 312 | } |
| 283 | 313 | ||
| 284 | const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | 314 | const projectConfig = cloneJson( |
| 315 | localWorkspace.loadedConfig.projectConfig ?? {}, | ||
| 316 | ); | ||
| 285 | const policies = { ...projectConfig.policies }; | 317 | const policies = { ...projectConfig.policies }; |
| 286 | const existingConfig = policies[existing.key]!; | 318 | const existingConfig = policies[existing.key]!; |
| 287 | 319 | ||
| 288 | policies[existing.key] = { | 320 | policies[existing.key] = { |
| 289 | ...existingConfig, | 321 | ...existingConfig, |
| 290 | ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}), | 322 | ...(input.payload.resourcePattern !== undefined |
| 291 | ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}), | 323 | ? { match: input.payload.resourcePattern } |
| 324 | : {}), | ||
| 325 | ...(input.payload.effect !== undefined | ||
| 326 | ? { action: input.payload.effect } | ||
| 327 | : {}), | ||
| 292 | ...(input.payload.approvalMode !== undefined | 328 | ...(input.payload.approvalMode !== undefined |
| 293 | ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" } | 329 | ? { |
| 330 | approval: | ||
| 331 | input.payload.approvalMode === "required" ? "manual" : "auto", | ||
| 332 | } | ||
| 333 | : {}), | ||
| 334 | ...(input.payload.enabled !== undefined | ||
| 335 | ? { enabled: input.payload.enabled } | ||
| 336 | : {}), | ||
| 337 | ...(input.payload.priority !== undefined | ||
| 338 | ? { priority: input.payload.priority } | ||
| 294 | : {}), | 339 | : {}), |
| 295 | ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}), | ||
| 296 | ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}), | ||
| 297 | }; | 340 | }; |
| 298 | 341 | ||
| 299 | const updatedAt = Date.now(); | 342 | const updatedAt = Date.now(); |
| @@ -336,10 +379,15 @@ export const removePolicy = (input: { · 36 unmodified lines | |||
| 336 | policyId: PolicyId; | 379 | policyId: PolicyId; |
| 337 | }) => | 380 | }) => |
| 338 | Effect.gen(function* () { | 381 | Effect.gen(function* () { |
| 339 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId); | 382 | const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext( |
| 383 | policyOps.remove, | ||
| 384 | input.workspaceId, | ||
| 385 | ); | ||
| 340 | const workspaceConfigStore = yield* WorkspaceConfigStore; | 386 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 341 | const workspaceStateStore = yield* WorkspaceStateStore; | 387 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 342 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | 388 | const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( |
| 389 | input.workspaceId, | ||
| 390 | ).pipe( | ||
| 343 | Effect.mapError((cause) => | 391 | Effect.mapError((cause) => |
| 344 | policyOps.remove.unknownStorage( | 392 | policyOps.remove.unknownStorage( |
| 345 | cause, | 393 | cause, |
| @@ -347,16 +395,22 @@ export const removePolicy = (input: { · 1 unmodified line | |||
| 347 | ), | 395 | ), |
| 348 | ), | 396 | ), |
| 349 | ); | 397 | ); |
| 350 | const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | 398 | const existing = |
| 399 | localWorkspace.policies.find( | ||
| 400 | (candidate) => candidate.id === input.policyId, | ||
| 401 | ) ?? null; | ||
| 351 | if (existing === null) { | 402 | if (existing === null) { |
| 352 | return { removed: false }; | 403 | return { removed: false }; |
| 353 | } | 404 | } |
| 354 | 405 | ||
| 355 | const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | 406 | const projectConfig = cloneJson( |
| 407 | localWorkspace.loadedConfig.projectConfig ?? {}, | ||
| 408 | ); | ||
| 356 | const policies = { ...projectConfig.policies }; | 409 | const policies = { ...projectConfig.policies }; |
| 357 | delete policies[existing.key]; | 410 | delete policies[existing.key]; |
| 358 | 411 | ||
| 359 | const { [existing.key]: _removedPolicy, ...remainingPolicies } = localWorkspace.workspaceState.policies; | 412 | const { [existing.key]: _removedPolicy, ...remainingPolicies } = |
| 413 | localWorkspace.workspaceState.policies; | ||
| 360 | yield* writeLocalPolicyFiles({ | 414 | yield* writeLocalPolicyFiles({ |
| 361 | operation: policyOps.remove, | 415 | operation: policyOps.remove, |
| 362 | context: runtimeLocalWorkspace.context, | 416 | context: runtimeLocalWorkspace.context, |
| @@ -35,4 +35,4 @@ On releases where you want upgrade coverage: · 34 unmodified lines | |||
| 35 | 1. Start from a real workspace that has a fully synced source on the released version. | 35 | 1. Start from a real workspace that has a fully synced source on the released version. |
| 36 | 2. Run `bun run fixture:release:capture -- ...` for that source. | 36 | 2. Run `bun run fixture:release:capture -- ...` for that source. |
| 37 | 3. Review the fixture for secrets before committing it. | 37 | 3. Review the fixture for secrets before committing it. |
| 38 | 4. Run `bun run --cwd packages/platform/control-plane test -- src/runtime/local/source-artifacts.test.ts`. | 38 | 4. Run `bun run --cwd packages/platform/sdk test -- src/runtime/local/source-artifacts.test.ts`. |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -0,0 +1,2356 @@ | |||
| 1 | import { createServer } from "node:http"; | ||
| 2 | import { join } from "node:path"; | ||
| 3 | |||
| 4 | import { FileSystem } from "@effect/platform"; | ||
| 5 | import { NodeFileSystem } from "@effect/platform-node"; | ||
| 6 | import { describe, expect, it } from "@effect/vitest"; | ||
| 7 | import { assertTrue } from "@effect/vitest/utils"; | ||
| 8 | import * as Either from "effect/Either"; | ||
| 9 | import * as Effect from "effect/Effect"; | ||
| 10 | import * as Fiber from "effect/Fiber"; | ||
| 11 | import * as Option from "effect/Option"; | ||
| 12 | |||
| 13 | import { | ||
| 14 | ExecutionIdSchema, | ||
| 15 | ExecutionInteractionIdSchema, | ||
| 16 | type ExecutionInteraction, | ||
| 17 | ProviderAuthGrantIdSchema, | ||
| 18 | SecretMaterialIdSchema, | ||
| 19 | SourceAuthSessionIdSchema, | ||
| 20 | SourceIdSchema, | ||
| 21 | WorkspaceOauthClientIdSchema, | ||
| 22 | } from "#schema"; | ||
| 23 | import type { ToolPath } from "@executor/codemode-core"; | ||
| 24 | import { createCatalogImportMetadata } from "@executor/source-core"; | ||
| 25 | import { createGraphqlCatalogFragment } from "@executor/source-graphql"; | ||
| 26 | import { createExecutorAdminToolMap } from "@executor/platform-internal"; | ||
| 27 | |||
| 28 | import { | ||
| 29 | type ControlPlaneRuntime, | ||
| 30 | createControlPlaneRuntime, | ||
| 31 | LiveExecutionManagerService, | ||
| 32 | provideControlPlaneRuntime, | ||
| 33 | RuntimeExecutionResolverService, | ||
| 34 | } from "./index"; | ||
| 35 | import { createSourceFromPayload } from "./sources/source-definitions"; | ||
| 36 | import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions"; | ||
| 37 | import { persistSource } from "./sources/source-store"; | ||
| 38 | import { withControlPlaneClient } from "./execution/test-http-client"; | ||
| 39 | import { runtimeEffectError } from "./effect-errors"; | ||
| 40 | import { | ||
| 41 | resolveLocalWorkspaceContext, | ||
| 42 | writeProjectLocalExecutorConfig, | ||
| 43 | } from "./local/config"; | ||
| 44 | import { writeLocalControlPlaneState } from "./local/control-plane-store"; | ||
| 45 | import { deriveLocalInstallation } from "./local/installation"; | ||
| 46 | import { | ||
| 47 | decodeSourceCredentialInteraction, | ||
| 48 | submitSourceCredentialInteraction, | ||
| 49 | } from "./local/operations"; | ||
| 50 | import { | ||
| 51 | buildLocalSourceArtifact, | ||
| 52 | readLocalSourceArtifact, | ||
| 53 | writeLocalSourceArtifact, | ||
| 54 | } from "./local/source-artifacts"; | ||
| 55 | import { writeLocalWorkspaceState } from "./local/workspace-state"; | ||
| 56 | |||
| 57 | const makeRuntime = Effect.gen(function* () { | ||
| 58 | const fs = yield* FileSystem.FileSystem; | ||
| 59 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 60 | prefix: "executor-control-plane-runtime-", | ||
| 61 | }); | ||
| 62 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 63 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 64 | |||
| 65 | return yield* Effect.acquireRelease( | ||
| 66 | createControlPlaneRuntime({ | ||
| 67 | workspaceRoot, | ||
| 68 | homeConfigPath, | ||
| 69 | homeStateDirectory, | ||
| 70 | createInternalToolMap: createExecutorAdminToolMap, | ||
| 71 | }), | ||
| 72 | (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie), | ||
| 73 | ); | ||
| 74 | }).pipe(Effect.provide(NodeFileSystem.layer)); | ||
| 75 | |||
| 76 | type OpenApiSpecServer = { | ||
| 77 | baseUrl: string; | ||
| 78 | specUrl: string; | ||
| 79 | close: () => Promise<void>; | ||
| 80 | }; | ||
| 81 | |||
| 82 | type GoogleWorkspaceTestServer = { | ||
| 83 | baseUrl: string; | ||
| 84 | tokenEndpoint: string; | ||
| 85 | discoveryUrl: (input: { | ||
| 86 | service: string; | ||
| 87 | version: string; | ||
| 88 | scope: string; | ||
| 89 | }) => string; | ||
| 90 | queueTokenResponse: (response: { | ||
| 91 | access_token: string; | ||
| 92 | refresh_token?: string; | ||
| 93 | expires_in?: number; | ||
| 94 | scope?: string; | ||
| 95 | }) => void; | ||
| 96 | tokenRequests: URLSearchParams[]; | ||
| 97 | discoveryAuthorizations: string[]; | ||
| 98 | close: () => Promise<void>; | ||
| 99 | }; | ||
| 100 | |||
| 101 | const makeOpenApiSpecServer = Effect.acquireRelease( | ||
| 102 | Effect.promise<OpenApiSpecServer>( | ||
| 103 | () => | ||
| 104 | new Promise<OpenApiSpecServer>((resolve, reject) => { | ||
| 105 | const openApiDocument = JSON.stringify({ | ||
| 106 | openapi: "3.0.3", | ||
| 107 | info: { | ||
| 108 | title: "GitHub Test API", | ||
| 109 | version: "1.0.0", | ||
| 110 | }, | ||
| 111 | paths: { | ||
| 112 | "/repos/{owner}/{repo}": { | ||
| 113 | get: { | ||
| 114 | operationId: "repos/get-repo", | ||
| 115 | tags: ["repos"], | ||
| 116 | summary: "Get a repository", | ||
| 117 | responses: { | ||
| 118 | 200: { | ||
| 119 | description: "ok", | ||
| 120 | }, | ||
| 121 | }, | ||
| 122 | }, | ||
| 123 | }, | ||
| 124 | }, | ||
| 125 | }); | ||
| 126 | |||
| 127 | const server = createServer((request, response) => { | ||
| 128 | if (request.url !== "/openapi.json") { | ||
| 129 | response.statusCode = 404; | ||
| 130 | response.end(); | ||
| 131 | return; | ||
| 132 | } | ||
| 133 | |||
| 134 | response.statusCode = 200; | ||
| 135 | response.setHeader("content-type", "application/json"); | ||
| 136 | response.end(openApiDocument); | ||
| 137 | }); | ||
| 138 | |||
| 139 | server.once("error", reject); | ||
| 140 | server.listen(0, "127.0.0.1", () => { | ||
| 141 | const address = server.address(); | ||
| 142 | if (!address || typeof address === "string") { | ||
| 143 | reject(new Error("Failed to bind OpenAPI runtime test server")); | ||
| 144 | return; | ||
| 145 | } | ||
| 146 | |||
| 147 | const baseUrl = `http://127.0.0.1:${address.port}`; | ||
| 148 | resolve({ | ||
| 149 | baseUrl, | ||
| 150 | specUrl: `${baseUrl}/openapi.json`, | ||
| 151 | close: () => | ||
| 152 | new Promise<void>((closeResolve, closeReject) => { | ||
| 153 | server.close((error) => { | ||
| 154 | if (error) { | ||
| 155 | closeReject(error); | ||
| 156 | return; | ||
| 157 | } | ||
| 158 | |||
| 159 | closeResolve(); | ||
| 160 | }); | ||
| 161 | }), | ||
| 162 | }); | ||
| 163 | }); | ||
| 164 | }), | ||
| 165 | ), | ||
| 166 | (server) => Effect.promise(() => server.close()).pipe(Effect.orDie), | ||
| 167 | ); | ||
| 168 | |||
| 169 | const makeGoogleWorkspaceTestServer = Effect.acquireRelease( | ||
| 170 | Effect.promise<GoogleWorkspaceTestServer>( | ||
| 171 | () => | ||
| 172 | new Promise<GoogleWorkspaceTestServer>((resolve, reject) => { | ||
| 173 | const discoveryScopes = new Map<string, string>(); | ||
| 174 | const tokenResponses: Array<{ | ||
| 175 | access_token: string; | ||
| 176 | refresh_token?: string; | ||
| 177 | expires_in?: number; | ||
| 178 | scope?: string; | ||
| 179 | }> = []; | ||
| 180 | const tokenRequests: URLSearchParams[] = []; | ||
| 181 | const discoveryAuthorizations: string[] = []; | ||
| 182 | let baseUrl = ""; | ||
| 183 | |||
| 184 | const server = createServer(async (request, response) => { | ||
| 185 | const requestUrl = new URL( | ||
| 186 | request.url ?? "/", | ||
| 187 | `http://${request.headers.host ?? "127.0.0.1"}`, | ||
| 188 | ); | ||
| 189 | |||
| 190 | if ( | ||
| 191 | request.method === "POST" && | ||
| 192 | requestUrl.pathname === "/oauth/token" | ||
| 193 | ) { | ||
| 194 | const chunks: Buffer[] = []; | ||
| 195 | for await (const chunk of request) { | ||
| 196 | chunks.push(Buffer.from(chunk)); | ||
| 197 | } | ||
| 198 | |||
| 199 | const params = new URLSearchParams( | ||
| 200 | Buffer.concat(chunks).toString("utf8"), | ||
| 201 | ); | ||
| 202 | tokenRequests.push(params); | ||
| 203 | const nextResponse = tokenResponses.shift(); | ||
| 204 | if (!nextResponse) { | ||
| 205 | response.statusCode = 500; | ||
| 206 | response.setHeader("content-type", "application/json"); | ||
| 207 | response.end( | ||
| 208 | JSON.stringify({ | ||
| 209 | error: "missing_token_response", | ||
| 210 | }), | ||
| 211 | ); | ||
| 212 | return; | ||
| 213 | } | ||
| 214 | |||
| 215 | response.statusCode = 200; | ||
| 216 | response.setHeader("content-type", "application/json"); | ||
| 217 | response.end( | ||
| 218 | JSON.stringify({ | ||
| 219 | token_type: "Bearer", | ||
| 220 | expires_in: 3600, | ||
| 221 | ...nextResponse, | ||
| 222 | }), | ||
| 223 | ); | ||
| 224 | return; | ||
| 225 | } | ||
| 226 | |||
| 227 | const discoveryMatch = /^\/([^/]+)\/\$discovery\/rest$/.exec( | ||
| 228 | requestUrl.pathname, | ||
| 229 | ); | ||
| 230 | if (request.method === "GET" && discoveryMatch) { | ||
| 231 | const service = discoveryMatch[1]!; | ||
| 232 | const version = requestUrl.searchParams.get("version") ?? "v1"; | ||
| 233 | const authorizationHeader = | ||
| 234 | typeof request.headers.authorization === "string" | ||
| 235 | ? request.headers.authorization | ||
| 236 | : Array.isArray(request.headers.authorization) | ||
| 237 | ? (request.headers.authorization[0] ?? "") | ||
| 238 | : ""; | ||
| 239 | if (authorizationHeader.length > 0) { | ||
| 240 | discoveryAuthorizations.push(authorizationHeader); | ||
| 241 | } | ||
| 242 | |||
| 243 | const scope = | ||
| 244 | discoveryScopes.get(`${service}:${version}`) ?? | ||
| 245 | `https://example.test/auth/${service}`; | ||
| 246 | response.statusCode = 200; | ||
| 247 | response.setHeader("content-type", "application/json"); | ||
| 248 | response.end( | ||
| 249 | JSON.stringify({ | ||
| 250 | name: service, | ||
| 251 | version, | ||
| 252 | title: `${service} API`, | ||
| 253 | description: `Test ${service} API`, | ||
| 254 | rootUrl: `${baseUrl}/`, | ||
| 255 | servicePath: `${service}/${version}/`, | ||
| 256 | auth: { | ||
| 257 | oauth2: { | ||
| 258 | scopes: { | ||
| 259 | [scope]: { | ||
| 260 | description: `${service} scope`, | ||
| 261 | }, | ||
| 262 | }, | ||
| 263 | }, | ||
| 264 | }, | ||
| 265 | methods: { | ||
| 266 | list: { | ||
| 267 | id: `${service}.list`, | ||
| 268 | path: "items", | ||
| 269 | httpMethod: "GET", | ||
| 270 | response: { | ||
| 271 | $ref: "ListResponse", | ||
| 272 | }, | ||
| 273 | }, | ||
| 274 | }, | ||
| 275 | schemas: { | ||
| 276 | ListResponse: { | ||
| 277 | id: "ListResponse", | ||
| 278 | type: "object", | ||
| 279 | properties: { | ||
| 280 | items: { | ||
| 281 | type: "array", | ||
| 282 | items: { | ||
| 283 | type: "string", | ||
| 284 | }, | ||
| 285 | }, | ||
| 286 | }, | ||
| 287 | }, | ||
| 288 | }, | ||
| 289 | }), | ||
| 290 | ); | ||
| 291 | return; | ||
| 292 | } | ||
| 293 | |||
| 294 | response.statusCode = 404; | ||
| 295 | response.end(); | ||
| 296 | }); | ||
| 297 | |||
| 298 | server.once("error", reject); | ||
| 299 | server.listen(0, "127.0.0.1", () => { | ||
| 300 | const address = server.address(); | ||
| 301 | if (!address || typeof address === "string") { | ||
| 302 | reject(new Error("Failed to bind Google workspace test server")); | ||
| 303 | return; | ||
| 304 | } | ||
| 305 | |||
| 306 | baseUrl = `http://127.0.0.1:${address.port}`; | ||
| 307 | resolve({ | ||
| 308 | baseUrl, | ||
| 309 | tokenEndpoint: `${baseUrl}/oauth/token`, | ||
| 310 | discoveryUrl: ({ service, version, scope }) => { | ||
| 311 | discoveryScopes.set(`${service}:${version}`, scope); | ||
| 312 | return `${baseUrl}/${service}/$discovery/rest?version=${encodeURIComponent(version)}`; | ||
| 313 | }, | ||
| 314 | queueTokenResponse: (tokenResponse) => { | ||
| 315 | tokenResponses.push(tokenResponse); | ||
| 316 | }, | ||
| 317 | tokenRequests, | ||
| 318 | discoveryAuthorizations, | ||
| 319 | close: () => | ||
| 320 | new Promise<void>((closeResolve, closeReject) => { | ||
| 321 | server.close((error) => { | ||
| 322 | if (error) { | ||
| 323 | closeReject(error); | ||
| 324 | return; | ||
| 325 | } | ||
| 326 | |||
| 327 | closeResolve(); | ||
| 328 | }); | ||
| 329 | }), | ||
| 330 | }); | ||
| 331 | }); | ||
| 332 | }), | ||
| 333 | ), | ||
| 334 | (server) => Effect.promise(() => server.close()).pipe(Effect.orDie), | ||
| 335 | ); | ||
| 336 | |||
| 337 | const upsertLocalSecret = ( | ||
| 338 | runtime: ControlPlaneRuntime, | ||
| 339 | input: { | ||
| 340 | id: string; | ||
| 341 | name: string; | ||
| 342 | purpose: "oauth_refresh_token" | "oauth_client_info" | "oauth_access_token"; | ||
| 343 | value: string; | ||
| 344 | }, | ||
| 345 | ) => | ||
| 346 | runtime.persistence.rows.secretMaterials.upsert({ | ||
| 347 | id: SecretMaterialIdSchema.make(input.id), | ||
| 348 | providerId: "local", | ||
| 349 | handle: input.id, | ||
| 350 | name: input.name, | ||
| 351 | purpose: input.purpose, | ||
| 352 | value: input.value, | ||
| 353 | createdAt: Date.now(), | ||
| 354 | updatedAt: Date.now(), | ||
| 355 | }); | ||
| 356 | |||
| 357 | const createPersistedGoogleSource = (input: { | ||
| 358 | runtime: ControlPlaneRuntime; | ||
| 359 | name: string; | ||
| 360 | namespace: string; | ||
| 361 | service: string; | ||
| 362 | version: string; | ||
| 363 | discoveryUrl: string; | ||
| 364 | scopes: ReadonlyArray<string>; | ||
| 365 | auth: | ||
| 366 | | { | ||
| 367 | kind: "none"; | ||
| 368 | } | ||
| 369 | | { | ||
| 370 | kind: "provider_grant_ref"; | ||
| 371 | grantId: ReturnType<typeof ProviderAuthGrantIdSchema.make>; | ||
| 372 | providerKey: string; | ||
| 373 | requiredScopes: ReadonlyArray<string>; | ||
| 374 | headerName: string; | ||
| 375 | prefix: string; | ||
| 376 | }; | ||
| 377 | }) => | ||
| 378 | Effect.gen(function* () { | ||
| 379 | const installation = input.runtime.localInstallation; | ||
| 380 | const source = yield* createSourceFromPayload({ | ||
| 381 | workspaceId: installation.workspaceId, | ||
| 382 | sourceId: SourceIdSchema.make(`src_${crypto.randomUUID()}`), | ||
| 383 | payload: { | ||
| 384 | name: input.name, | ||
| 385 | kind: "google_discovery", | ||
| 386 | endpoint: input.discoveryUrl, | ||
| 387 | namespace: input.namespace, | ||
| 388 | binding: { | ||
| 389 | service: input.service, | ||
| 390 | version: input.version, | ||
| 391 | discoveryUrl: input.discoveryUrl, | ||
| 392 | scopes: [...input.scopes], | ||
| 393 | }, | ||
| 394 | importAuthPolicy: "reuse_runtime", | ||
| 395 | importAuth: { kind: "none" }, | ||
| 396 | auth: input.auth as any, | ||
| 397 | status: "connected", | ||
| 398 | enabled: true, | ||
| 399 | }, | ||
| 400 | now: Date.now(), | ||
| 401 | }); | ||
| 402 | |||
| 403 | return yield* persistSource(input.runtime.persistence.rows, source, { | ||
| 404 | actorAccountId: installation.accountId, | ||
| 405 | }).pipe((effect) => provideControlPlaneRuntime(effect, input.runtime)); | ||
| 406 | }); | ||
| 407 | |||
| 408 | const expectLeft = <A, E>(effect: Effect.Effect<A, E, never>) => | ||
| 409 | Effect.either(effect).pipe( | ||
| 410 | Effect.flatMap((result) => | ||
| 411 | Either.isLeft(result) | ||
| 412 | ? Effect.succeed(result.left) | ||
| 413 | : Effect.fail( | ||
| 414 | runtimeEffectError( | ||
| 415 | "control-plane-runtime.test", | ||
| 416 | "Expected effect to fail", | ||
| 417 | ), | ||
| 418 | ), | ||
| 419 | ), | ||
| 420 | ); | ||
| 421 | |||
| 422 | const invokeWorkspaceTool = <A>(input: { | ||
| 423 | runtime: ControlPlaneRuntime; | ||
| 424 | path: ToolPath; | ||
| 425 | args: unknown; | ||
| 426 | }) => | ||
| 427 | provideControlPlaneRuntime( | ||
| 428 | Effect.gen(function* () { | ||
| 429 | const installation = input.runtime.localInstallation; | ||
| 430 | const resolveEnvironment = yield* RuntimeExecutionResolverService; | ||
| 431 | const environment = yield* resolveEnvironment({ | ||
| 432 | workspaceId: installation.workspaceId, | ||
| 433 | accountId: installation.accountId, | ||
| 434 | executionId: ExecutionIdSchema.make(`exec_tool_${crypto.randomUUID()}`), | ||
| 435 | }); | ||
| 436 | |||
| 437 | return (yield* environment.toolInvoker.invoke({ | ||
| 438 | path: input.path, | ||
| 439 | args: input.args, | ||
| 440 | })) as A; | ||
| 441 | }), | ||
| 442 | input.runtime, | ||
| 443 | ); | ||
| 444 | |||
| 445 | describe("control-plane-runtime", () => { | ||
| 446 | it.scoped( | ||
| 447 | "writes local source changes through executor.jsonc", | ||
| 448 | () => | ||
| 449 | Effect.gen(function* () { | ||
| 450 | const fs = yield* FileSystem.FileSystem; | ||
| 451 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 452 | prefix: "executor-local-config-runtime-", | ||
| 453 | }); | ||
| 454 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 455 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 456 | const runtime = yield* Effect.acquireRelease( | ||
| 457 | createControlPlaneRuntime({ | ||
| 458 | workspaceRoot, | ||
| 459 | homeConfigPath, | ||
| 460 | homeStateDirectory, | ||
| 461 | }), | ||
| 462 | (createdRuntime) => | ||
| 463 | Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 464 | ); | ||
| 465 | const openApiServer = yield* makeOpenApiSpecServer; | ||
| 466 | const installation = runtime.localInstallation; | ||
| 467 | |||
| 468 | const createdSource = yield* withControlPlaneClient( | ||
| 469 | { runtime, accountId: installation.accountId }, | ||
| 470 | (client) => | ||
| 471 | client.sources.create({ | ||
| 472 | path: { workspaceId: installation.workspaceId }, | ||
| 473 | payload: { | ||
| 474 | name: "GitHub", | ||
| 475 | kind: "openapi", | ||
| 476 | endpoint: openApiServer.baseUrl, | ||
| 477 | namespace: "github", | ||
| 478 | binding: { | ||
| 479 | specUrl: openApiServer.specUrl, | ||
| 480 | defaultHeaders: null, | ||
| 481 | }, | ||
| 482 | auth: { kind: "none" }, | ||
| 483 | }, | ||
| 484 | }), | ||
| 485 | ); | ||
| 486 | |||
| 487 | const configPath = join(workspaceRoot, ".executor", "executor.jsonc"); | ||
| 488 | const createdConfig = JSON.parse( | ||
| 489 | yield* fs.readFileString(configPath, "utf8"), | ||
| 490 | ) as { | ||
| 491 | sources?: Record< | ||
| 492 | string, | ||
| 493 | { kind: string; connection: { endpoint: string } } | ||
| 494 | >; | ||
| 495 | }; | ||
| 496 | expect(createdConfig.sources?.github?.kind).toBe("openapi"); | ||
| 497 | expect(createdConfig.sources?.github?.connection.endpoint).toBe( | ||
| 498 | openApiServer.baseUrl, | ||
| 499 | ); | ||
| 500 | |||
| 501 | const removed = yield* withControlPlaneClient( | ||
| 502 | { runtime, accountId: installation.accountId }, | ||
| 503 | (client) => | ||
| 504 | client.sources.remove({ | ||
| 505 | path: { | ||
| 506 | workspaceId: installation.workspaceId, | ||
| 507 | sourceId: createdSource.id, | ||
| 508 | }, | ||
| 509 | }), | ||
| 510 | ); | ||
| 511 | expect(removed.removed).toBe(true); | ||
| 512 | |||
| 513 | const removedConfig = JSON.parse( | ||
| 514 | yield* fs.readFileString(configPath, "utf8"), | ||
| 515 | ) as { | ||
| 516 | sources?: Record<string, unknown>; | ||
| 517 | }; | ||
| 518 | expect(removedConfig.sources?.github).toBeUndefined(); | ||
| 519 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 520 | 60_000, | ||
| 521 | ); | ||
| 522 | |||
| 523 | it.scoped("manages local secrets through provider-backed metadata rows", () => | ||
| 524 | Effect.gen(function* () { | ||
| 525 | const runtime = yield* makeRuntime; | ||
| 526 | |||
| 527 | const created = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 528 | client.local.createSecret({ | ||
| 529 | payload: { | ||
| 530 | name: "GitHub PAT", | ||
| 531 | value: "ghp_test_token", | ||
| 532 | providerId: "local", | ||
| 533 | }, | ||
| 534 | }), | ||
| 535 | ); | ||
| 536 | |||
| 537 | expect(created.providerId).toBe("local"); | ||
| 538 | |||
| 539 | const listed = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 540 | client.local.listSecrets({}), | ||
| 541 | ); | ||
| 542 | expect(listed).toEqual([ | ||
| 543 | expect.objectContaining({ | ||
| 544 | id: created.id, | ||
| 545 | providerId: "local", | ||
| 546 | name: "GitHub PAT", | ||
| 547 | }), | ||
| 548 | ]); | ||
| 549 | |||
| 550 | const updated = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 551 | client.local.updateSecret({ | ||
| 552 | path: { secretId: created.id }, | ||
| 553 | payload: { | ||
| 554 | name: "GitHub PAT Updated", | ||
| 555 | value: "ghp_test_token_v2", | ||
| 556 | }, | ||
| 557 | }), | ||
| 558 | ); | ||
| 559 | expect(updated.providerId).toBe("local"); | ||
| 560 | expect(updated.name).toBe("GitHub PAT Updated"); | ||
| 561 | |||
| 562 | const storedSecret = | ||
| 563 | yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 564 | SecretMaterialIdSchema.make(created.id), | ||
| 565 | ); | ||
| 566 | assertTrue(Option.isSome(storedSecret)); | ||
| 567 | expect(storedSecret.value.providerId).toBe("local"); | ||
| 568 | expect(storedSecret.value.value).toBe("ghp_test_token_v2"); | ||
| 569 | |||
| 570 | const removed = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 571 | client.local.deleteSecret({ | ||
| 572 | path: { secretId: created.id }, | ||
| 573 | }), | ||
| 574 | ); | ||
| 575 | expect(removed.removed).toBe(true); | ||
| 576 | }), | ||
| 577 | ); | ||
| 578 | |||
| 579 | it.scoped( | ||
| 580 | "reports safe secret storage options for the current platform", | ||
| 581 | () => | ||
| 582 | Effect.gen(function* () { | ||
| 583 | const runtime = yield* makeRuntime; | ||
| 584 | |||
| 585 | const config = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 586 | client.local.config({}), | ||
| 587 | ); | ||
| 588 | |||
| 589 | const keychainProvider = | ||
| 590 | config.secretProviders.find( | ||
| 591 | (provider) => provider.id === "keychain", | ||
| 592 | ) ?? null; | ||
| 593 | |||
| 594 | if (process.platform === "linux") { | ||
| 595 | expect(config.defaultSecretStoreProvider).toBe("local"); | ||
| 596 | expect(keychainProvider?.canStore).toBe(false); | ||
| 597 | return; | ||
| 598 | } | ||
| 599 | |||
| 600 | if (process.platform === "darwin") { | ||
| 601 | expect(keychainProvider?.canStore).toBe(true); | ||
| 602 | return; | ||
| 603 | } | ||
| 604 | |||
| 605 | expect(keychainProvider).toBeNull(); | ||
| 606 | }), | ||
| 607 | ); | ||
| 608 | |||
| 609 | it.scoped( | ||
| 610 | "exposes shared local admin operations through executor tools", | ||
| 611 | () => | ||
| 612 | Effect.gen(function* () { | ||
| 613 | const runtime = yield* makeRuntime; | ||
| 614 | const openApiServer = yield* makeOpenApiSpecServer; | ||
| 615 | const installation = runtime.localInstallation; | ||
| 616 | const createdSource = yield* withControlPlaneClient( | ||
| 617 | { runtime, accountId: installation.accountId }, | ||
| 618 | (client) => | ||
| 619 | client.sources.create({ | ||
| 620 | path: { | ||
| 621 | workspaceId: installation.workspaceId, | ||
| 622 | }, | ||
| 623 | payload: { | ||
| 624 | name: "GitHub", | ||
| 625 | kind: "openapi", | ||
| 626 | endpoint: openApiServer.baseUrl, | ||
| 627 | status: "connected", | ||
| 628 | enabled: true, | ||
| 629 | namespace: "github", | ||
| 630 | importAuthPolicy: "reuse_runtime", | ||
| 631 | binding: { | ||
| 632 | specUrl: openApiServer.specUrl, | ||
| 633 | defaultHeaders: null, | ||
| 634 | }, | ||
| 635 | importAuth: { kind: "none" }, | ||
| 636 | auth: { kind: "none" }, | ||
| 637 | }, | ||
| 638 | }), | ||
| 639 | ); | ||
| 640 | |||
| 641 | const localInstallation = yield* invokeWorkspaceTool<{ | ||
| 642 | workspaceId: string; | ||
| 643 | accountId: string; | ||
| 644 | }>({ | ||
| 645 | runtime, | ||
| 646 | path: "executor.local.installation.get" as ToolPath, | ||
| 647 | args: {}, | ||
| 648 | }); | ||
| 649 | expect(localInstallation.workspaceId).toBe(installation.workspaceId); | ||
| 650 | expect(localInstallation.accountId).toBe(installation.accountId); | ||
| 651 | |||
| 652 | const localConfig = yield* invokeWorkspaceTool<{ | ||
| 653 | defaultSecretStoreProvider: string; | ||
| 654 | secretProviders: Array<{ id: string }>; | ||
| 655 | }>({ | ||
| 656 | runtime, | ||
| 657 | path: "executor.local.config.get" as ToolPath, | ||
| 658 | args: {}, | ||
| 659 | }); | ||
| 660 | expect(localConfig.secretProviders.length).toBeGreaterThan(0); | ||
| 661 | expect(typeof localConfig.defaultSecretStoreProvider).toBe("string"); | ||
| 662 | |||
| 663 | const createdSecret = yield* invokeWorkspaceTool<{ | ||
| 664 | id: string; | ||
| 665 | providerId: string; | ||
| 666 | name: string | null; | ||
| 667 | }>({ | ||
| 668 | runtime, | ||
| 669 | path: "executor.secrets.create" as ToolPath, | ||
| 670 | args: { | ||
| 671 | name: "Executor Admin Token", | ||
| 672 | value: "shh", | ||
| 673 | providerId: "local", | ||
| 674 | }, | ||
| 675 | }); | ||
| 676 | expect(createdSecret.providerId).toBe("local"); | ||
| 677 | |||
| 678 | const listedSecrets = yield* invokeWorkspaceTool< | ||
| 679 | Array<{ id: string; providerId: string; name: string | null }> | ||
| 680 | >({ | ||
| 681 | runtime, | ||
| 682 | path: "executor.secrets.list" as ToolPath, | ||
| 683 | args: {}, | ||
| 684 | }); | ||
| 685 | expect( | ||
| 686 | listedSecrets.some((secret) => secret.id === createdSecret.id), | ||
| 687 | ).toBe(true); | ||
| 688 | |||
| 689 | const apiSecrets = yield* withControlPlaneClient( | ||
| 690 | { runtime }, | ||
| 691 | (client) => client.local.listSecrets({}), | ||
| 692 | ); | ||
| 693 | expect( | ||
| 694 | apiSecrets.some((secret) => secret.id === createdSecret.id), | ||
| 695 | ).toBe(true); | ||
| 696 | |||
| 697 | const createdPolicy = yield* invokeWorkspaceTool<{ | ||
| 698 | id: string; | ||
| 699 | resourcePattern: string; | ||
| 700 | approvalMode: string; | ||
| 701 | }>({ | ||
| 702 | runtime, | ||
| 703 | path: "executor.policies.create" as ToolPath, | ||
| 704 | args: { | ||
| 705 | resourcePattern: "github.*", | ||
| 706 | effect: "allow", | ||
| 707 | approvalMode: "auto", | ||
| 708 | }, | ||
| 709 | }); | ||
| 710 | expect(createdPolicy.resourcePattern).toBe("github.*"); | ||
| 711 | |||
| 712 | const loadedPolicy = yield* invokeWorkspaceTool<{ | ||
| 713 | id: string; | ||
| 714 | resourcePattern: string; | ||
| 715 | }>({ | ||
| 716 | runtime, | ||
| 717 | path: "executor.policies.get" as ToolPath, | ||
| 718 | args: { | ||
| 719 | policyId: createdPolicy.id, | ||
| 720 | }, | ||
| 721 | }); | ||
| 722 | expect(loadedPolicy.id).toBe(createdPolicy.id); | ||
| 723 | |||
| 724 | const listedPolicies = yield* withControlPlaneClient( | ||
| 725 | { runtime, accountId: installation.accountId }, | ||
| 726 | (client) => | ||
| 727 | client.policies.list({ | ||
| 728 | path: { | ||
| 729 | workspaceId: installation.workspaceId, | ||
| 730 | }, | ||
| 731 | }), | ||
| 732 | ); | ||
| 733 | expect( | ||
| 734 | listedPolicies.some((policy) => policy.id === createdPolicy.id), | ||
| 735 | ).toBe(true); | ||
| 736 | |||
| 737 | const listedSources = yield* invokeWorkspaceTool<Array<{ id: string }>>( | ||
| 738 | { | ||
| 739 | runtime, | ||
| 740 | path: "executor.sources.list" as ToolPath, | ||
| 741 | args: {}, | ||
| 742 | }, | ||
| 743 | ); | ||
| 744 | expect( | ||
| 745 | listedSources.some((source) => source.id === createdSource.id), | ||
| 746 | ).toBe(true); | ||
| 747 | |||
| 748 | const loadedSource = yield* invokeWorkspaceTool<{ | ||
| 749 | id: string; | ||
| 750 | name: string; | ||
| 751 | }>({ | ||
| 752 | runtime, | ||
| 753 | path: "executor.sources.get" as ToolPath, | ||
| 754 | args: { | ||
| 755 | sourceId: createdSource.id, | ||
| 756 | }, | ||
| 757 | }); | ||
| 758 | expect(loadedSource.name).toBe("GitHub"); | ||
| 759 | |||
| 760 | const removedSource = yield* invokeWorkspaceTool<{ removed: boolean }>({ | ||
| 761 | runtime, | ||
| 762 | path: "executor.sources.remove" as ToolPath, | ||
| 763 | args: { | ||
| 764 | sourceId: createdSource.id, | ||
| 765 | }, | ||
| 766 | }); | ||
| 767 | expect(removedSource.removed).toBe(true); | ||
| 768 | |||
| 769 | const apiSources = yield* withControlPlaneClient( | ||
| 770 | { runtime, accountId: installation.accountId }, | ||
| 771 | (client) => | ||
| 772 | client.sources.list({ | ||
| 773 | path: { | ||
| 774 | workspaceId: installation.workspaceId, | ||
| 775 | }, | ||
| 776 | }), | ||
| 777 | ); | ||
| 778 | expect( | ||
| 779 | apiSources.some((source) => source.id === createdSource.id), | ||
| 780 | ).toBe(false); | ||
| 781 | }), | ||
| 782 | ); | ||
| 783 | |||
| 784 | it.scoped( | ||
| 785 | "captures credential requests through the local HTML flow without persisting raw tokens", | ||
| 786 | () => | ||
| 787 | Effect.gen(function* () { | ||
| 788 | const runtime = yield* makeRuntime; | ||
| 789 | const installation = runtime.localInstallation; | ||
| 790 | const executionId = ExecutionIdSchema.make("exec_local_credential"); | ||
| 791 | const sourceId = SourceIdSchema.make("github"); | ||
| 792 | const interactionSuffix = "executor.sources.add:test"; | ||
| 793 | const interactionId = ExecutionInteractionIdSchema.make( | ||
| 794 | `${executionId}:${interactionSuffix}`, | ||
| 795 | ); | ||
| 796 | const now = Date.now(); | ||
| 797 | |||
| 798 | yield* runtime.persistence.rows.executions.insert({ | ||
| 799 | id: executionId, | ||
| 800 | workspaceId: installation.workspaceId, | ||
| 801 | createdByAccountId: installation.accountId, | ||
| 802 | status: "running", | ||
| 803 | code: "return await tools.executor.sources.add(...)", | ||
| 804 | resultJson: null, | ||
| 805 | errorText: null, | ||
| 806 | logsJson: null, | ||
| 807 | startedAt: now, | ||
| 808 | completedAt: null, | ||
| 809 | createdAt: now, | ||
| 810 | updatedAt: now, | ||
| 811 | }); | ||
| 812 | |||
| 813 | const localSource = yield* createSourceFromPayload({ | ||
| 814 | workspaceId: installation.workspaceId, | ||
| 815 | sourceId, | ||
| 816 | payload: { | ||
| 817 | name: "GitHub", | ||
| 818 | kind: "openapi", | ||
| 819 | endpoint: "https://api.github.com", | ||
| 820 | status: "auth_required", | ||
| 821 | enabled: true, | ||
| 822 | namespace: "github", | ||
| 823 | importAuthPolicy: "reuse_runtime", | ||
| 824 | binding: { | ||
| 825 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 826 | defaultHeaders: null, | ||
| 827 | }, | ||
| 828 | importAuth: { kind: "none" }, | ||
| 829 | auth: { kind: "none" }, | ||
| 830 | }, | ||
| 831 | now, | ||
| 832 | }).pipe(Effect.orDie); | ||
| 833 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 834 | actorAccountId: installation.accountId, | ||
| 835 | }).pipe( | ||
| 836 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 837 | Effect.orDie, | ||
| 838 | ); | ||
| 839 | |||
| 840 | const interactionFiber = yield* Effect.gen(function* () { | ||
| 841 | const liveExecutionManager = yield* LiveExecutionManagerService; | ||
| 842 | const onElicitation = liveExecutionManager.createOnElicitation({ | ||
| 843 | rows: runtime.persistence.rows, | ||
| 844 | executionId, | ||
| 845 | }); | ||
| 846 | |||
| 847 | return yield* onElicitation({ | ||
| 848 | interactionId: interactionSuffix, | ||
| 849 | path: "executor.sources.add" as ToolPath, | ||
| 850 | sourceKey: "executor", | ||
| 851 | args: { | ||
| 852 | kind: "openapi", | ||
| 853 | endpoint: "https://api.github.com", | ||
| 854 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 855 | name: "GitHub", | ||
| 856 | workspaceId: installation.workspaceId, | ||
| 857 | sourceId, | ||
| 858 | }, | ||
| 859 | elicitation: { | ||
| 860 | mode: "url", | ||
| 861 | message: "Open the secure credential page to connect GitHub", | ||
| 862 | url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`, | ||
| 863 | elicitationId: interactionSuffix, | ||
| 864 | }, | ||
| 865 | }); | ||
| 866 | }).pipe( | ||
| 867 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 868 | Effect.fork, | ||
| 869 | ); | ||
| 870 | |||
| 871 | const waitForPendingInteraction = ( | ||
| 872 | remaining: number, | ||
| 873 | ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> => | ||
| 874 | runtime.persistence.rows.executionInteractions | ||
| 875 | .getPendingByExecutionId(executionId) | ||
| 876 | .pipe( | ||
| 877 | Effect.flatMap((pendingInteraction) => | ||
| 878 | Option.isSome(pendingInteraction) || remaining <= 0 | ||
| 879 | ? Effect.succeed(pendingInteraction) | ||
| 880 | : Effect.yieldNow().pipe( | ||
| 881 | Effect.zipRight(waitForPendingInteraction(remaining - 1)), | ||
| 882 | ), | ||
| 883 | ), | ||
| 884 | ); | ||
| 885 | |||
| 886 | const pendingInteraction = yield* waitForPendingInteraction(20); | ||
| 887 | assertTrue(Option.isSome(pendingInteraction)); | ||
| 888 | expect(pendingInteraction.value.id).toBe(interactionId); | ||
| 889 | |||
| 890 | const page = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 891 | client.sources.credentialPage({ | ||
| 892 | path: { | ||
| 893 | workspaceId: installation.workspaceId, | ||
| 894 | sourceId, | ||
| 895 | }, | ||
| 896 | urlParams: { | ||
| 897 | interactionId: pendingInteraction.value.id, | ||
| 898 | }, | ||
| 899 | }), | ||
| 900 | ); | ||
| 901 | expect(page).toContain("Configure Source Access"); | ||
| 902 | expect(page).toContain("GitHub"); | ||
| 903 | expect(page).toContain("Continue without auth"); | ||
| 904 | |||
| 905 | const submittedPage = yield* withControlPlaneClient( | ||
| 906 | { runtime }, | ||
| 907 | (client) => | ||
| 908 | client.sources.credentialSubmit({ | ||
| 909 | path: { | ||
| 910 | workspaceId: installation.workspaceId, | ||
| 911 | sourceId, | ||
| 912 | }, | ||
| 913 | urlParams: { | ||
| 914 | interactionId: pendingInteraction.value.id, | ||
| 915 | }, | ||
| 916 | payload: { | ||
| 917 | action: "submit", | ||
| 918 | token: "ghp_local_test_token", | ||
| 919 | }, | ||
| 920 | }), | ||
| 921 | ); | ||
| 922 | expect(submittedPage).toContain("Credential Stored"); | ||
| 923 | |||
| 924 | const response = yield* Fiber.join(interactionFiber); | ||
| 925 | expect(response.action).toBe("accept"); | ||
| 926 | expect(response.content?.authKind).toBe("bearer"); | ||
| 927 | |||
| 928 | const decodedCredentialSelection = | ||
| 929 | decodeSourceCredentialSelectionContent(response.content); | ||
| 930 | expect(decodedCredentialSelection.authKind).toBe("bearer"); | ||
| 931 | |||
| 932 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( | ||
| 933 | decodedCredentialSelection.authKind === "bearer" | ||
| 934 | ? decodedCredentialSelection.tokenRef.handle | ||
| 935 | : "", | ||
| 936 | ); | ||
| 937 | const storedSecret = | ||
| 938 | yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 939 | tokenSecretMaterialId, | ||
| 940 | ); | ||
| 941 | assertTrue(Option.isSome(storedSecret)); | ||
| 942 | expect( | ||
| 943 | decodedCredentialSelection.authKind === "bearer" | ||
| 944 | ? decodedCredentialSelection.tokenRef.providerId | ||
| 945 | : null, | ||
| 946 | ).toBe(storedSecret.value.providerId); | ||
| 947 | expect(storedSecret.value.value).toBe( | ||
| 948 | storedSecret.value.providerId === "local" | ||
| 949 | ? "ghp_local_test_token" | ||
| 950 | : null, | ||
| 951 | ); | ||
| 952 | |||
| 953 | const storedInteraction = | ||
| 954 | yield* runtime.persistence.rows.executionInteractions.getById( | ||
| 955 | pendingInteraction.value.id, | ||
| 956 | ); | ||
| 957 | assertTrue(Option.isSome(storedInteraction)); | ||
| 958 | expect(storedInteraction.value.responseJson).toContain( | ||
| 959 | '"authKind":"bearer"', | ||
| 960 | ); | ||
| 961 | expect(storedInteraction.value.responseJson).not.toContain("tokenRef"); | ||
| 962 | expect(storedInteraction.value.responseJson).not.toContain( | ||
| 963 | "ghp_local_test_token", | ||
| 964 | ); | ||
| 965 | }), | ||
| 966 | ); | ||
| 967 | |||
| 968 | it.scoped("loads a v1.2.3-style workspace artifact on startup", () => | ||
| 969 | Effect.gen(function* () { | ||
| 970 | const fs = yield* FileSystem.FileSystem; | ||
| 971 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 972 | prefix: "executor-control-plane-runtime-v123-", | ||
| 973 | }); | ||
| 974 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 975 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 976 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 977 | const configPath = join(configDirectory, "executor.jsonc"); | ||
| 978 | const sourceId = SourceIdSchema.make("graphql"); | ||
| 979 | const now = Date.now(); | ||
| 980 | |||
| 981 | yield* fs.makeDirectory(configDirectory, { recursive: true }); | ||
| 982 | yield* fs.writeFileString( | ||
| 983 | configPath, | ||
| 984 | `{ | ||
| 985 | "sources": { | ||
| 986 | "graphql": { | ||
| 987 | "kind": "graphql", | ||
| 988 | "name": "GraphQL API", | ||
| 989 | "namespace": "graphql", | ||
| 990 | "connection": { | ||
| 991 | "endpoint": "https://example.com/graphql" | ||
| 992 | }, | ||
| 993 | "binding": { | ||
| 994 | "defaultHeaders": null | ||
| 995 | } | ||
| 996 | } | ||
| 997 | } | ||
| 998 | } | ||
| 999 | `, | ||
| 1000 | ); | ||
| 1001 | |||
| 1002 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 1003 | workspaceRoot, | ||
| 1004 | homeConfigPath, | ||
| 1005 | homeStateDirectory, | ||
| 1006 | }); | ||
| 1007 | const installation = deriveLocalInstallation(context); | ||
| 1008 | const source = yield* createSourceFromPayload({ | ||
| 1009 | workspaceId: installation.workspaceId, | ||
| 1010 | sourceId, | ||
| 1011 | payload: { | ||
| 1012 | name: "GraphQL API", | ||
| 1013 | kind: "graphql", | ||
| 1014 | endpoint: "https://example.com/graphql", | ||
| 1015 | status: "connected", | ||
| 1016 | enabled: true, | ||
| 1017 | namespace: "graphql", | ||
| 1018 | importAuthPolicy: "reuse_runtime", | ||
| 1019 | binding: { | ||
| 1020 | defaultHeaders: null, | ||
| 1021 | }, | ||
| 1022 | importAuth: { kind: "none" }, | ||
| 1023 | auth: { kind: "none" }, | ||
| 1024 | }, | ||
| 1025 | now, | ||
| 1026 | }).pipe(Effect.orDie); | ||
| 1027 | |||
| 1028 | const artifact = buildLocalSourceArtifact({ | ||
| 1029 | source, | ||
| 1030 | syncResult: { | ||
| 1031 | fragment: createGraphqlCatalogFragment({ | ||
| 1032 | source, | ||
| 1033 | documents: [ | ||
| 1034 | { | ||
| 1035 | documentKind: "graphql_introspection", | ||
| 1036 | documentKey: source.endpoint, | ||
| 1037 | contentText: '{"__schema":{}}', | ||
| 1038 | fetchedAt: now, | ||
| 1039 | }, | ||
| 1040 | ], | ||
| 1041 | operations: [ | ||
| 1042 | { | ||
| 1043 | toolId: "viewer", | ||
| 1044 | title: "Viewer", | ||
| 1045 | description: "Load the current viewer", | ||
| 1046 | effect: "read", | ||
| 1047 | inputSchema: { type: "object", properties: {} }, | ||
| 1048 | outputSchema: { | ||
| 1049 | type: "object", | ||
| 1050 | properties: { login: { type: "string" } }, | ||
| 1051 | }, | ||
| 1052 | providerData: { | ||
| 1053 | kind: "graphql", | ||
| 1054 | toolKind: "field", | ||
| 1055 | toolId: "viewer", | ||
| 1056 | rawToolId: "viewer", | ||
| 1057 | group: "query", | ||
| 1058 | leaf: "viewer", | ||
| 1059 | fieldName: "viewer", | ||
| 1060 | operationType: "query", | ||
| 1061 | operationName: "ViewerQuery", | ||
| 1062 | operationDocument: "query ViewerQuery { viewer { login } }", | ||
| 1063 | queryTypeName: "Query", | ||
| 1064 | mutationTypeName: null, | ||
| 1065 | subscriptionTypeName: null, | ||
| 1066 | }, | ||
| 1067 | }, | ||
| 1068 | ], | ||
| 1069 | }), | ||
| 1070 | importMetadata: createCatalogImportMetadata({ | ||
| 1071 | source, | ||
| 1072 | adapterKey: "graphql", | ||
| 1073 | }), | ||
| 1074 | sourceHash: source.sourceHash, | ||
| 1075 | }, | ||
| 1076 | }); | ||
| 1077 | |||
| 1078 | yield* writeLocalWorkspaceState({ | ||
| 1079 | context, | ||
| 1080 | state: { | ||
| 1081 | version: 1, | ||
| 1082 | sources: { | ||
| 1083 | [sourceId]: { | ||
| 1084 | status: source.status, | ||
| 1085 | lastError: source.lastError, | ||
| 1086 | sourceHash: source.sourceHash, | ||
| 1087 | createdAt: source.createdAt, | ||
| 1088 | updatedAt: source.updatedAt, | ||
| 1089 | }, | ||
| 1090 | }, | ||
| 1091 | policies: {}, | ||
| 1092 | }, | ||
| 1093 | }); | ||
| 1094 | yield* writeLocalControlPlaneState({ | ||
| 1095 | context, | ||
| 1096 | state: { | ||
| 1097 | version: 1, | ||
| 1098 | authArtifacts: [], | ||
| 1099 | authLeases: [], | ||
| 1100 | sourceOauthClients: [], | ||
| 1101 | workspaceOauthClients: [], | ||
| 1102 | providerAuthGrants: [], | ||
| 1103 | sourceAuthSessions: [], | ||
| 1104 | secretMaterials: [], | ||
| 1105 | executions: [], | ||
| 1106 | executionInteractions: [], | ||
| 1107 | executionSteps: [], | ||
| 1108 | }, | ||
| 1109 | }); | ||
| 1110 | yield* writeLocalSourceArtifact({ | ||
| 1111 | context, | ||
| 1112 | sourceId, | ||
| 1113 | artifact, | ||
| 1114 | }); | ||
| 1115 | |||
| 1116 | const artifactPath = join( | ||
| 1117 | context.artifactsDirectory, | ||
| 1118 | "sources", | ||
| 1119 | `${sourceId}.json`, | ||
| 1120 | ); | ||
| 1121 | const legacyArtifact = JSON.parse( | ||
| 1122 | yield* fs.readFileString(artifactPath, "utf8"), | ||
| 1123 | ) as Record<string, unknown>; | ||
| 1124 | legacyArtifact.version = 3; | ||
| 1125 | yield* fs.writeFileString( | ||
| 1126 | artifactPath, | ||
| 1127 | `${JSON.stringify(legacyArtifact)}\n`, | ||
| 1128 | ); | ||
| 1129 | |||
| 1130 | const runtime = yield* Effect.acquireRelease( | ||
| 1131 | createControlPlaneRuntime({ | ||
| 1132 | workspaceRoot, | ||
| 1133 | homeConfigPath, | ||
| 1134 | homeStateDirectory, | ||
| 1135 | }), | ||
| 1136 | (createdRuntime) => | ||
| 1137 | Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 1138 | ); | ||
| 1139 | |||
| 1140 | const inspection = yield* withControlPlaneClient( | ||
| 1141 | { runtime, accountId: installation.accountId }, | ||
| 1142 | (client) => | ||
| 1143 | client.sources.inspection({ | ||
| 1144 | path: { | ||
| 1145 | workspaceId: installation.workspaceId, | ||
| 1146 | sourceId, | ||
| 1147 | }, | ||
| 1148 | }), | ||
| 1149 | ); | ||
| 1150 | |||
| 1151 | expect(inspection.source.id).toBe(sourceId); | ||
| 1152 | expect(inspection.source.kind).toBe("graphql"); | ||
| 1153 | expect(inspection.toolCount).toBe(1); | ||
| 1154 | expect(inspection.tools.map((tool) => tool.path)).toContain( | ||
| 1155 | "graphql.viewer", | ||
| 1156 | ); | ||
| 1157 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 1158 | ); | ||
| 1159 | |||
| 1160 | it.scoped( | ||
| 1161 | "rebuilds missing connected source artifacts on startup", | ||
| 1162 | () => | ||
| 1163 | Effect.gen(function* () { | ||
| 1164 | const fs = yield* FileSystem.FileSystem; | ||
| 1165 | const openApiServer = yield* makeOpenApiSpecServer; | ||
| 1166 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | ||
| 1167 | prefix: "executor-control-plane-runtime-startup-rebuild-", | ||
| 1168 | }); | ||
| 1169 | const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc"); | ||
| 1170 | const homeStateDirectory = join(workspaceRoot, ".executor-home-state"); | ||
| 1171 | const context = yield* resolveLocalWorkspaceContext({ | ||
| 1172 | workspaceRoot, | ||
| 1173 | homeConfigPath, | ||
| 1174 | homeStateDirectory, | ||
| 1175 | }); | ||
| 1176 | const installation = deriveLocalInstallation(context); | ||
| 1177 | const sourceId = SourceIdSchema.make("github"); | ||
| 1178 | const now = Date.now(); | ||
| 1179 | const source = yield* createSourceFromPayload({ | ||
| 1180 | workspaceId: installation.workspaceId, | ||
| 1181 | sourceId, | ||
| 1182 | payload: { | ||
| 1183 | name: "GitHub", | ||
| 1184 | kind: "openapi", | ||
| 1185 | endpoint: openApiServer.baseUrl, | ||
| 1186 | status: "connected", | ||
| 1187 | enabled: true, | ||
| 1188 | namespace: "github", | ||
| 1189 | importAuthPolicy: "reuse_runtime", | ||
| 1190 | binding: { | ||
| 1191 | specUrl: openApiServer.specUrl, | ||
| 1192 | defaultHeaders: null, | ||
| 1193 | }, | ||
| 1194 | importAuth: { kind: "none" }, | ||
| 1195 | auth: { kind: "none" }, | ||
| 1196 | }, | ||
| 1197 | now, | ||
| 1198 | }).pipe(Effect.orDie); | ||
| 1199 | |||
| 1200 | yield* writeProjectLocalExecutorConfig({ | ||
| 1201 | context, | ||
| 1202 | config: { | ||
| 1203 | sources: { | ||
| 1204 | [sourceId]: { | ||
| 1205 | kind: "openapi", | ||
| 1206 | name: "GitHub", | ||
| 1207 | namespace: "github", | ||
| 1208 | connection: { | ||
| 1209 | endpoint: openApiServer.baseUrl, | ||
| 1210 | }, | ||
| 1211 | binding: { | ||
| 1212 | specUrl: openApiServer.specUrl, | ||
| 1213 | defaultHeaders: null, | ||
| 1214 | }, | ||
| 1215 | }, | ||
| 1216 | }, | ||
| 1217 | }, | ||
| 1218 | }); | ||
| 1219 | yield* writeLocalWorkspaceState({ | ||
| 1220 | context, | ||
| 1221 | state: { | ||
| 1222 | version: 1, | ||
| 1223 | sources: { | ||
| 1224 | [sourceId]: { | ||
| 1225 | status: source.status, | ||
| 1226 | lastError: source.lastError, | ||
| 1227 | sourceHash: source.sourceHash, | ||
| 1228 | createdAt: source.createdAt, | ||
| 1229 | updatedAt: source.updatedAt, | ||
| 1230 | }, | ||
| 1231 | }, | ||
| 1232 | policies: {}, | ||
| 1233 | }, | ||
| 1234 | }); | ||
| 1235 | |||
| 1236 | const runtime = yield* Effect.acquireRelease( | ||
| 1237 | createControlPlaneRuntime({ | ||
| 1238 | workspaceRoot, | ||
| 1239 | homeConfigPath, | ||
| 1240 | homeStateDirectory, | ||
| 1241 | }), | ||
| 1242 | (createdRuntime) => | ||
| 1243 | Effect.promise(() => createdRuntime.close()).pipe(Effect.orDie), | ||
| 1244 | ); | ||
| 1245 | |||
| 1246 | const rebuiltArtifact = yield* readLocalSourceArtifact({ | ||
| 1247 | context, | ||
| 1248 | sourceId, | ||
| 1249 | }).pipe(Effect.provide(NodeFileSystem.layer)); | ||
| 1250 | expect(rebuiltArtifact).not.toBeNull(); | ||
| 1251 | |||
| 1252 | const inspection = yield* withControlPlaneClient( | ||
| 1253 | { runtime, accountId: installation.accountId }, | ||
| 1254 | (client) => | ||
| 1255 | client.sources.inspection({ | ||
| 1256 | path: { | ||
| 1257 | workspaceId: installation.workspaceId, | ||
| 1258 | sourceId, | ||
| 1259 | }, | ||
| 1260 | }), | ||
| 1261 | ); | ||
| 1262 | expect(inspection.tools.length).toBeGreaterThan(0); | ||
| 1263 | }).pipe(Effect.provide(NodeFileSystem.layer)), | ||
| 1264 | 60_000, | ||
| 1265 | ); | ||
| 1266 | |||
| 1267 | it.scoped( | ||
| 1268 | "returns an empty inspection bundle for auth-required sources without a catalog artifact", | ||
| 1269 | () => | ||
| 1270 | Effect.gen(function* () { | ||
| 1271 | const runtime = yield* makeRuntime; | ||
| 1272 | const installation = runtime.localInstallation; | ||
| 1273 | const sourceId = SourceIdSchema.make("googleapis"); | ||
| 1274 | const now = Date.now(); | ||
| 1275 | |||
| 1276 | const localSource = yield* createSourceFromPayload({ | ||
| 1277 | workspaceId: installation.workspaceId, | ||
| 1278 | sourceId, | ||
| 1279 | payload: { | ||
| 1280 | name: "Google Drive", | ||
| 1281 | kind: "google_discovery", | ||
| 1282 | endpoint: | ||
| 1283 | "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1284 | status: "auth_required", | ||
| 1285 | enabled: true, | ||
| 1286 | namespace: "googleapis", | ||
| 1287 | importAuthPolicy: "reuse_runtime", | ||
| 1288 | binding: { | ||
| 1289 | service: "drive", | ||
| 1290 | version: "v3", | ||
| 1291 | discoveryUrl: | ||
| 1292 | "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1293 | scopes: ["https://www.googleapis.com/auth/drive.readonly"], | ||
| 1294 | }, | ||
| 1295 | importAuth: { kind: "none" }, | ||
| 1296 | auth: { kind: "none" }, | ||
| 1297 | }, | ||
| 1298 | now, | ||
| 1299 | }).pipe(Effect.orDie); | ||
| 1300 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1301 | actorAccountId: installation.accountId, | ||
| 1302 | }).pipe( | ||
| 1303 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 1304 | Effect.orDie, | ||
| 1305 | ); | ||
| 1306 | |||
| 1307 | const inspection = yield* withControlPlaneClient( | ||
| 1308 | { runtime, accountId: installation.accountId }, | ||
| 1309 | (client) => | ||
| 1310 | client.sources.inspection({ | ||
| 1311 | path: { | ||
| 1312 | workspaceId: installation.workspaceId, | ||
| 1313 | sourceId, | ||
| 1314 | }, | ||
| 1315 | }), | ||
| 1316 | ); | ||
| 1317 | |||
| 1318 | expect(inspection.source.id).toBe(sourceId); | ||
| 1319 | expect(inspection.source.status).toBe("auth_required"); | ||
| 1320 | expect(inspection.toolCount).toBe(0); | ||
| 1321 | expect(inspection.tools).toEqual([]); | ||
| 1322 | }), | ||
| 1323 | ); | ||
| 1324 | |||
| 1325 | it.scoped( | ||
| 1326 | "still fails inspection when a connected source is missing its catalog artifact", | ||
| 1327 | () => | ||
| 1328 | Effect.gen(function* () { | ||
| 1329 | const runtime = yield* makeRuntime; | ||
| 1330 | const installation = runtime.localInstallation; | ||
| 1331 | const sourceId = SourceIdSchema.make("googleapis"); | ||
| 1332 | const now = Date.now(); | ||
| 1333 | |||
| 1334 | const localSource = yield* createSourceFromPayload({ | ||
| 1335 | workspaceId: installation.workspaceId, | ||
| 1336 | sourceId, | ||
| 1337 | payload: { | ||
| 1338 | name: "Google Drive", | ||
| 1339 | kind: "google_discovery", | ||
| 1340 | endpoint: | ||
| 1341 | "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1342 | status: "connected", | ||
| 1343 | enabled: true, | ||
| 1344 | namespace: "googleapis", | ||
| 1345 | importAuthPolicy: "reuse_runtime", | ||
| 1346 | binding: { | ||
| 1347 | service: "drive", | ||
| 1348 | version: "v3", | ||
| 1349 | discoveryUrl: | ||
| 1350 | "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 1351 | scopes: ["https://www.googleapis.com/auth/drive.readonly"], | ||
| 1352 | }, | ||
| 1353 | importAuth: { kind: "none" }, | ||
| 1354 | auth: { kind: "none" }, | ||
| 1355 | }, | ||
| 1356 | now, | ||
| 1357 | }).pipe(Effect.orDie); | ||
| 1358 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1359 | actorAccountId: installation.accountId, | ||
| 1360 | }).pipe( | ||
| 1361 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 1362 | Effect.orDie, | ||
| 1363 | ); | ||
| 1364 | |||
| 1365 | const error = yield* expectLeft( | ||
| 1366 | withControlPlaneClient( | ||
| 1367 | { runtime, accountId: installation.accountId }, | ||
| 1368 | (client) => | ||
| 1369 | client.sources.inspection({ | ||
| 1370 | path: { | ||
| 1371 | workspaceId: installation.workspaceId, | ||
| 1372 | sourceId, | ||
| 1373 | }, | ||
| 1374 | }), | ||
| 1375 | ), | ||
| 1376 | ); | ||
| 1377 | |||
| 1378 | expect(error.message).toContain( | ||
| 1379 | "Catalog artifact missing for source googleapis", | ||
| 1380 | ); | ||
| 1381 | }), | ||
| 1382 | ); | ||
| 1383 | |||
| 1384 | it.scoped( | ||
| 1385 | "allows continuing an OpenAPI source credential request without auth", | ||
| 1386 | () => | ||
| 1387 | Effect.gen(function* () { | ||
| 1388 | const runtime = yield* makeRuntime; | ||
| 1389 | const installation = runtime.localInstallation; | ||
| 1390 | const executionId = ExecutionIdSchema.make( | ||
| 1391 | "exec_local_credential_continue", | ||
| 1392 | ); | ||
| 1393 | const sourceId = SourceIdSchema.make("github"); | ||
| 1394 | const interactionSuffix = "executor.sources.add:continue"; | ||
| 1395 | const interactionId = ExecutionInteractionIdSchema.make( | ||
| 1396 | `${executionId}:${interactionSuffix}`, | ||
| 1397 | ); | ||
| 1398 | const now = Date.now(); | ||
| 1399 | |||
| 1400 | yield* runtime.persistence.rows.executions.insert({ | ||
| 1401 | id: executionId, | ||
| 1402 | workspaceId: installation.workspaceId, | ||
| 1403 | createdByAccountId: installation.accountId, | ||
| 1404 | status: "running", | ||
| 1405 | code: "return await tools.executor.sources.add(...)", | ||
| 1406 | resultJson: null, | ||
| 1407 | errorText: null, | ||
| 1408 | logsJson: null, | ||
| 1409 | startedAt: now, | ||
| 1410 | completedAt: null, | ||
| 1411 | createdAt: now, | ||
| 1412 | updatedAt: now, | ||
| 1413 | }); | ||
| 1414 | |||
| 1415 | const localSource = yield* createSourceFromPayload({ | ||
| 1416 | workspaceId: installation.workspaceId, | ||
| 1417 | sourceId, | ||
| 1418 | payload: { | ||
| 1419 | name: "GitHub", | ||
| 1420 | kind: "openapi", | ||
| 1421 | endpoint: "https://api.github.com", | ||
| 1422 | status: "auth_required", | ||
| 1423 | enabled: true, | ||
| 1424 | namespace: "github", | ||
| 1425 | importAuthPolicy: "reuse_runtime", | ||
| 1426 | binding: { | ||
| 1427 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 1428 | defaultHeaders: null, | ||
| 1429 | }, | ||
| 1430 | importAuth: { kind: "none" }, | ||
| 1431 | auth: { kind: "none" }, | ||
| 1432 | }, | ||
| 1433 | now, | ||
| 1434 | }).pipe(Effect.orDie); | ||
| 1435 | yield* persistSource(runtime.persistence.rows, localSource, { | ||
| 1436 | actorAccountId: installation.accountId, | ||
| 1437 | }).pipe( | ||
| 1438 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 1439 | Effect.orDie, | ||
| 1440 | ); | ||
| 1441 | |||
| 1442 | const interactionFiber = yield* Effect.gen(function* () { | ||
| 1443 | const liveExecutionManager = yield* LiveExecutionManagerService; | ||
| 1444 | const onElicitation = liveExecutionManager.createOnElicitation({ | ||
| 1445 | rows: runtime.persistence.rows, | ||
| 1446 | executionId, | ||
| 1447 | }); | ||
| 1448 | |||
| 1449 | return yield* onElicitation({ | ||
| 1450 | interactionId: interactionSuffix, | ||
| 1451 | path: "executor.sources.add" as ToolPath, | ||
| 1452 | sourceKey: "executor", | ||
| 1453 | args: { | ||
| 1454 | kind: "openapi", | ||
| 1455 | endpoint: "https://api.github.com", | ||
| 1456 | specUrl: "https://example.com/github-openapi.yaml", | ||
| 1457 | name: "GitHub", | ||
| 1458 | workspaceId: installation.workspaceId, | ||
| 1459 | sourceId, | ||
| 1460 | }, | ||
| 1461 | elicitation: { | ||
| 1462 | mode: "url", | ||
| 1463 | message: "Open the secure credential page to connect GitHub", | ||
| 1464 | url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`, | ||
| 1465 | elicitationId: interactionSuffix, | ||
| 1466 | }, | ||
| 1467 | }); | ||
| 1468 | }).pipe( | ||
| 1469 | (effect) => provideControlPlaneRuntime(effect, runtime), | ||
| 1470 | Effect.fork, | ||
| 1471 | ); | ||
| 1472 | |||
| 1473 | const waitForPendingInteraction = ( | ||
| 1474 | remaining: number, | ||
| 1475 | ): Effect.Effect<Option.Option<ExecutionInteraction>, Error> => | ||
| 1476 | runtime.persistence.rows.executionInteractions | ||
| 1477 | .getPendingByExecutionId(executionId) | ||
| 1478 | .pipe( | ||
| 1479 | Effect.flatMap((pendingInteraction) => | ||
| 1480 | Option.isSome(pendingInteraction) || remaining <= 0 | ||
| 1481 | ? Effect.succeed(pendingInteraction) | ||
| 1482 | : Effect.yieldNow().pipe( | ||
| 1483 | Effect.zipRight(waitForPendingInteraction(remaining - 1)), | ||
| 1484 | ), | ||
| 1485 | ), | ||
| 1486 | ); | ||
| 1487 | |||
| 1488 | const pendingInteraction = yield* waitForPendingInteraction(20); | ||
| 1489 | assertTrue(Option.isSome(pendingInteraction)); | ||
| 1490 | |||
| 1491 | const submitted = yield* submitSourceCredentialInteraction({ | ||
| 1492 | workspaceId: installation.workspaceId, | ||
| 1493 | sourceId, | ||
| 1494 | interactionId: pendingInteraction.value.id, | ||
| 1495 | action: "continue", | ||
| 1496 | }).pipe((effect) => provideControlPlaneRuntime(effect, runtime)); | ||
| 1497 | |||
| 1498 | expect(submitted.kind).toBe("continued"); | ||
| 1499 | |||
| 1500 | const response = yield* Fiber.join(interactionFiber); | ||
| 1501 | expect(response.action).toBe("accept"); | ||
| 1502 | expect(response.content).toEqual({ | ||
| 1503 | authKind: "none", | ||
| 1504 | }); | ||
| 1505 | |||
| 1506 | const storedInteraction = | ||
| 1507 | yield* runtime.persistence.rows.executionInteractions.getById( | ||
| 1508 | pendingInteraction.value.id, | ||
| 1509 | ); | ||
| 1510 | assertTrue(Option.isSome(storedInteraction)); | ||
| 1511 | expect(storedInteraction.value.responseJson).toContain( | ||
| 1512 | '"authKind":"none"', | ||
| 1513 | ); | ||
| 1514 | expect(storedInteraction.value.responseJson).not.toContain("tokenRef"); | ||
| 1515 | }), | ||
| 1516 | ); | ||
| 1517 | |||
| 1518 | it.scoped( | ||
| 1519 | "decodes GraphQL source credential interactions for the local credential flow", | ||
| 1520 | () => | ||
| 1521 | Effect.gen(function* () { | ||
| 1522 | const executionId = ExecutionIdSchema.make( | ||
| 1523 | "exec_local_graphql_credential_continue", | ||
| 1524 | ); | ||
| 1525 | const workspaceId = "ws_local_graphql" as const; | ||
| 1526 | const sourceId = SourceIdSchema.make("graphql_api"); | ||
| 1527 | const now = Date.now(); | ||
| 1528 | const decoded = decodeSourceCredentialInteraction({ | ||
| 1529 | id: ExecutionInteractionIdSchema.make( | ||
| 1530 | "exec_local_graphql_credential_continue:executor.sources.add:graphql-continue", | ||
| 1531 | ), | ||
| 1532 | executionId, | ||
| 1533 | status: "pending", | ||
| 1534 | purpose: "source_connect_oauth2", | ||
| 1535 | payloadJson: JSON.stringify({ | ||
| 1536 | path: "executor.sources.add", | ||
| 1537 | args: { | ||
| 1538 | kind: "graphql", | ||
| 1539 | endpoint: "https://example.com/graphql", | ||
| 1540 | name: "GraphQL API", | ||
| 1541 | namespace: "graphql", | ||
| 1542 | workspaceId, | ||
| 1543 | sourceId, | ||
| 1544 | }, | ||
| 1545 | elicitation: { | ||
| 1546 | mode: "url", | ||
| 1547 | message: "Open the secure credential page to connect GraphQL API", | ||
| 1548 | }, | ||
| 1549 | }), | ||
| 1550 | responseJson: null, | ||
| 1551 | responsePrivateJson: null, | ||
| 1552 | createdAt: now, | ||
| 1553 | updatedAt: now, | ||
| 1554 | }); | ||
| 1555 | |||
| 1556 | expect(decoded).not.toBeNull(); | ||
| 1557 | expect(decoded?.workspaceId).toBe(workspaceId); | ||
| 1558 | expect(decoded?.sourceId).toBe(sourceId); | ||
| 1559 | expect(decoded?.message).toContain("GraphQL API"); | ||
| 1560 | }), | ||
| 1561 | ); | ||
| 1562 | |||
| 1563 | it.scoped( | ||
| 1564 | "reuses existing Google provider grants for single connects and clears orphan state", | ||
| 1565 | () => | ||
| 1566 | Effect.gen(function* () { | ||
| 1567 | const runtime = yield* makeRuntime; | ||
| 1568 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1569 | const installation = runtime.localInstallation; | ||
| 1570 | |||
| 1571 | const oauthClient = yield* withControlPlaneClient( | ||
| 1572 | { runtime }, | ||
| 1573 | (client) => | ||
| 1574 | client.sources.createWorkspaceOauthClient({ | ||
| 1575 | path: { | ||
| 1576 | workspaceId: installation.workspaceId, | ||
| 1577 | }, | ||
| 1578 | payload: { | ||
| 1579 | providerKey: "google_workspace", | ||
| 1580 | label: "Local Google Workspace Client", | ||
| 1581 | oauthClient: { | ||
| 1582 | clientId: "google-client-id", | ||
| 1583 | clientSecret: "google-client-secret", | ||
| 1584 | }, | ||
| 1585 | }, | ||
| 1586 | }), | ||
| 1587 | ); | ||
| 1588 | |||
| 1589 | const refreshSecretId = "sec_google_refresh_single_reuse"; | ||
| 1590 | yield* upsertLocalSecret(runtime, { | ||
| 1591 | id: refreshSecretId, | ||
| 1592 | name: "Google Refresh Token", | ||
| 1593 | purpose: "oauth_refresh_token", | ||
| 1594 | value: "refresh-token-reuse", | ||
| 1595 | }); | ||
| 1596 | |||
| 1597 | const grantId = ProviderAuthGrantIdSchema.make( | ||
| 1598 | `provider_grant_${crypto.randomUUID()}`, | ||
| 1599 | ); | ||
| 1600 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1601 | id: grantId, | ||
| 1602 | workspaceId: installation.workspaceId, | ||
| 1603 | actorAccountId: installation.accountId, | ||
| 1604 | providerKey: "google_workspace", | ||
| 1605 | oauthClientId: oauthClient.id, | ||
| 1606 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1607 | clientAuthentication: "client_secret_post", | ||
| 1608 | headerName: "Authorization", | ||
| 1609 | prefix: "Bearer ", | ||
| 1610 | refreshToken: { | ||
| 1611 | providerId: "local", | ||
| 1612 | handle: refreshSecretId, | ||
| 1613 | }, | ||
| 1614 | grantedScopes: ["scope:drive.readonly"], | ||
| 1615 | lastRefreshedAt: null, | ||
| 1616 | orphanedAt: 123, | ||
| 1617 | createdAt: Date.now(), | ||
| 1618 | updatedAt: Date.now(), | ||
| 1619 | }); | ||
| 1620 | |||
| 1621 | googleServer.queueTokenResponse({ | ||
| 1622 | access_token: "reuse-access-token", | ||
| 1623 | scope: "scope:drive.readonly", | ||
| 1624 | }); | ||
| 1625 | |||
| 1626 | const result = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 1627 | client.sources.connect({ | ||
| 1628 | path: { | ||
| 1629 | workspaceId: installation.workspaceId, | ||
| 1630 | }, | ||
| 1631 | payload: { | ||
| 1632 | kind: "google_discovery", | ||
| 1633 | workspaceOauthClientId: oauthClient.id, | ||
| 1634 | service: "drive", | ||
| 1635 | version: "v3", | ||
| 1636 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1637 | service: "drive", | ||
| 1638 | version: "v3", | ||
| 1639 | scope: "scope:drive.readonly", | ||
| 1640 | }), | ||
| 1641 | scopes: ["scope:drive.readonly"], | ||
| 1642 | name: "Drive", | ||
| 1643 | namespace: "google.drive", | ||
| 1644 | }, | ||
| 1645 | }), | ||
| 1646 | ); | ||
| 1647 | |||
| 1648 | expect(result.kind).toBe("connected"); | ||
| 1649 | if (result.kind !== "connected") { | ||
| 1650 | throw new Error(`Expected connected result, received ${result.kind}`); | ||
| 1651 | } | ||
| 1652 | expect(result.source.auth).toEqual({ | ||
| 1653 | kind: "provider_grant_ref", | ||
| 1654 | grantId, | ||
| 1655 | providerKey: "google_workspace", | ||
| 1656 | requiredScopes: ["scope:drive.readonly"], | ||
| 1657 | headerName: "Authorization", | ||
| 1658 | prefix: "Bearer ", | ||
| 1659 | }); | ||
| 1660 | |||
| 1661 | const storedGrant = | ||
| 1662 | yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1663 | assertTrue(Option.isSome(storedGrant)); | ||
| 1664 | expect(storedGrant.value.orphanedAt).toBeNull(); | ||
| 1665 | expect(googleServer.discoveryAuthorizations).toContain( | ||
| 1666 | "Bearer reuse-access-token", | ||
| 1667 | ); | ||
| 1668 | }), | ||
| 1669 | 60_000, | ||
| 1670 | ); | ||
| 1671 | |||
| 1672 | it.scoped( | ||
| 1673 | "uses the app callback redirect for browser-started Google single-source OAuth", | ||
| 1674 | () => | ||
| 1675 | Effect.gen(function* () { | ||
| 1676 | const runtime = yield* makeRuntime; | ||
| 1677 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1678 | const installation = runtime.localInstallation; | ||
| 1679 | |||
| 1680 | const oauthClient = yield* withControlPlaneClient( | ||
| 1681 | { runtime }, | ||
| 1682 | (client) => | ||
| 1683 | client.sources.createWorkspaceOauthClient({ | ||
| 1684 | path: { | ||
| 1685 | workspaceId: installation.workspaceId, | ||
| 1686 | }, | ||
| 1687 | payload: { | ||
| 1688 | providerKey: "google_workspace", | ||
| 1689 | label: "Local Google Workspace Client", | ||
| 1690 | oauthClient: { | ||
| 1691 | clientId: "google-client-id", | ||
| 1692 | clientSecret: "google-client-secret", | ||
| 1693 | }, | ||
| 1694 | }, | ||
| 1695 | }), | ||
| 1696 | ); | ||
| 1697 | |||
| 1698 | const result = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 1699 | client.sources.connect({ | ||
| 1700 | path: { | ||
| 1701 | workspaceId: installation.workspaceId, | ||
| 1702 | }, | ||
| 1703 | payload: { | ||
| 1704 | kind: "google_discovery", | ||
| 1705 | workspaceOauthClientId: oauthClient.id, | ||
| 1706 | service: "gmail", | ||
| 1707 | version: "v1", | ||
| 1708 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1709 | service: "gmail", | ||
| 1710 | version: "v1", | ||
| 1711 | scope: "scope:gmail.readonly", | ||
| 1712 | }), | ||
| 1713 | scopes: ["scope:gmail.readonly"], | ||
| 1714 | name: "Gmail", | ||
| 1715 | namespace: "google.gmail", | ||
| 1716 | }, | ||
| 1717 | }), | ||
| 1718 | ); | ||
| 1719 | |||
| 1720 | expect(result.kind).toBe("oauth_required"); | ||
| 1721 | if (result.kind !== "oauth_required") { | ||
| 1722 | throw new Error( | ||
| 1723 | `Expected oauth_required result, received ${result.kind}`, | ||
| 1724 | ); | ||
| 1725 | } | ||
| 1726 | |||
| 1727 | const redirectUri = new URL(result.authorizationUrl).searchParams.get( | ||
| 1728 | "redirect_uri", | ||
| 1729 | ); | ||
| 1730 | expect(redirectUri).not.toBeNull(); | ||
| 1731 | expect(new URL(redirectUri!).pathname).toBe( | ||
| 1732 | `/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/oauth/provider/callback`, | ||
| 1733 | ); | ||
| 1734 | expect(result.source.status).toBe("auth_required"); | ||
| 1735 | }), | ||
| 1736 | 60_000, | ||
| 1737 | ); | ||
| 1738 | |||
| 1739 | it.scoped( | ||
| 1740 | "reuses existing Google provider grants for batch connects and clears orphan state", | ||
| 1741 | () => | ||
| 1742 | Effect.gen(function* () { | ||
| 1743 | const runtime = yield* makeRuntime; | ||
| 1744 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1745 | const installation = runtime.localInstallation; | ||
| 1746 | |||
| 1747 | const oauthClient = yield* withControlPlaneClient( | ||
| 1748 | { runtime }, | ||
| 1749 | (client) => | ||
| 1750 | client.sources.createWorkspaceOauthClient({ | ||
| 1751 | path: { | ||
| 1752 | workspaceId: installation.workspaceId, | ||
| 1753 | }, | ||
| 1754 | payload: { | ||
| 1755 | providerKey: "google_workspace", | ||
| 1756 | label: "Local Google Workspace Client", | ||
| 1757 | oauthClient: { | ||
| 1758 | clientId: "google-client-id", | ||
| 1759 | clientSecret: "google-client-secret", | ||
| 1760 | }, | ||
| 1761 | }, | ||
| 1762 | }), | ||
| 1763 | ); | ||
| 1764 | |||
| 1765 | const refreshSecretId = "sec_google_refresh_reuse"; | ||
| 1766 | yield* upsertLocalSecret(runtime, { | ||
| 1767 | id: refreshSecretId, | ||
| 1768 | name: "Google Refresh Token", | ||
| 1769 | purpose: "oauth_refresh_token", | ||
| 1770 | value: "refresh-token-reuse", | ||
| 1771 | }); | ||
| 1772 | |||
| 1773 | const grantId = ProviderAuthGrantIdSchema.make( | ||
| 1774 | `provider_grant_${crypto.randomUUID()}`, | ||
| 1775 | ); | ||
| 1776 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1777 | id: grantId, | ||
| 1778 | workspaceId: installation.workspaceId, | ||
| 1779 | actorAccountId: installation.accountId, | ||
| 1780 | providerKey: "google_workspace", | ||
| 1781 | oauthClientId: oauthClient.id, | ||
| 1782 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1783 | clientAuthentication: "client_secret_post", | ||
| 1784 | headerName: "Authorization", | ||
| 1785 | prefix: "Bearer ", | ||
| 1786 | refreshToken: { | ||
| 1787 | providerId: "local", | ||
| 1788 | handle: refreshSecretId, | ||
| 1789 | }, | ||
| 1790 | grantedScopes: ["scope:gmail.readonly", "scope:calendar.readonly"], | ||
| 1791 | lastRefreshedAt: null, | ||
| 1792 | orphanedAt: 123, | ||
| 1793 | createdAt: Date.now(), | ||
| 1794 | updatedAt: Date.now(), | ||
| 1795 | }); | ||
| 1796 | |||
| 1797 | googleServer.queueTokenResponse({ | ||
| 1798 | access_token: "reuse-access-token", | ||
| 1799 | scope: "scope:gmail.readonly scope:calendar.readonly", | ||
| 1800 | }); | ||
| 1801 | googleServer.queueTokenResponse({ | ||
| 1802 | access_token: "reuse-access-token", | ||
| 1803 | scope: "scope:gmail.readonly scope:calendar.readonly", | ||
| 1804 | }); | ||
| 1805 | |||
| 1806 | const result = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 1807 | client.sources.connectBatch({ | ||
| 1808 | path: { | ||
| 1809 | workspaceId: installation.workspaceId, | ||
| 1810 | }, | ||
| 1811 | payload: { | ||
| 1812 | workspaceOauthClientId: oauthClient.id, | ||
| 1813 | sources: [ | ||
| 1814 | { | ||
| 1815 | service: "gmail", | ||
| 1816 | version: "v1", | ||
| 1817 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1818 | service: "gmail", | ||
| 1819 | version: "v1", | ||
| 1820 | scope: "scope:gmail.readonly", | ||
| 1821 | }), | ||
| 1822 | scopes: ["scope:gmail.readonly"], | ||
| 1823 | name: "Gmail", | ||
| 1824 | namespace: "google.gmail", | ||
| 1825 | }, | ||
| 1826 | { | ||
| 1827 | service: "calendar", | ||
| 1828 | version: "v3", | ||
| 1829 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1830 | service: "calendar", | ||
| 1831 | version: "v3", | ||
| 1832 | scope: "scope:calendar.readonly", | ||
| 1833 | }), | ||
| 1834 | scopes: ["scope:calendar.readonly"], | ||
| 1835 | name: "Calendar", | ||
| 1836 | namespace: "google.calendar", | ||
| 1837 | }, | ||
| 1838 | ], | ||
| 1839 | }, | ||
| 1840 | }), | ||
| 1841 | ); | ||
| 1842 | |||
| 1843 | expect(result.providerOauthSession).toBeNull(); | ||
| 1844 | expect(result.results).toHaveLength(2); | ||
| 1845 | expect( | ||
| 1846 | result.results.every((entry) => entry.status === "connected"), | ||
| 1847 | ).toBe(true); | ||
| 1848 | expect( | ||
| 1849 | result.results.every( | ||
| 1850 | (entry) => entry.source.auth.kind === "provider_grant_ref", | ||
| 1851 | ), | ||
| 1852 | ).toBe(true); | ||
| 1853 | |||
| 1854 | const storedGrant = | ||
| 1855 | yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 1856 | assertTrue(Option.isSome(storedGrant)); | ||
| 1857 | expect(storedGrant.value.orphanedAt).toBeNull(); | ||
| 1858 | expect(googleServer.tokenRequests.length).toBeGreaterThanOrEqual(1); | ||
| 1859 | expect( | ||
| 1860 | googleServer.discoveryAuthorizations.every( | ||
| 1861 | (authorization) => authorization === "Bearer reuse-access-token", | ||
| 1862 | ), | ||
| 1863 | ).toBe(true); | ||
| 1864 | }), | ||
| 1865 | 60_000, | ||
| 1866 | ); | ||
| 1867 | |||
| 1868 | it.scoped( | ||
| 1869 | "uses the app callback redirect for browser-started Google batch OAuth", | ||
| 1870 | () => | ||
| 1871 | Effect.gen(function* () { | ||
| 1872 | const runtime = yield* makeRuntime; | ||
| 1873 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1874 | const installation = runtime.localInstallation; | ||
| 1875 | |||
| 1876 | const oauthClient = yield* withControlPlaneClient( | ||
| 1877 | { runtime }, | ||
| 1878 | (client) => | ||
| 1879 | client.sources.createWorkspaceOauthClient({ | ||
| 1880 | path: { | ||
| 1881 | workspaceId: installation.workspaceId, | ||
| 1882 | }, | ||
| 1883 | payload: { | ||
| 1884 | providerKey: "google_workspace", | ||
| 1885 | label: "Local Google Workspace Client", | ||
| 1886 | oauthClient: { | ||
| 1887 | clientId: "google-client-id", | ||
| 1888 | clientSecret: "google-client-secret", | ||
| 1889 | }, | ||
| 1890 | }, | ||
| 1891 | }), | ||
| 1892 | ); | ||
| 1893 | |||
| 1894 | const result = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 1895 | client.sources.connectBatch({ | ||
| 1896 | path: { | ||
| 1897 | workspaceId: installation.workspaceId, | ||
| 1898 | }, | ||
| 1899 | payload: { | ||
| 1900 | workspaceOauthClientId: oauthClient.id, | ||
| 1901 | sources: [ | ||
| 1902 | { | ||
| 1903 | service: "gmail", | ||
| 1904 | version: "v1", | ||
| 1905 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1906 | service: "gmail", | ||
| 1907 | version: "v1", | ||
| 1908 | scope: "scope:gmail.readonly", | ||
| 1909 | }), | ||
| 1910 | scopes: ["scope:gmail.readonly"], | ||
| 1911 | name: "Gmail", | ||
| 1912 | namespace: "google.gmail", | ||
| 1913 | }, | ||
| 1914 | ], | ||
| 1915 | }, | ||
| 1916 | }), | ||
| 1917 | ); | ||
| 1918 | |||
| 1919 | expect(result.providerOauthSession).not.toBeNull(); | ||
| 1920 | const redirectUri = new URL( | ||
| 1921 | result.providerOauthSession!.authorizationUrl, | ||
| 1922 | ).searchParams.get("redirect_uri"); | ||
| 1923 | |||
| 1924 | expect(redirectUri).not.toBeNull(); | ||
| 1925 | expect(new URL(redirectUri!).pathname).toBe( | ||
| 1926 | `/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/oauth/provider/callback`, | ||
| 1927 | ); | ||
| 1928 | }), | ||
| 1929 | 60_000, | ||
| 1930 | ); | ||
| 1931 | |||
| 1932 | it.scoped( | ||
| 1933 | "preserves the existing provider refresh token when callback expansion omits refresh_token", | ||
| 1934 | () => | ||
| 1935 | Effect.gen(function* () { | ||
| 1936 | const runtime = yield* makeRuntime; | ||
| 1937 | const googleServer = yield* makeGoogleWorkspaceTestServer; | ||
| 1938 | const installation = runtime.localInstallation; | ||
| 1939 | |||
| 1940 | const oauthClient = yield* withControlPlaneClient( | ||
| 1941 | { runtime }, | ||
| 1942 | (client) => | ||
| 1943 | client.sources.createWorkspaceOauthClient({ | ||
| 1944 | path: { | ||
| 1945 | workspaceId: installation.workspaceId, | ||
| 1946 | }, | ||
| 1947 | payload: { | ||
| 1948 | providerKey: "google_workspace", | ||
| 1949 | label: "Local Google Workspace Client", | ||
| 1950 | oauthClient: { | ||
| 1951 | clientId: "google-client-id", | ||
| 1952 | clientSecret: "google-client-secret", | ||
| 1953 | }, | ||
| 1954 | }, | ||
| 1955 | }), | ||
| 1956 | ); | ||
| 1957 | |||
| 1958 | const preservedRefreshSecretId = "sec_google_refresh_preserved"; | ||
| 1959 | yield* upsertLocalSecret(runtime, { | ||
| 1960 | id: preservedRefreshSecretId, | ||
| 1961 | name: "Preserved Google Refresh Token", | ||
| 1962 | purpose: "oauth_refresh_token", | ||
| 1963 | value: "refresh-token-preserved", | ||
| 1964 | }); | ||
| 1965 | |||
| 1966 | const source = yield* createPersistedGoogleSource({ | ||
| 1967 | runtime, | ||
| 1968 | name: "Drive", | ||
| 1969 | namespace: "google.drive", | ||
| 1970 | service: "drive", | ||
| 1971 | version: "v3", | ||
| 1972 | discoveryUrl: googleServer.discoveryUrl({ | ||
| 1973 | service: "drive", | ||
| 1974 | version: "v3", | ||
| 1975 | scope: "scope:drive.readonly", | ||
| 1976 | }), | ||
| 1977 | scopes: ["scope:drive.readonly"], | ||
| 1978 | auth: { | ||
| 1979 | kind: "none", | ||
| 1980 | }, | ||
| 1981 | }); | ||
| 1982 | |||
| 1983 | const grantId = ProviderAuthGrantIdSchema.make( | ||
| 1984 | `provider_grant_${crypto.randomUUID()}`, | ||
| 1985 | ); | ||
| 1986 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 1987 | id: grantId, | ||
| 1988 | workspaceId: installation.workspaceId, | ||
| 1989 | actorAccountId: installation.accountId, | ||
| 1990 | providerKey: "google_workspace", | ||
| 1991 | oauthClientId: oauthClient.id, | ||
| 1992 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 1993 | clientAuthentication: "client_secret_post", | ||
| 1994 | headerName: "Authorization", | ||
| 1995 | prefix: "Bearer ", | ||
| 1996 | refreshToken: { | ||
| 1997 | providerId: "local", | ||
| 1998 | handle: preservedRefreshSecretId, | ||
| 1999 | }, | ||
| 2000 | grantedScopes: ["scope:gmail.readonly"], | ||
| 2001 | lastRefreshedAt: null, | ||
| 2002 | orphanedAt: null, | ||
| 2003 | createdAt: Date.now(), | ||
| 2004 | updatedAt: Date.now(), | ||
| 2005 | }); | ||
| 2006 | |||
| 2007 | const sessionId = SourceAuthSessionIdSchema.make( | ||
| 2008 | `src_auth_${crypto.randomUUID()}`, | ||
| 2009 | ); | ||
| 2010 | const state = `provider-state-${crypto.randomUUID()}`; | ||
| 2011 | yield* runtime.persistence.rows.sourceAuthSessions.upsert({ | ||
| 2012 | id: sessionId, | ||
| 2013 | workspaceId: installation.workspaceId, | ||
| 2014 | sourceId: SourceIdSchema.make( | ||
| 2015 | `oauth_provider_${crypto.randomUUID()}`, | ||
| 2016 | ), | ||
| 2017 | actorAccountId: installation.accountId, | ||
| 2018 | credentialSlot: "runtime", | ||
| 2019 | executionId: null, | ||
| 2020 | interactionId: null, | ||
| 2021 | providerKind: "oauth2_provider_batch", | ||
| 2022 | status: "pending", | ||
| 2023 | state, | ||
| 2024 | sessionDataJson: JSON.stringify({ | ||
| 2025 | kind: "provider_oauth_batch", | ||
| 2026 | providerKey: "google_workspace", | ||
| 2027 | authorizationEndpoint: | ||
| 2028 | "https://accounts.google.com/o/oauth2/v2/auth", | ||
| 2029 | tokenEndpoint: googleServer.tokenEndpoint, | ||
| 2030 | redirectUri: `${googleServer.baseUrl}/oauth/callback`, | ||
| 2031 | oauthClientId: oauthClient.id, | ||
| 2032 | clientAuthentication: "client_secret_post", | ||
| 2033 | scopes: ["scope:gmail.readonly", "scope:drive.readonly"], | ||
| 2034 | headerName: "Authorization", | ||
| 2035 | prefix: "Bearer ", | ||
| 2036 | authorizationParams: { | ||
| 2037 | access_type: "offline", | ||
| 2038 | prompt: "consent", | ||
| 2039 | include_granted_scopes: "true", | ||
| 2040 | }, | ||
| 2041 | targetSources: [ | ||
| 2042 | { | ||
| 2043 | sourceId: source.id, | ||
| 2044 | requiredScopes: ["scope:drive.readonly"], | ||
| 2045 | }, | ||
| 2046 | ], | ||
| 2047 | codeVerifier: "pkce-verifier", | ||
| 2048 | authorizationUrl: `https://accounts.google.com/o/oauth2/v2/auth?state=${encodeURIComponent(state)}`, | ||
| 2049 | }), | ||
| 2050 | errorText: null, | ||
| 2051 | completedAt: null, | ||
| 2052 | createdAt: Date.now(), | ||
| 2053 | updatedAt: Date.now(), | ||
| 2054 | }); | ||
| 2055 | |||
| 2056 | googleServer.queueTokenResponse({ | ||
| 2057 | access_token: "authorization-code-access-token", | ||
| 2058 | scope: "scope:gmail.readonly scope:drive.readonly", | ||
| 2059 | }); | ||
| 2060 | googleServer.queueTokenResponse({ | ||
| 2061 | access_token: "post-callback-sync-access-token", | ||
| 2062 | scope: "scope:gmail.readonly scope:drive.readonly", | ||
| 2063 | }); | ||
| 2064 | |||
| 2065 | const callbackPage = yield* withControlPlaneClient( | ||
| 2066 | { runtime }, | ||
| 2067 | (client) => | ||
| 2068 | client.sources.providerOauthComplete({ | ||
| 2069 | path: { | ||
| 2070 | workspaceId: installation.workspaceId, | ||
| 2071 | }, | ||
| 2072 | urlParams: { | ||
| 2073 | state, | ||
| 2074 | code: "authorization-code", | ||
| 2075 | }, | ||
| 2076 | }), | ||
| 2077 | ); | ||
| 2078 | |||
| 2079 | expect(callbackPage).toContain("Connected 1 source"); | ||
| 2080 | |||
| 2081 | const storedGrant = | ||
| 2082 | yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 2083 | assertTrue(Option.isSome(storedGrant)); | ||
| 2084 | expect(storedGrant.value.refreshToken).toEqual({ | ||
| 2085 | providerId: "local", | ||
| 2086 | handle: preservedRefreshSecretId, | ||
| 2087 | }); | ||
| 2088 | expect(storedGrant.value.grantedScopes).toEqual([ | ||
| 2089 | "scope:gmail.readonly", | ||
| 2090 | "scope:drive.readonly", | ||
| 2091 | ]); | ||
| 2092 | |||
| 2093 | const refreshedSource = yield* withControlPlaneClient( | ||
| 2094 | { runtime }, | ||
| 2095 | (client) => | ||
| 2096 | client.sources.get({ | ||
| 2097 | path: { | ||
| 2098 | workspaceId: installation.workspaceId, | ||
| 2099 | sourceId: source.id, | ||
| 2100 | }, | ||
| 2101 | }), | ||
| 2102 | ); | ||
| 2103 | |||
| 2104 | expect(refreshedSource.status).toBe("connected"); | ||
| 2105 | expect(refreshedSource.auth).toEqual({ | ||
| 2106 | kind: "provider_grant_ref", | ||
| 2107 | grantId, | ||
| 2108 | providerKey: "google_workspace", | ||
| 2109 | requiredScopes: ["scope:drive.readonly"], | ||
| 2110 | headerName: "Authorization", | ||
| 2111 | prefix: "Bearer ", | ||
| 2112 | }); | ||
| 2113 | |||
| 2114 | expect( | ||
| 2115 | googleServer.tokenRequests.map((request) => | ||
| 2116 | request.get("grant_type"), | ||
| 2117 | ), | ||
| 2118 | ).toEqual(["authorization_code", "refresh_token"]); | ||
| 2119 | expect(googleServer.tokenRequests[1]?.get("refresh_token")).toBe( | ||
| 2120 | "refresh-token-preserved", | ||
| 2121 | ); | ||
| 2122 | expect(googleServer.discoveryAuthorizations).toContain( | ||
| 2123 | "Bearer post-callback-sync-access-token", | ||
| 2124 | ); | ||
| 2125 | }), | ||
| 2126 | 60_000, | ||
| 2127 | ); | ||
| 2128 | |||
| 2129 | it.scoped( | ||
| 2130 | "marks provider grants as orphaned when the last referencing source is removed", | ||
| 2131 | () => | ||
| 2132 | Effect.gen(function* () { | ||
| 2133 | const runtime = yield* makeRuntime; | ||
| 2134 | const installation = runtime.localInstallation; | ||
| 2135 | const oauthClientId = WorkspaceOauthClientIdSchema.make( | ||
| 2136 | `ws_oauth_client_${crypto.randomUUID()}`, | ||
| 2137 | ); | ||
| 2138 | const refreshSecretId = "sec_google_refresh_orphan"; | ||
| 2139 | |||
| 2140 | yield* runtime.persistence.rows.workspaceOauthClients.upsert({ | ||
| 2141 | id: oauthClientId, | ||
| 2142 | workspaceId: installation.workspaceId, | ||
| 2143 | providerKey: "google_workspace", | ||
| 2144 | label: "Local Google Workspace Client", | ||
| 2145 | clientId: "google-client-id", | ||
| 2146 | clientSecretProviderId: null, | ||
| 2147 | clientSecretHandle: null, | ||
| 2148 | clientMetadataJson: null, | ||
| 2149 | createdAt: Date.now(), | ||
| 2150 | updatedAt: Date.now(), | ||
| 2151 | }); | ||
| 2152 | yield* upsertLocalSecret(runtime, { | ||
| 2153 | id: refreshSecretId, | ||
| 2154 | name: "Google Refresh Token", | ||
| 2155 | purpose: "oauth_refresh_token", | ||
| 2156 | value: "refresh-token-orphan", | ||
| 2157 | }); | ||
| 2158 | |||
| 2159 | const grantId = ProviderAuthGrantIdSchema.make( | ||
| 2160 | `provider_grant_${crypto.randomUUID()}`, | ||
| 2161 | ); | ||
| 2162 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 2163 | id: grantId, | ||
| 2164 | workspaceId: installation.workspaceId, | ||
| 2165 | actorAccountId: installation.accountId, | ||
| 2166 | providerKey: "google_workspace", | ||
| 2167 | oauthClientId, | ||
| 2168 | tokenEndpoint: "https://example.test/oauth/token", | ||
| 2169 | clientAuthentication: "client_secret_post", | ||
| 2170 | headerName: "Authorization", | ||
| 2171 | prefix: "Bearer ", | ||
| 2172 | refreshToken: { | ||
| 2173 | providerId: "local", | ||
| 2174 | handle: refreshSecretId, | ||
| 2175 | }, | ||
| 2176 | grantedScopes: ["scope:drive.readonly"], | ||
| 2177 | lastRefreshedAt: null, | ||
| 2178 | orphanedAt: null, | ||
| 2179 | createdAt: Date.now(), | ||
| 2180 | updatedAt: Date.now(), | ||
| 2181 | }); | ||
| 2182 | |||
| 2183 | const source = yield* createPersistedGoogleSource({ | ||
| 2184 | runtime, | ||
| 2185 | name: "Drive", | ||
| 2186 | namespace: "google.drive", | ||
| 2187 | service: "drive", | ||
| 2188 | version: "v3", | ||
| 2189 | discoveryUrl: "https://example.test/drive/$discovery/rest?version=v3", | ||
| 2190 | scopes: ["scope:drive.readonly"], | ||
| 2191 | auth: { | ||
| 2192 | kind: "provider_grant_ref", | ||
| 2193 | grantId, | ||
| 2194 | providerKey: "google_workspace", | ||
| 2195 | requiredScopes: ["scope:drive.readonly"], | ||
| 2196 | headerName: "Authorization", | ||
| 2197 | prefix: "Bearer ", | ||
| 2198 | }, | ||
| 2199 | }); | ||
| 2200 | |||
| 2201 | const removed = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 2202 | client.sources.remove({ | ||
| 2203 | path: { | ||
| 2204 | workspaceId: installation.workspaceId, | ||
| 2205 | sourceId: source.id, | ||
| 2206 | }, | ||
| 2207 | }), | ||
| 2208 | ); | ||
| 2209 | |||
| 2210 | expect(removed.removed).toBe(true); | ||
| 2211 | |||
| 2212 | const storedGrant = | ||
| 2213 | yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 2214 | assertTrue(Option.isSome(storedGrant)); | ||
| 2215 | expect(storedGrant.value.orphanedAt).not.toBeNull(); | ||
| 2216 | }), | ||
| 2217 | ); | ||
| 2218 | |||
| 2219 | it.scoped( | ||
| 2220 | "revokes shared provider grants through the API and disconnects linked sources", | ||
| 2221 | () => | ||
| 2222 | Effect.gen(function* () { | ||
| 2223 | const runtime = yield* makeRuntime; | ||
| 2224 | const installation = runtime.localInstallation; | ||
| 2225 | const oauthClientId = WorkspaceOauthClientIdSchema.make( | ||
| 2226 | `ws_oauth_client_${crypto.randomUUID()}`, | ||
| 2227 | ); | ||
| 2228 | const refreshSecretId = "sec_google_refresh_revoke"; | ||
| 2229 | |||
| 2230 | yield* runtime.persistence.rows.workspaceOauthClients.upsert({ | ||
| 2231 | id: oauthClientId, | ||
| 2232 | workspaceId: installation.workspaceId, | ||
| 2233 | providerKey: "google_workspace", | ||
| 2234 | label: "Local Google Workspace Client", | ||
| 2235 | clientId: "google-client-id", | ||
| 2236 | clientSecretProviderId: null, | ||
| 2237 | clientSecretHandle: null, | ||
| 2238 | clientMetadataJson: null, | ||
| 2239 | createdAt: Date.now(), | ||
| 2240 | updatedAt: Date.now(), | ||
| 2241 | }); | ||
| 2242 | yield* upsertLocalSecret(runtime, { | ||
| 2243 | id: refreshSecretId, | ||
| 2244 | name: "Google Refresh Token", | ||
| 2245 | purpose: "oauth_refresh_token", | ||
| 2246 | value: "refresh-token-revoke", | ||
| 2247 | }); | ||
| 2248 | |||
| 2249 | const grantId = ProviderAuthGrantIdSchema.make( | ||
| 2250 | `provider_grant_${crypto.randomUUID()}`, | ||
| 2251 | ); | ||
| 2252 | yield* runtime.persistence.rows.providerAuthGrants.upsert({ | ||
| 2253 | id: grantId, | ||
| 2254 | workspaceId: installation.workspaceId, | ||
| 2255 | actorAccountId: installation.accountId, | ||
| 2256 | providerKey: "google_workspace", | ||
| 2257 | oauthClientId, | ||
| 2258 | tokenEndpoint: "https://example.test/oauth/token", | ||
| 2259 | clientAuthentication: "client_secret_post", | ||
| 2260 | headerName: "Authorization", | ||
| 2261 | prefix: "Bearer ", | ||
| 2262 | refreshToken: { | ||
| 2263 | providerId: "local", | ||
| 2264 | handle: refreshSecretId, | ||
| 2265 | }, | ||
| 2266 | grantedScopes: ["scope:drive.readonly", "scope:gmail.readonly"], | ||
| 2267 | lastRefreshedAt: null, | ||
| 2268 | orphanedAt: null, | ||
| 2269 | createdAt: Date.now(), | ||
| 2270 | updatedAt: Date.now(), | ||
| 2271 | }); | ||
| 2272 | |||
| 2273 | const driveSource = yield* createPersistedGoogleSource({ | ||
| 2274 | runtime, | ||
| 2275 | name: "Drive", | ||
| 2276 | namespace: "google.drive", | ||
| 2277 | service: "drive", | ||
| 2278 | version: "v3", | ||
| 2279 | discoveryUrl: "https://example.test/drive/$discovery/rest?version=v3", | ||
| 2280 | scopes: ["scope:drive.readonly"], | ||
| 2281 | auth: { | ||
| 2282 | kind: "provider_grant_ref", | ||
| 2283 | grantId, | ||
| 2284 | providerKey: "google_workspace", | ||
| 2285 | requiredScopes: ["scope:drive.readonly"], | ||
| 2286 | headerName: "Authorization", | ||
| 2287 | prefix: "Bearer ", | ||
| 2288 | }, | ||
| 2289 | }); | ||
| 2290 | const gmailSource = yield* createPersistedGoogleSource({ | ||
| 2291 | runtime, | ||
| 2292 | name: "Gmail", | ||
| 2293 | namespace: "google.gmail", | ||
| 2294 | service: "gmail", | ||
| 2295 | version: "v1", | ||
| 2296 | discoveryUrl: "https://example.test/gmail/$discovery/rest?version=v1", | ||
| 2297 | scopes: ["scope:gmail.readonly"], | ||
| 2298 | auth: { | ||
| 2299 | kind: "provider_grant_ref", | ||
| 2300 | grantId, | ||
| 2301 | providerKey: "google_workspace", | ||
| 2302 | requiredScopes: ["scope:gmail.readonly"], | ||
| 2303 | headerName: "Authorization", | ||
| 2304 | prefix: "Bearer ", | ||
| 2305 | }, | ||
| 2306 | }); | ||
| 2307 | |||
| 2308 | const removed = yield* withControlPlaneClient({ runtime }, (client) => | ||
| 2309 | client.sources.removeProviderAuthGrant({ | ||
| 2310 | path: { | ||
| 2311 | workspaceId: installation.workspaceId, | ||
| 2312 | grantId, | ||
| 2313 | }, | ||
| 2314 | }), | ||
| 2315 | ); | ||
| 2316 | |||
| 2317 | expect(removed.removed).toBe(true); | ||
| 2318 | |||
| 2319 | const storedGrant = | ||
| 2320 | yield* runtime.persistence.rows.providerAuthGrants.getById(grantId); | ||
| 2321 | assertTrue(Option.isNone(storedGrant)); | ||
| 2322 | |||
| 2323 | const storedRefreshSecret = | ||
| 2324 | yield* runtime.persistence.rows.secretMaterials.getById( | ||
| 2325 | SecretMaterialIdSchema.make(refreshSecretId), | ||
| 2326 | ); | ||
| 2327 | assertTrue(Option.isNone(storedRefreshSecret)); | ||
| 2328 | |||
| 2329 | const disconnectedDrive = yield* withControlPlaneClient( | ||
| 2330 | { runtime }, | ||
| 2331 | (client) => | ||
| 2332 | client.sources.get({ | ||
| 2333 | path: { | ||
| 2334 | workspaceId: installation.workspaceId, | ||
| 2335 | sourceId: driveSource.id, | ||
| 2336 | }, | ||
| 2337 | }), | ||
| 2338 | ); | ||
| 2339 | const disconnectedGmail = yield* withControlPlaneClient( | ||
| 2340 | { runtime }, | ||
| 2341 | (client) => | ||
| 2342 | client.sources.get({ | ||
| 2343 | path: { | ||
| 2344 | workspaceId: installation.workspaceId, | ||
| 2345 | sourceId: gmailSource.id, | ||
| 2346 | }, | ||
| 2347 | }), | ||
| 2348 | ); | ||
| 2349 | |||
| 2350 | expect(disconnectedDrive.status).toBe("auth_required"); | ||
| 2351 | expect(disconnectedDrive.auth).toEqual({ kind: "none" }); | ||
| 2352 | expect(disconnectedGmail.status).toBe("auth_required"); | ||
| 2353 | expect(disconnectedGmail.auth).toEqual({ kind: "none" }); | ||
| 2354 | }), | ||
| 2355 | ); | ||
| 2356 | }); | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -1,7 +1,7 @@ | |||
| 1 | import { | 1 | import { |
| 2 | ControlPlaneNotFoundError, | 2 | ControlPlaneNotFoundError, |
| 3 | ControlPlaneStorageError, | 3 | ControlPlaneStorageError, |
| 4 | } from "../../api/errors"; | 4 | } from "../../errors"; |
| 5 | import type { | 5 | import type { |
| 6 | ElicitationResponse, | 6 | ElicitationResponse, |
| 7 | OnElicitation, | 7 | OnElicitation, |
| @@ -11,7 +11,7 @@ import type { · 3 unmodified lines | |||
| 11 | import type { | 11 | import type { |
| 12 | CreateExecutionPayload, | 12 | CreateExecutionPayload, |
| 13 | ResumeExecutionPayload, | 13 | ResumeExecutionPayload, |
| 14 | } from "../../api/executions/api"; | 14 | } from "../../executions/contracts"; |
| 15 | import { | 15 | import { |
| 16 | ExecutionIdSchema, | 16 | ExecutionIdSchema, |
| 17 | ExecutionInteractionIdSchema, | 17 | ExecutionInteractionIdSchema, |
File renamed without changes.
| @@ -3,13 +3,12 @@ import { · 2 unmodified lines | |||
| 3 | HttpApiClient, | 3 | HttpApiClient, |
| 4 | } from "@effect/platform"; | 4 | } from "@effect/platform"; |
| 5 | import { NodeHttpServer } from "@effect/platform-node"; | 5 | import { NodeHttpServer } from "@effect/platform-node"; |
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | import * as Layer from "effect/Layer"; | ||
| 8 | |||
| 9 | import { | 6 | import { |
| 10 | ControlPlaneApi, | 7 | ControlPlaneApi, |
| 11 | createControlPlaneApiLayer, | 8 | createControlPlaneApiLayer, |
| 12 | } from "#api"; | 9 | } from "@executor/platform-api"; |
| 10 | import * as Effect from "effect/Effect"; | ||
| 11 | import * as Layer from "effect/Layer"; | ||
| 13 | 12 | ||
| 14 | import { | 13 | import { |
| 15 | type ControlPlaneRuntime, | 14 | type ControlPlaneRuntime, |
File renamed without changes.
| @@ -15,16 +15,11 @@ import { · 14 unmodified lines | |||
| 15 | resolveConfiguredExecutionRuntime, | 15 | resolveConfiguredExecutionRuntime, |
| 16 | } from "../runtime"; | 16 | } from "../runtime"; |
| 17 | import { createWorkspaceToolInvoker } from "./tool-invoker"; | 17 | import { createWorkspaceToolInvoker } from "./tool-invoker"; |
| 18 | import { | 18 | import { RuntimeSourceAuthServiceTag } from "../../sources/source-auth-service"; |
| 19 | RuntimeSourceAuthServiceTag, | 19 | import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime"; |
| 20 | } from "../../sources/source-auth-service"; | ||
| 21 | import { | ||
| 22 | RuntimeSourceCatalogStoreService, | ||
| 23 | } from "../../catalog/source/runtime"; | ||
| 24 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; | 20 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; |
| 25 | import { | 21 | import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync"; |
| 26 | getRuntimeLocalWorkspaceOption, | 22 | import { getRuntimeLocalWorkspaceOption } from "../../local/runtime-context"; |
| 27 | } from "../../local/runtime-context"; | ||
| 28 | import { | 23 | import { |
| 29 | LocalToolRuntimeLoaderService, | 24 | LocalToolRuntimeLoaderService, |
| 30 | type LocalToolRuntimeLoaderShape, | 25 | type LocalToolRuntimeLoaderShape, |
| @@ -38,6 +33,9 @@ import { · 7 unmodified lines | |||
| 38 | WorkspaceStateStore, | 33 | WorkspaceStateStore, |
| 39 | type WorkspaceStateStoreShape, | 34 | type WorkspaceStateStoreShape, |
| 40 | } from "../../local/storage"; | 35 | } from "../../local/storage"; |
| 36 | import { ControlPlaneStore } from "../../store"; | ||
| 37 | import { RuntimeSourceStoreService } from "../../sources/source-store"; | ||
| 38 | import type { CreateWorkspaceInternalToolMap } from "./tool-invoker"; | ||
| 41 | export { | 39 | export { |
| 42 | createCodeExecutorForRuntime, | 40 | createCodeExecutorForRuntime, |
| 43 | resolveConfiguredExecutionRuntime, | 41 | resolveConfiguredExecutionRuntime, |
| @@ -50,29 +48,49 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 6 unmodified lines | |||
| 50 | toolPaths: new Set<string>(), | 48 | toolPaths: new Set<string>(), |
| 51 | }); | 49 | }); |
| 52 | 50 | ||
| 53 | export const createWorkspaceExecutionEnvironmentResolver = (input: { | 51 | export const createWorkspaceExecutionEnvironmentResolver = |
| 54 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | 52 | (input: { |
| 55 | sourceAuthService: Effect.Effect.Success<typeof RuntimeSourceAuthServiceTag>; | 53 | controlPlaneStore: Effect.Effect.Success<typeof ControlPlaneStore>; |
| 56 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | 54 | sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>; |
| 57 | localToolRuntimeLoader: LocalToolRuntimeLoaderShape; | 55 | sourceCatalogSyncService: Effect.Effect.Success< |
| 58 | workspaceConfigStore: WorkspaceConfigStoreShape; | 56 | typeof RuntimeSourceCatalogSyncService |
| 59 | workspaceStateStore: WorkspaceStateStoreShape; | 57 | >; |
| 60 | sourceArtifactStore: SourceArtifactStoreShape; | 58 | sourceAuthMaterialService: Effect.Effect.Success< |
| 61 | }): ResolveExecutionEnvironment => | 59 | typeof RuntimeSourceAuthMaterialService |
| 60 | >; | ||
| 61 | sourceAuthService: Effect.Effect.Success< | ||
| 62 | typeof RuntimeSourceAuthServiceTag | ||
| 63 | >; | ||
| 64 | sourceCatalogStore: Effect.Effect.Success< | ||
| 65 | typeof RuntimeSourceCatalogStoreService | ||
| 66 | >; | ||
| 67 | localToolRuntimeLoader: LocalToolRuntimeLoaderShape; | ||
| 68 | workspaceConfigStore: WorkspaceConfigStoreShape; | ||
| 69 | workspaceStateStore: WorkspaceStateStoreShape; | ||
| 70 | sourceArtifactStore: SourceArtifactStoreShape; | ||
| 71 | createInternalToolMap?: CreateWorkspaceInternalToolMap; | ||
| 72 | }): ResolveExecutionEnvironment => | ||
| 62 | ({ workspaceId, accountId, onElicitation }) => | 73 | ({ workspaceId, accountId, onElicitation }) => |
| 63 | Effect.gen(function* () { | 74 | Effect.gen(function* () { |
| 64 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | 75 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 65 | const loadedConfig = | 76 | const loadedConfig = |
| 66 | runtimeLocalWorkspace === null | 77 | runtimeLocalWorkspace === null |
| 67 | ? null | 78 | ? null |
| 68 | : yield* input.workspaceConfigStore.load(runtimeLocalWorkspace.context); | 79 | : yield* input.workspaceConfigStore.load( |
| 80 | runtimeLocalWorkspace.context, | ||
| 81 | ); | ||
| 69 | const localToolRuntime = | 82 | const localToolRuntime = |
| 70 | runtimeLocalWorkspace === null | 83 | runtimeLocalWorkspace === null |
| 71 | ? createEmptyLocalToolRuntime() | 84 | ? createEmptyLocalToolRuntime() |
| 72 | : yield* input.localToolRuntimeLoader.load(runtimeLocalWorkspace.context); | 85 | : yield* input.localToolRuntimeLoader.load( |
| 86 | runtimeLocalWorkspace.context, | ||
| 87 | ); | ||
| 73 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ | 88 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 74 | workspaceId, | 89 | workspaceId, |
| 75 | accountId, | 90 | accountId, |
| 91 | controlPlaneStore: input.controlPlaneStore, | ||
| 92 | sourceStore: input.sourceStore, | ||
| 93 | sourceCatalogSyncService: input.sourceCatalogSyncService, | ||
| 76 | sourceCatalogStore: input.sourceCatalogStore, | 94 | sourceCatalogStore: input.sourceCatalogStore, |
| 77 | workspaceConfigStore: input.workspaceConfigStore, | 95 | workspaceConfigStore: input.workspaceConfigStore, |
| 78 | workspaceStateStore: input.workspaceStateStore, | 96 | workspaceStateStore: input.workspaceStateStore, |
| @@ -81,6 +99,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 2 unmodified lines | |||
| 81 | sourceAuthService: input.sourceAuthService, | 99 | sourceAuthService: input.sourceAuthService, |
| 82 | runtimeLocalWorkspace, | 100 | runtimeLocalWorkspace, |
| 83 | localToolRuntime, | 101 | localToolRuntime, |
| 102 | createInternalToolMap: input.createInternalToolMap, | ||
| 84 | onElicitation, | 103 | onElicitation, |
| 85 | }); | 104 | }); |
| 86 | 105 | ||
| @@ -105,14 +124,20 @@ export class RuntimeExecutionResolverService extends Context.Tag( · 18 unmodified lines | |||
| 105 | export const RuntimeExecutionResolverLive = ( | 124 | export const RuntimeExecutionResolverLive = ( |
| 106 | input: { | 125 | input: { |
| 107 | executionResolver?: ResolveExecutionEnvironment; | 126 | executionResolver?: ResolveExecutionEnvironment; |
| 127 | createInternalToolMap?: CreateWorkspaceInternalToolMap; | ||
| 108 | } = {}, | 128 | } = {}, |
| 109 | ) => | 129 | ) => |
| 110 | input.executionResolver | 130 | input.executionResolver |
| 111 | ? Layer.succeed(RuntimeExecutionResolverService, input.executionResolver) | 131 | ? Layer.succeed(RuntimeExecutionResolverService, input.executionResolver) |
| 112 | : Layer.effect( | 132 | : Layer.effect( |
| 113 | RuntimeExecutionResolverService, | 133 | RuntimeExecutionResolverService, |
| 114 | Effect.gen(function* () { | 134 | Effect.gen(function* () { |
| 115 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; | 135 | const controlPlaneStore = yield* ControlPlaneStore; |
| 136 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 137 | const sourceCatalogSyncService = | ||
| 138 | yield* RuntimeSourceCatalogSyncService; | ||
| 139 | const sourceAuthMaterialService = | ||
| 140 | yield* RuntimeSourceAuthMaterialService; | ||
| 116 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 141 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 117 | const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService; | 142 | const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService; |
| 118 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; | 143 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; |
| @@ -121,6 +146,9 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines | |||
| 121 | const sourceArtifactStore = yield* SourceArtifactStore; | 146 | const sourceArtifactStore = yield* SourceArtifactStore; |
| 122 | 147 | ||
| 123 | return createWorkspaceExecutionEnvironmentResolver({ | 148 | return createWorkspaceExecutionEnvironmentResolver({ |
| 149 | controlPlaneStore, | ||
| 150 | sourceStore, | ||
| 151 | sourceCatalogSyncService, | ||
| 124 | sourceAuthService, | 152 | sourceAuthService, |
| 125 | sourceAuthMaterialService, | 153 | sourceAuthMaterialService, |
| 126 | sourceCatalogStore, | 154 | sourceCatalogStore, |
| @@ -128,6 +156,7 @@ export const RuntimeExecutionResolverLive = ( · 1 unmodified line | |||
| 128 | workspaceConfigStore, | 156 | workspaceConfigStore, |
| 129 | workspaceStateStore, | 157 | workspaceStateStore, |
| 130 | sourceArtifactStore, | 158 | sourceArtifactStore, |
| 159 | createInternalToolMap: input.createInternalToolMap, | ||
| 131 | }); | 160 | }); |
| 132 | }), | 161 | }), |
| 133 | ); | 162 | ); |
File renamed without changes.
File renamed without changes.
| @@ -5,6 +5,7 @@ import { · 4 unmodified lines | |||
| 5 | mergeToolCatalogs, | 5 | mergeToolCatalogs, |
| 6 | mergeToolMaps, | 6 | mergeToolMaps, |
| 7 | type ToolCatalog, | 7 | type ToolCatalog, |
| 8 | type ToolMap, | ||
| 8 | type ToolInvoker, | 9 | type ToolInvoker, |
| 9 | } from "@executor/codemode-core"; | 10 | } from "@executor/codemode-core"; |
| 10 | import type { AccountId, Source } from "#schema"; | 11 | import type { AccountId, Source } from "#schema"; |
| @@ -13,19 +14,18 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines | |||
| 13 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; | 14 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; |
| 14 | import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime"; | 15 | import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime"; |
| 15 | import type { RuntimeLocalWorkspaceState } from "../../local/runtime-context"; | 16 | import type { RuntimeLocalWorkspaceState } from "../../local/runtime-context"; |
| 16 | import { | 17 | import { type LocalToolRuntime } from "../../local/tools"; |
| 17 | type LocalToolRuntime, | ||
| 18 | } from "../../local/tools"; | ||
| 19 | import { | 18 | import { |
| 20 | makeWorkspaceStorageLayer, | 19 | makeWorkspaceStorageLayer, |
| 21 | type SourceArtifactStoreShape, | 20 | type SourceArtifactStoreShape, |
| 22 | type WorkspaceConfigStoreShape, | 21 | type WorkspaceConfigStoreShape, |
| 23 | type WorkspaceStateStoreShape, | 22 | type WorkspaceStateStoreShape, |
| 24 | } from "../../local/storage"; | 23 | } from "../../local/storage"; |
| 25 | import { | 24 | import type { ControlPlaneStoreShape } from "../../store"; |
| 26 | RuntimeSourceAuthService, | 25 | import { RuntimeSourceAuthService } from "../../sources/source-auth-service"; |
| 27 | } from "../../sources/source-auth-service"; | 26 | import { type RuntimeSourceStore } from "../../sources/source-store"; |
| 28 | import { createExecutorToolMap } from "../../sources/executor-tools"; | 27 | import { createExecutorToolMap } from "../../sources/executor-tools"; |
| 28 | import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync"; | ||
| 29 | import { invokeIrTool } from "../ir-execution"; | 29 | import { invokeIrTool } from "../ir-execution"; |
| 30 | import { | 30 | import { |
| 31 | authorizePersistedToolInvocation, | 31 | authorizePersistedToolInvocation, |
| @@ -38,17 +38,46 @@ import { · 6 unmodified lines | |||
| 38 | } from "./source-catalog"; | 38 | } from "./source-catalog"; |
| 39 | import { runtimeEffectError } from "../../effect-errors"; | 39 | import { runtimeEffectError } from "../../effect-errors"; |
| 40 | 40 | ||
| 41 | export type WorkspaceInternalToolContext = { | ||
| 42 | workspaceId: Source["workspaceId"]; | ||
| 43 | accountId: AccountId; | ||
| 44 | controlPlaneStore: ControlPlaneStoreShape; | ||
| 45 | sourceStore: RuntimeSourceStore; | ||
| 46 | sourceCatalogSyncService: Effect.Effect.Success< | ||
| 47 | typeof RuntimeSourceCatalogSyncService | ||
| 48 | >; | ||
| 49 | sourceAuthService: RuntimeSourceAuthService; | ||
| 50 | workspaceConfigStore: WorkspaceConfigStoreShape; | ||
| 51 | workspaceStateStore: WorkspaceStateStoreShape; | ||
| 52 | sourceArtifactStore: SourceArtifactStoreShape; | ||
| 53 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | ||
| 54 | }; | ||
| 55 | |||
| 56 | export type CreateWorkspaceInternalToolMap = ( | ||
| 57 | input: WorkspaceInternalToolContext, | ||
| 58 | ) => ToolMap; | ||
| 59 | |||
| 41 | export const createWorkspaceToolInvoker = (input: { | 60 | export const createWorkspaceToolInvoker = (input: { |
| 42 | workspaceId: Source["workspaceId"]; | 61 | workspaceId: Source["workspaceId"]; |
| 43 | accountId: AccountId; | 62 | accountId: AccountId; |
| 44 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | 63 | controlPlaneStore: ControlPlaneStoreShape; |
| 64 | sourceStore: RuntimeSourceStore; | ||
| 65 | sourceCatalogSyncService: Effect.Effect.Success< | ||
| 66 | typeof RuntimeSourceCatalogSyncService | ||
| 67 | >; | ||
| 68 | sourceCatalogStore: Effect.Effect.Success< | ||
| 69 | typeof RuntimeSourceCatalogStoreService | ||
| 70 | >; | ||
| 45 | workspaceConfigStore: WorkspaceConfigStoreShape; | 71 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 46 | workspaceStateStore: WorkspaceStateStoreShape; | 72 | workspaceStateStore: WorkspaceStateStoreShape; |
| 47 | sourceArtifactStore: SourceArtifactStoreShape; | 73 | sourceArtifactStore: SourceArtifactStoreShape; |
| 48 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | 74 | sourceAuthMaterialService: Effect.Effect.Success< |
| 75 | typeof RuntimeSourceAuthMaterialService | ||
| 76 | >; | ||
| 49 | sourceAuthService: RuntimeSourceAuthService; | 77 | sourceAuthService: RuntimeSourceAuthService; |
| 50 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | 78 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 51 | localToolRuntime: LocalToolRuntime; | 79 | localToolRuntime: LocalToolRuntime; |
| 80 | createInternalToolMap?: CreateWorkspaceInternalToolMap; | ||
| 52 | onElicitation?: Parameters< | 81 | onElicitation?: Parameters< |
| 53 | typeof makeToolInvokerFromTools | 82 | typeof makeToolInvokerFromTools |
| 54 | >[0]["onElicitation"]; | 83 | >[0]["onElicitation"]; |
| @@ -70,6 +99,19 @@ export const createWorkspaceToolInvoker = (input: { · 15 unmodified lines | |||
| 70 | sourceAuthService: input.sourceAuthService, | 99 | sourceAuthService: input.sourceAuthService, |
| 71 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | 100 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 72 | }); | 101 | }); |
| 102 | const internalTools = | ||
| 103 | input.createInternalToolMap?.({ | ||
| 104 | workspaceId: input.workspaceId, | ||
| 105 | accountId: input.accountId, | ||
| 106 | controlPlaneStore: input.controlPlaneStore, | ||
| 107 | sourceStore: input.sourceStore, | ||
| 108 | sourceCatalogSyncService: input.sourceCatalogSyncService, | ||
| 109 | sourceAuthService: input.sourceAuthService, | ||
| 110 | workspaceConfigStore: input.workspaceConfigStore, | ||
| 111 | workspaceStateStore: input.workspaceStateStore, | ||
| 112 | sourceArtifactStore: input.sourceArtifactStore, | ||
| 113 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | ||
| 114 | }) ?? {}; | ||
| 73 | const sourceCatalog = createWorkspaceSourceCatalog({ | 115 | const sourceCatalog = createWorkspaceSourceCatalog({ |
| 74 | workspaceId: input.workspaceId, | 116 | workspaceId: input.workspaceId, |
| 75 | accountId: input.accountId, | 117 | accountId: input.accountId, |
| @@ -92,6 +134,7 @@ export const createWorkspaceToolInvoker = (input: { · 16 unmodified lines | |||
| 92 | const authoredTools = mergeToolMaps([ | 134 | const authoredTools = mergeToolMaps([ |
| 93 | systemTools, | 135 | systemTools, |
| 94 | executorTools, | 136 | executorTools, |
| 137 | internalTools, | ||
| 95 | input.localToolRuntime.tools, | 138 | input.localToolRuntime.tools, |
| 96 | ]); | 139 | ]); |
| 97 | const authoredCatalog = createToolCatalogFromTools({ | 140 | const authoredCatalog = createToolCatalogFromTools({ |
| @@ -112,41 +155,46 @@ export const createWorkspaceToolInvoker = (input: { · 14 unmodified lines | |||
| 112 | context?: Record<string, unknown>; | 155 | context?: Record<string, unknown>; |
| 113 | }) => | 156 | }) => |
| 114 | provideRuntimeLocalWorkspace( | 157 | provideRuntimeLocalWorkspace( |
| 115 | provideWorkspaceStorage(Effect.gen(function* () { | 158 | provideWorkspaceStorage( |
| 116 | const catalogTool = yield* loadWorkspaceCatalogToolByPath({ | 159 | Effect.gen(function* () { |
| 117 | workspaceId: input.workspaceId, | 160 | const catalogTool = yield* loadWorkspaceCatalogToolByPath({ |
| 118 | accountId: input.accountId, | 161 | workspaceId: input.workspaceId, |
| 119 | sourceCatalogStore: input.sourceCatalogStore, | 162 | accountId: input.accountId, |
| 120 | path: invocation.path, | 163 | sourceCatalogStore: input.sourceCatalogStore, |
| 121 | includeSchemas: false, | 164 | path: invocation.path, |
| 122 | }); | 165 | includeSchemas: false, |
| 123 | if (!catalogTool) { | 166 | }); |
| 124 | return yield* runtimeEffectError("execution/workspace/tool-invoker", `Unknown tool path: ${invocation.path}`); | 167 | if (!catalogTool) { |
| 125 | } | 168 | return yield* runtimeEffectError( |
| 169 | "execution/workspace/tool-invoker", | ||
| 170 | `Unknown tool path: ${invocation.path}`, | ||
| 171 | ); | ||
| 172 | } | ||
| 126 | 173 | ||
| 127 | yield* authorizePersistedToolInvocation({ | 174 | yield* authorizePersistedToolInvocation({ |
| 128 | workspaceId: input.workspaceId, | 175 | workspaceId: input.workspaceId, |
| 129 | tool: catalogTool, | 176 | tool: catalogTool, |
| 130 | args: invocation.args, | 177 | args: invocation.args, |
| 131 | context: invocation.context, | 178 | context: invocation.context, |
| 132 | onElicitation: input.onElicitation, | 179 | onElicitation: input.onElicitation, |
| 133 | }); | 180 | }); |
| 134 | 181 | ||
| 135 | const auth = yield* input.sourceAuthMaterialService.resolve({ | 182 | const auth = yield* input.sourceAuthMaterialService.resolve({ |
| 136 | source: catalogTool.source, | 183 | source: catalogTool.source, |
| 137 | actorAccountId: input.accountId, | 184 | actorAccountId: input.accountId, |
| 138 | context: toSecretResolutionContext(invocation.context), | 185 | context: toSecretResolutionContext(invocation.context), |
| 139 | }); | 186 | }); |
| 140 | return yield* invokeIrTool({ | 187 | return yield* invokeIrTool({ |
| 141 | workspaceId: input.workspaceId, | 188 | workspaceId: input.workspaceId, |
| 142 | accountId: input.accountId, | 189 | accountId: input.accountId, |
| 143 | tool: catalogTool, | 190 | tool: catalogTool, |
| 144 | auth, | 191 | auth, |
| 145 | args: invocation.args, | 192 | args: invocation.args, |
| 146 | onElicitation: input.onElicitation, | 193 | onElicitation: input.onElicitation, |
| 147 | context: invocation.context, | 194 | context: invocation.context, |
| 148 | }); | 195 | }); |
| 149 | })), | 196 | }), |
| 197 | ), | ||
| 150 | input.runtimeLocalWorkspace, | 198 | input.runtimeLocalWorkspace, |
| 151 | ); | 199 | ); |
| 152 | 200 | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -6,8 +6,6 @@ import * as ManagedRuntime from "effect/ManagedRuntime"; · 5 unmodified lines | |||
| 6 | import * as Scope from "effect/Scope"; | 6 | import * as Scope from "effect/Scope"; |
| 7 | import { NodeFileSystem } from "@effect/platform-node"; | 7 | import { NodeFileSystem } from "@effect/platform-node"; |
| 8 | import { clearAllMcpConnectionPools } from "@executor/source-mcp"; | 8 | import { clearAllMcpConnectionPools } from "@executor/source-mcp"; |
| 9 | |||
| 10 | import { type ControlPlaneApiRuntimeContext } from "#api"; | ||
| 11 | import type { LocalInstallation } from "#schema"; | 9 | import type { LocalInstallation } from "#schema"; |
| 12 | 10 | ||
| 13 | import { type ResolveExecutionEnvironment } from "./execution/state"; | 11 | import { type ResolveExecutionEnvironment } from "./execution/state"; |
| @@ -20,9 +18,7 @@ import { · 6 unmodified lines | |||
| 20 | type LocalControlPlanePersistence, | 18 | type LocalControlPlanePersistence, |
| 21 | } from "./local/control-plane-store"; | 19 | } from "./local/control-plane-store"; |
| 22 | 20 | ||
| 23 | import { | 21 | import { resolveLocalWorkspaceContext } from "./local/config"; |
| 24 | resolveLocalWorkspaceContext, | ||
| 25 | } from "./local/config"; | ||
| 26 | import { | 22 | import { |
| 27 | LocalStorageLive, | 23 | LocalStorageLive, |
| 28 | LocalInstallationStore, | 24 | LocalInstallationStore, |
| @@ -40,36 +36,57 @@ import { RuntimeSourceCatalogStoreLive } from "./catalog/source/runtime"; · 11 unmodified lines | |||
| 40 | import { reconcileMissingSourceCatalogArtifacts } from "./catalog/source/reconcile"; | 36 | import { reconcileMissingSourceCatalogArtifacts } from "./catalog/source/reconcile"; |
| 41 | import { RuntimeSourceAuthMaterialLive } from "./auth/source-auth-material"; | 37 | import { RuntimeSourceAuthMaterialLive } from "./auth/source-auth-material"; |
| 42 | import { RuntimeSourceCatalogSyncLive } from "./catalog/source/sync"; | 38 | import { RuntimeSourceCatalogSyncLive } from "./catalog/source/sync"; |
| 43 | import { | 39 | import { RuntimeSourceAuthServiceLive } from "./sources/source-auth-service"; |
| 44 | RuntimeSourceAuthServiceLive, | ||
| 45 | } from "./sources/source-auth-service"; | ||
| 46 | import type { ResolveSecretMaterial } from "./local/secret-material-providers"; | 40 | import type { ResolveSecretMaterial } from "./local/secret-material-providers"; |
| 47 | import { SecretMaterialLive } from "./local/secret-material-providers"; | 41 | import { SecretMaterialLive } from "./local/secret-material-providers"; |
| 48 | import { | 42 | import { RuntimeExecutionResolverLive } from "./execution/workspace/environment"; |
| 49 | RuntimeExecutionResolverLive, | 43 | import type { CreateWorkspaceInternalToolMap } from "./execution/workspace/tool-invoker"; |
| 50 | } from "./execution/workspace/environment"; | ||
| 51 | 44 | ||
| 52 | export * from "./execution/state"; | 45 | export * from "./execution/state"; |
| 53 | export * from "./sources/executor-tools"; | 46 | export * from "./sources/executor-tools"; |
| 54 | export * from "./execution/live"; | 47 | export * from "./execution/live"; |
| 55 | export * from "./local/config"; | 48 | export * from "./local/config"; |
| 49 | export * from "./local/errors"; | ||
| 56 | export * from "./local/installation"; | 50 | export * from "./local/installation"; |
| 51 | export * from "./local/runtime-context"; | ||
| 57 | export * from "./local/storage"; | 52 | export * from "./local/storage"; |
| 58 | export * from "./local/source-artifacts"; | 53 | export * from "./local/source-artifacts"; |
| 59 | export * from "./local/tools"; | 54 | export * from "./local/tools"; |
| 60 | export * from "./catalog/schema-type-signature"; | 55 | export * from "./catalog/schema-type-signature"; |
| 56 | export * from "./catalog/source/runtime"; | ||
| 57 | export * from "./catalog/source/sync"; | ||
| 61 | export * from "./sources/source-auth-service"; | 58 | export * from "./sources/source-auth-service"; |
| 62 | export * from "./local/secret-material-providers"; | 59 | export * from "./local/secret-material-providers"; |
| 63 | export * from "./sources/source-credential-interactions"; | 60 | export * from "./sources/source-credential-interactions"; |
| 64 | export * from "./sources/source-adapters/mcp"; | 61 | export * from "./sources/source-adapters/mcp"; |
| 62 | export * from "./sources/source-store"; | ||
| 65 | export * from "./store"; | 63 | export * from "./store"; |
| 66 | export * from "./execution/workspace/environment"; | 64 | export * from "./execution/workspace/environment"; |
| 67 | export * from "./sources/source-inspection"; | 65 | export * from "../sources/inspection"; |
| 68 | export * from "./sources/source-discovery"; | 66 | export * from "../sources/discovery"; |
| 69 | export * from "./execution/service"; | 67 | export * from "./execution/service"; |
| 68 | export type { | ||
| 69 | CreateWorkspaceInternalToolMap, | ||
| 70 | WorkspaceInternalToolContext, | ||
| 71 | } from "./execution/workspace/tool-invoker"; | ||
| 72 | export { | ||
| 73 | builtInSourceAdapters, | ||
| 74 | connectableSourceAdapters, | ||
| 75 | executorAddableSourceAdapters, | ||
| 76 | localConfigurableSourceAdapters, | ||
| 77 | getSourceAdapter, | ||
| 78 | getSourceAdapterForSource, | ||
| 79 | findSourceAdapterByProviderKey, | ||
| 80 | sourceBindingStateFromSource, | ||
| 81 | sourceAdapterCatalogKind, | ||
| 82 | sourceAdapterRequiresInteractiveConnect, | ||
| 83 | sourceAdapterUsesCredentialManagedAuth, | ||
| 84 | isInternalSourceAdapter, | ||
| 85 | } from "./sources/source-adapters"; | ||
| 70 | 86 | ||
| 71 | export type RuntimeControlPlaneOptions = { | 87 | export type RuntimeControlPlaneOptions = { |
| 72 | executionResolver?: ResolveExecutionEnvironment; | 88 | executionResolver?: ResolveExecutionEnvironment; |
| 89 | createInternalToolMap?: CreateWorkspaceInternalToolMap; | ||
| 73 | resolveSecretMaterial?: ResolveSecretMaterial; | 90 | resolveSecretMaterial?: ResolveSecretMaterial; |
| 74 | getLocalServerBaseUrl?: () => string | undefined; | 91 | getLocalServerBaseUrl?: () => string | undefined; |
| 75 | localDataDir?: string; | 92 | localDataDir?: string; |
| @@ -81,9 +98,7 @@ export type RuntimeControlPlaneOptions = { · 5 unmodified lines | |||
| 81 | const detailsFromCause = (cause: unknown): string => | 98 | const detailsFromCause = (cause: unknown): string => |
| 82 | cause instanceof Error ? cause.message : String(cause); | 99 | cause instanceof Error ? cause.message : String(cause); |
| 83 | 100 | ||
| 84 | const toLocalRuntimeBootstrapError = ( | 101 | const toLocalRuntimeBootstrapError = (cause: unknown): Error => { |
| 85 | cause: unknown, | ||
| 86 | ): Error => { | ||
| 87 | const details = detailsFromCause(cause); | 102 | const details = detailsFromCause(cause); |
| 88 | return new Error(`Failed initializing local runtime: ${details}`); | 103 | return new Error(`Failed initializing local runtime: ${details}`); |
| 89 | }; | 104 | }; |
| @@ -91,11 +106,7 @@ const toLocalRuntimeBootstrapError = ( · 1 unmodified line | |||
| 91 | const closeScope = (scope: Scope.CloseableScope) => | 106 | const closeScope = (scope: Scope.CloseableScope) => |
| 92 | Scope.close(scope, Exit.void).pipe(Effect.orDie); | 107 | Scope.close(scope, Exit.void).pipe(Effect.orDie); |
| 93 | 108 | ||
| 94 | export type RuntimeControlPlaneLayer = Layer.Layer< | 109 | export type RuntimeControlPlaneLayer = Layer.Layer<any, never, never>; |
| 95 | ControlPlaneApiRuntimeContext, | ||
| 96 | never, | ||
| 97 | never | ||
| 98 | >; | ||
| 99 | 110 | ||
| 100 | export const createRuntimeControlPlaneLayer = ( | 111 | export const createRuntimeControlPlaneLayer = ( |
| 101 | input: RuntimeControlPlaneOptions & { | 112 | input: RuntimeControlPlaneOptions & { |
| @@ -105,9 +116,7 @@ export const createRuntimeControlPlaneLayer = ( · 3 unmodified lines | |||
| 105 | }, | 116 | }, |
| 106 | ) => { | 117 | ) => { |
| 107 | const platformLayer = NodeFileSystem.layer; | 118 | const platformLayer = NodeFileSystem.layer; |
| 108 | const storageLayer = LocalStorageLive.pipe( | 119 | const storageLayer = LocalStorageLive.pipe(Layer.provide(platformLayer)); |
| 109 | Layer.provide(platformLayer), | ||
| 110 | ); | ||
| 111 | const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe( | 120 | const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe( |
| 112 | Layer.provide(platformLayer), | 121 | Layer.provide(platformLayer), |
| 113 | ); | 122 | ); |
| @@ -157,11 +166,13 @@ export const createRuntimeControlPlaneLayer = ( · 43 unmodified lines | |||
| 157 | 166 | ||
| 158 | const executionResolverLayer = RuntimeExecutionResolverLive({ | 167 | const executionResolverLayer = RuntimeExecutionResolverLive({ |
| 159 | executionResolver: input.executionResolver, | 168 | executionResolver: input.executionResolver, |
| 169 | createInternalToolMap: input.createInternalToolMap, | ||
| 160 | }).pipe( | 170 | }).pipe( |
| 161 | Layer.provide( | 171 | Layer.provide( |
| 162 | Layer.mergeAll( | 172 | Layer.mergeAll( |
| 163 | baseLayer, | 173 | baseLayer, |
| 164 | secretMaterialLayer, | 174 | secretMaterialLayer, |
| 175 | sourceStoreLayer, | ||
| 165 | sourceAuthMaterialLayer, | 176 | sourceAuthMaterialLayer, |
| 166 | sourceCatalogSyncLayer, | 177 | sourceCatalogSyncLayer, |
| 167 | sourceAuthLayer, | 178 | sourceAuthLayer, |
| @@ -187,7 +198,7 @@ export const createRuntimeControlPlaneLayer = ( · 19 unmodified lines | |||
| 187 | export type ControlPlaneRuntime = { | 198 | export type ControlPlaneRuntime = { |
| 188 | persistence: LocalControlPlanePersistence; | 199 | persistence: LocalControlPlanePersistence; |
| 189 | localInstallation: LocalInstallation; | 200 | localInstallation: LocalInstallation; |
| 190 | managedRuntime: ManagedRuntime.ManagedRuntime<ControlPlaneApiRuntimeContext, never>; | 201 | managedRuntime: ManagedRuntime.ManagedRuntime<any, never>; |
| 191 | runtimeLayer: RuntimeControlPlaneLayer; | 202 | runtimeLayer: RuntimeControlPlaneLayer; |
| 192 | close: () => Promise<void>; | 203 | close: () => Promise<void>; |
| 193 | }; | 204 | }; |
| @@ -197,13 +208,13 @@ export type CreateControlPlaneRuntimeOptions = RuntimeControlPlaneOptions; · 3 unmodified lines | |||
| 197 | export const provideControlPlaneRuntime = <A, E, R>( | 208 | export const provideControlPlaneRuntime = <A, E, R>( |
| 198 | effect: Effect.Effect<A, E, R>, | 209 | effect: Effect.Effect<A, E, R>, |
| 199 | runtime: ControlPlaneRuntime, | 210 | runtime: ControlPlaneRuntime, |
| 200 | ): Effect.Effect<A, E | never, Exclude<R, ControlPlaneApiRuntimeContext>> => | 211 | ): Effect.Effect<A, E | never, any> => |
| 201 | effect.pipe(Effect.provide(runtime.managedRuntime)); | 212 | effect.pipe(Effect.provide(runtime.managedRuntime)); |
| 202 | 213 | ||
| 203 | export const createControlPlaneRuntime = ( | 214 | export const createControlPlaneRuntime = ( |
| 204 | options: CreateControlPlaneRuntimeOptions, | 215 | options: CreateControlPlaneRuntimeOptions, |
| 205 | ): Effect.Effect<ControlPlaneRuntime, Error> => | 216 | ): Effect.Effect<ControlPlaneRuntime, Error> => |
| 206 | Effect.gen(function* () { | 217 | (Effect.gen(function* () { |
| 207 | const scope = yield* Scope.make(); | 218 | const scope = yield* Scope.make(); |
| 208 | 219 | ||
| 209 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ | 220 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ |
| @@ -217,19 +228,20 @@ export const createControlPlaneRuntime = ( · 7 unmodified lines | |||
| 217 | ), | 228 | ), |
| 218 | ); | 229 | ); |
| 219 | 230 | ||
| 220 | |||
| 221 | const installationStore = LocalInstallationStore; | 231 | const installationStore = LocalInstallationStore; |
| 222 | const workspaceConfigStore = LocalWorkspaceConfigStore; | 232 | const workspaceConfigStore = LocalWorkspaceConfigStore; |
| 223 | const fileSystem = yield* FileSystem.FileSystem; | 233 | const fileSystem = yield* FileSystem.FileSystem; |
| 224 | 234 | ||
| 225 | const localInstallation = yield* installationStore.getOrProvision({ | 235 | const localInstallation = yield* installationStore |
| 226 | context: localWorkspaceContext, | 236 | .getOrProvision({ |
| 227 | }).pipe( | 237 | context: localWorkspaceContext, |
| 228 | Effect.mapError(toLocalRuntimeBootstrapError), | 238 | }) |
| 229 | Effect.catchAll((error) => | 239 | .pipe( |
| 230 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | 240 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 231 | ), | 241 | Effect.catchAll((error) => |
| 232 | ); | 242 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 243 | ), | ||
| 244 | ); | ||
| 233 | 245 | ||
| 234 | const persistence = createLocalControlPlanePersistence( | 246 | const persistence = createLocalControlPlanePersistence( |
| 235 | localWorkspaceContext, | 247 | localWorkspaceContext, |
| @@ -237,14 +249,14 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | |||
| 237 | ); | 249 | ); |
| 238 | const rows = persistence.rows; | 250 | const rows = persistence.rows; |
| 239 | 251 | ||
| 240 | const loadedLocalConfig = yield* workspaceConfigStore.load( | 252 | const loadedLocalConfig = yield* workspaceConfigStore |
| 241 | localWorkspaceContext, | 253 | .load(localWorkspaceContext) |
| 242 | ).pipe( | 254 | .pipe( |
| 243 | Effect.mapError(toLocalRuntimeBootstrapError), | 255 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 244 | Effect.catchAll((error) => | 256 | Effect.catchAll((error) => |
| 245 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | 257 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 246 | ), | 258 | ), |
| 247 | ); | 259 | ); |
| 248 | 260 | ||
| 249 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ | 261 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ |
| 250 | context: localWorkspaceContext, | 262 | context: localWorkspaceContext, |
| @@ -291,8 +303,13 @@ export const createControlPlaneRuntime = ( · 40 unmodified lines | |||
| 291 | managedRuntime, | 303 | managedRuntime, |
| 292 | runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer, | 304 | runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer, |
| 293 | close: async () => { | 305 | close: async () => { |
| 294 | await Effect.runPromise(clearAllMcpConnectionPools()).catch(() => undefined); | 306 | await Effect.runPromise(clearAllMcpConnectionPools()).catch( |
| 307 | () => undefined, | ||
| 308 | ); | ||
| 295 | await managedRuntime.dispose().catch(() => undefined); | 309 | await managedRuntime.dispose().catch(() => undefined); |
| 296 | }, | 310 | }, |
| 297 | }; | 311 | }; |
| 298 | }).pipe(Effect.provide(NodeFileSystem.layer)); | 312 | }).pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect< |
| 313 | ControlPlaneRuntime, | ||
| 314 | Error | ||
| 315 | >); | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../local/operations"; | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../local/secrets"; | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -2,7 +2,7 @@ import { · 1 unmodified line | |||
| 2 | ControlPlaneBadRequestError, | 2 | ControlPlaneBadRequestError, |
| 3 | ControlPlaneNotFoundError, | 3 | ControlPlaneNotFoundError, |
| 4 | ControlPlaneStorageError, | 4 | ControlPlaneStorageError, |
| 5 | } from "../../api/errors"; | 5 | } from "../../errors"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | ||
| 8 | export type OperationErrors<TOperation extends string = string> = { | 8 | export type OperationErrors<TOperation extends string = string> = { |
| @@ -1,7 +1,7 @@ | |||
| 1 | import { | 1 | import { |
| 2 | ControlPlaneBadRequestError, | 2 | ControlPlaneBadRequestError, |
| 3 | ControlPlaneStorageError, | 3 | ControlPlaneStorageError, |
| 4 | } from "../../api/errors"; | 4 | } from "../../errors"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | ||
| 7 | import { | 7 | import { |
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../policies/operations"; | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -3,7 +3,7 @@ import { sha256Hex } from "@executor/codemode-core"; · 2 unmodified lines | |||
| 3 | import type { | 3 | import type { |
| 4 | CreateSourcePayload, | 4 | CreateSourcePayload, |
| 5 | UpdateSourcePayload, | 5 | UpdateSourcePayload, |
| 6 | } from "#api"; | 6 | } from "../../sources/contracts"; |
| 7 | import type { | 7 | import type { |
| 8 | AccountId, | 8 | AccountId, |
| 9 | AuthArtifact, | 9 | AuthArtifact, |
| @@ -26,7 +26,7 @@ import { · 16 unmodified lines | |||
| 26 | SourceCatalogRevisionIdSchema, | 26 | SourceCatalogRevisionIdSchema, |
| 27 | } from "#schema"; | 27 | } from "#schema"; |
| 28 | import * as Effect from "effect/Effect"; | 28 | import * as Effect from "effect/Effect"; |
| 29 | 29 | ||
| 30 | import { getSourceAdapter, getSourceAdapterForSource } from "./source-adapters"; | 30 | import { getSourceAdapter, getSourceAdapterForSource } from "./source-adapters"; |
| 31 | import { | 31 | import { |
| 32 | authArtifactFromSourceAuth, | 32 | authArtifactFromSourceAuth, |
File renamed without changes.
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../sources/discovery"; | ||
File renamed without changes.
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../sources/inspection"; | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -0,0 +1 @@ | |||
| 1 | export * from "../../sources/operations"; | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -0,0 +1,170 @@ | |||
| 1 | import { | ||
| 2 | ExecutionInteractionIdSchema, | ||
| 3 | JsonObjectSchema, | ||
| 4 | ProviderAuthGrantIdSchema, | ||
| 5 | SourceAuthSchema, | ||
| 6 | SourceAuthSessionIdSchema, | ||
| 7 | SourceDiscoveryResultSchema, | ||
| 8 | SourceIdSchema, | ||
| 9 | SourceImportAuthPolicySchema, | ||
| 10 | SourceKindSchema, | ||
| 11 | SourceProbeAuthSchema, | ||
| 12 | SourceSchema, | ||
| 13 | SourceStatusSchema, | ||
| 14 | SourceOauthClientInputSchema, | ||
| 15 | WorkspaceIdSchema, | ||
| 16 | WorkspaceOauthClientIdSchema, | ||
| 17 | WorkspaceOauthClientSchema, | ||
| 18 | } from "../schema"; | ||
| 19 | import { | ||
| 20 | ConnectSourcePayloadSchema, | ||
| 21 | type ConnectSourcePayload, | ||
| 22 | } from "../runtime/sources/source-adapters"; | ||
| 23 | import * as Schema from "effect/Schema"; | ||
| 24 | |||
| 25 | import { | ||
| 26 | OptionalTrimmedNonEmptyStringSchema, | ||
| 27 | TrimmedNonEmptyStringSchema, | ||
| 28 | } from "../string-schemas"; | ||
| 29 | |||
| 30 | const createSourcePayloadRequiredSchema = Schema.Struct({ | ||
| 31 | name: TrimmedNonEmptyStringSchema, | ||
| 32 | kind: SourceKindSchema, | ||
| 33 | endpoint: TrimmedNonEmptyStringSchema, | ||
| 34 | }); | ||
| 35 | |||
| 36 | const createSourcePayloadOptionalSchema = Schema.Struct({ | ||
| 37 | status: Schema.optional(SourceStatusSchema), | ||
| 38 | enabled: Schema.optional(Schema.Boolean), | ||
| 39 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 40 | binding: Schema.optional(JsonObjectSchema), | ||
| 41 | importAuthPolicy: Schema.optional(SourceImportAuthPolicySchema), | ||
| 42 | importAuth: Schema.optional(SourceAuthSchema), | ||
| 43 | auth: Schema.optional(SourceAuthSchema), | ||
| 44 | sourceHash: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 45 | lastError: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 46 | }); | ||
| 47 | |||
| 48 | export const CreateSourcePayloadSchema = Schema.extend( | ||
| 49 | createSourcePayloadRequiredSchema, | ||
| 50 | createSourcePayloadOptionalSchema, | ||
| 51 | ); | ||
| 52 | |||
| 53 | export type CreateSourcePayload = typeof CreateSourcePayloadSchema.Type; | ||
| 54 | |||
| 55 | export const UpdateSourcePayloadSchema = Schema.Struct({ | ||
| 56 | name: OptionalTrimmedNonEmptyStringSchema, | ||
| 57 | endpoint: OptionalTrimmedNonEmptyStringSchema, | ||
| 58 | status: Schema.optional(SourceStatusSchema), | ||
| 59 | enabled: Schema.optional(Schema.Boolean), | ||
| 60 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 61 | binding: Schema.optional(JsonObjectSchema), | ||
| 62 | importAuthPolicy: Schema.optional(SourceImportAuthPolicySchema), | ||
| 63 | importAuth: Schema.optional(SourceAuthSchema), | ||
| 64 | auth: Schema.optional(SourceAuthSchema), | ||
| 65 | sourceHash: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 66 | lastError: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 67 | }); | ||
| 68 | |||
| 69 | export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; | ||
| 70 | |||
| 71 | export const CredentialPageUrlParamsSchema = Schema.Struct({ | ||
| 72 | interactionId: ExecutionInteractionIdSchema, | ||
| 73 | }); | ||
| 74 | |||
| 75 | export const CredentialSubmitPayloadSchema = Schema.Struct({ | ||
| 76 | action: Schema.optional(Schema.Literal("submit", "continue", "cancel")), | ||
| 77 | token: Schema.optional(Schema.String), | ||
| 78 | }); | ||
| 79 | |||
| 80 | export const CredentialOauthCompleteUrlParamsSchema = Schema.Struct({ | ||
| 81 | state: Schema.String, | ||
| 82 | code: Schema.optional(Schema.String), | ||
| 83 | error: Schema.optional(Schema.String), | ||
| 84 | error_description: Schema.optional(Schema.String), | ||
| 85 | }); | ||
| 86 | |||
| 87 | export const WorkspaceOauthClientQuerySchema = Schema.Struct({ | ||
| 88 | providerKey: Schema.String, | ||
| 89 | }); | ||
| 90 | |||
| 91 | export const CreateWorkspaceOauthClientPayloadSchema = Schema.Struct({ | ||
| 92 | providerKey: Schema.String, | ||
| 93 | label: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 94 | oauthClient: SourceOauthClientInputSchema, | ||
| 95 | }); | ||
| 96 | |||
| 97 | export type CreateWorkspaceOauthClientPayload = | ||
| 98 | typeof CreateWorkspaceOauthClientPayloadSchema.Type; | ||
| 99 | |||
| 100 | export const oauthClientIdParam = WorkspaceOauthClientIdSchema; | ||
| 101 | export const grantIdParam = ProviderAuthGrantIdSchema; | ||
| 102 | |||
| 103 | const ConnectGoogleDiscoveryBatchSourceSchema = Schema.Struct({ | ||
| 104 | service: TrimmedNonEmptyStringSchema, | ||
| 105 | version: TrimmedNonEmptyStringSchema, | ||
| 106 | discoveryUrl: Schema.optional(Schema.NullOr(TrimmedNonEmptyStringSchema)), | ||
| 107 | scopes: Schema.optional(Schema.Array(TrimmedNonEmptyStringSchema)), | ||
| 108 | name: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 109 | namespace: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 110 | }); | ||
| 111 | |||
| 112 | export const ConnectSourceBatchPayloadSchema = Schema.Struct({ | ||
| 113 | workspaceOauthClientId: WorkspaceOauthClientIdSchema, | ||
| 114 | sources: Schema.Array(ConnectGoogleDiscoveryBatchSourceSchema), | ||
| 115 | }); | ||
| 116 | |||
| 117 | export type ConnectSourceBatchPayload = typeof ConnectSourceBatchPayloadSchema.Type; | ||
| 118 | |||
| 119 | export const ConnectSourceBatchResultSchema = Schema.Struct({ | ||
| 120 | results: Schema.Array( | ||
| 121 | Schema.Struct({ | ||
| 122 | source: SourceSchema, | ||
| 123 | status: Schema.Literal("connected", "pending_oauth"), | ||
| 124 | }), | ||
| 125 | ), | ||
| 126 | providerOauthSession: Schema.NullOr( | ||
| 127 | Schema.Struct({ | ||
| 128 | sessionId: SourceAuthSessionIdSchema, | ||
| 129 | authorizationUrl: Schema.String, | ||
| 130 | sourceIds: Schema.Array(SourceIdSchema), | ||
| 131 | }), | ||
| 132 | ), | ||
| 133 | }); | ||
| 134 | |||
| 135 | export type ConnectSourceBatchResult = typeof ConnectSourceBatchResultSchema.Type; | ||
| 136 | |||
| 137 | export const DiscoverSourcePayloadSchema = Schema.Struct({ | ||
| 138 | url: TrimmedNonEmptyStringSchema, | ||
| 139 | probeAuth: Schema.optional(SourceProbeAuthSchema), | ||
| 140 | }); | ||
| 141 | |||
| 142 | export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type; | ||
| 143 | |||
| 144 | export const ConnectSourceResultSchema = Schema.Union( | ||
| 145 | Schema.Struct({ | ||
| 146 | kind: Schema.Literal("connected"), | ||
| 147 | source: SourceSchema, | ||
| 148 | }), | ||
| 149 | Schema.Struct({ | ||
| 150 | kind: Schema.Literal("credential_required"), | ||
| 151 | source: SourceSchema, | ||
| 152 | credentialSlot: Schema.Literal("runtime", "import"), | ||
| 153 | }), | ||
| 154 | Schema.Struct({ | ||
| 155 | kind: Schema.Literal("oauth_required"), | ||
| 156 | source: SourceSchema, | ||
| 157 | sessionId: SourceAuthSessionIdSchema, | ||
| 158 | authorizationUrl: Schema.String, | ||
| 159 | }), | ||
| 160 | ); | ||
| 161 | |||
| 162 | export type ConnectSourceResult = typeof ConnectSourceResultSchema.Type; | ||
| 163 | |||
| 164 | export { | ||
| 165 | ConnectSourcePayloadSchema, | ||
| 166 | type ConnectSourcePayload, | ||
| 167 | SourceDiscoveryResultSchema, | ||
| 168 | WorkspaceIdSchema, | ||
| 169 | WorkspaceOauthClientSchema, | ||
| 170 | }; | ||
| @@ -9,7 +9,7 @@ import { · 8 unmodified lines | |||
| 9 | type SourceDiscoveryResult, | 9 | type SourceDiscoveryResult, |
| 10 | } from "@executor/source-core"; | 10 | } from "@executor/source-core"; |
| 11 | 11 | ||
| 12 | import { builtInSourceAdapters } from "./source-adapters"; | 12 | import { builtInSourceAdapters } from "../runtime/sources/source-adapters"; |
| 13 | 13 | ||
| 14 | type DiscoverableSourceAdapter = SourceAdapter & { | 14 | type DiscoverableSourceAdapter = SourceAdapter & { |
| 15 | detectSource: NonNullable<SourceAdapter["detectSource"]>; | 15 | detectSource: NonNullable<SourceAdapter["detectSource"]>; |
| @@ -36,8 +36,8 @@ export const discoverSource = (input: { · 20 unmodified lines | |||
| 36 | 36 | ||
| 37 | const adaptersByPriority = [...discoverableSourceAdapters].sort( | 37 | const adaptersByPriority = [...discoverableSourceAdapters].sort( |
| 38 | (left, right) => | 38 | (left, right) => |
| 39 | (right.discoveryPriority?.({ normalizedUrl }) ?? 0) | 39 | (right.discoveryPriority?.({ normalizedUrl }) ?? 0) - |
| 40 | - (left.discoveryPriority?.({ normalizedUrl }) ?? 0), | 40 | (left.discoveryPriority?.({ normalizedUrl }) ?? 0), |
| 41 | ); | 41 | ); |
| 42 | 42 | ||
| 43 | for (const adapter of adaptersByPriority) { | 43 | for (const adapter of adaptersByPriority) { |
| @@ -9,23 +9,23 @@ import type { · 8 unmodified lines | |||
| 9 | SourceInspectionToolListItem, | 9 | SourceInspectionToolListItem, |
| 10 | SourceInspectionToolSummary, | 10 | SourceInspectionToolSummary, |
| 11 | WorkspaceId, | 11 | WorkspaceId, |
| 12 | } from "#schema"; | 12 | } from "../schema"; |
| 13 | import { | 13 | import { |
| 14 | ControlPlaneNotFoundError, | 14 | ControlPlaneNotFoundError, |
| 15 | ControlPlaneStorageError, | 15 | ControlPlaneStorageError, |
| 16 | } from "../../api/errors"; | 16 | } from "../errors"; |
| 17 | import * as Effect from "effect/Effect"; | 17 | import * as Effect from "effect/Effect"; |
| 18 | 18 | ||
| 19 | import { LocalSourceArtifactMissingError } from "../local/errors"; | 19 | import { LocalSourceArtifactMissingError } from "../runtime/local/errors"; |
| 20 | import { operationErrors } from "../policy/operation-errors"; | 20 | import { operationErrors } from "../runtime/policy/operation-errors"; |
| 21 | import { | 21 | import { |
| 22 | buildLoadedSourceCatalogToolContract, | 22 | buildLoadedSourceCatalogToolContract, |
| 23 | expandCatalogToolByPath, | 23 | expandCatalogToolByPath, |
| 24 | expandCatalogTools, | 24 | expandCatalogTools, |
| 25 | loadSourceWithCatalog, | 25 | loadSourceWithCatalog, |
| 26 | type LoadedSourceCatalogTool, | 26 | type LoadedSourceCatalogTool, |
| 27 | } from "../catalog/source/runtime"; | 27 | } from "../runtime/catalog/source/runtime"; |
| 28 | import { RuntimeSourceStoreService } from "./source-store"; | 28 | import { RuntimeSourceStoreService } from "../runtime/sources/source-store"; |
| 29 | 29 | ||
| 30 | const sourceInspectOps = { | 30 | const sourceInspectOps = { |
| 31 | bundle: operationErrors("sources.inspect.bundle"), | 31 | bundle: operationErrors("sources.inspect.bundle"), |
| @@ -41,9 +41,9 @@ const tokenize = (value: string): Array<string> => · 9 unmodified lines | |||
| 41 | .filter(Boolean); | 41 | .filter(Boolean); |
| 42 | 42 | ||
| 43 | const canInspectSourceWithoutCatalog = (source: Source): boolean => | 43 | const canInspectSourceWithoutCatalog = (source: Source): boolean => |
| 44 | source.status === "draft" | 44 | source.status === "draft" || |
| 45 | || source.status === "probing" | 45 | source.status === "probing" || |
| 46 | || source.status === "auth_required"; | 46 | source.status === "auth_required"; |
| 47 | 47 | ||
| 48 | const loadSourceForMissingCatalog = (input: { | 48 | const loadSourceForMissingCatalog = (input: { |
| 49 | workspaceId: WorkspaceId; | 49 | workspaceId: WorkspaceId; |
| @@ -88,7 +88,8 @@ const loadSourceCatalogOrEmpty = (input: { · 38 unmodified lines | |||
| 88 | kind: "empty" as const, | 88 | kind: "empty" as const, |
| 89 | source, | 89 | source, |
| 90 | }; | 90 | }; |
| 91 | })), | 91 | }), |
| 92 | ), | ||
| 92 | ); | 93 | ); |
| 93 | 94 | ||
| 94 | const executableDetails = (tool: LoadedSourceCatalogTool) => { | 95 | const executableDetails = (tool: LoadedSourceCatalogTool) => { |
| @@ -105,7 +106,9 @@ const executableDetails = (tool: LoadedSourceCatalogTool) => { · 10 unmodified lines | |||
| 105 | }; | 106 | }; |
| 106 | }; | 107 | }; |
| 107 | 108 | ||
| 108 | const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({ | 109 | const inspectionToolListItemFromTool = ( |
| 110 | tool: LoadedSourceCatalogTool, | ||
| 111 | ): SourceInspectionToolListItem => ({ | ||
| 109 | path: tool.path, | 112 | path: tool.path, |
| 110 | method: executableDetails(tool).method, | 113 | method: executableDetails(tool).method, |
| 111 | ...(tool.descriptor.contract?.inputTypePreview | 114 | ...(tool.descriptor.contract?.inputTypePreview |
| @@ -116,15 +119,23 @@ const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceIn · 4 unmodified lines | |||
| 116 | : {}), | 119 | : {}), |
| 117 | }); | 120 | }); |
| 118 | 121 | ||
| 119 | const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => { | 122 | const persistedToolSummaryFromTool = ( |
| 123 | tool: LoadedSourceCatalogTool, | ||
| 124 | ): SourceInspectionToolSummary => { | ||
| 120 | const details = executableDetails(tool); | 125 | const details = executableDetails(tool); |
| 121 | 126 | ||
| 122 | return { | 127 | return { |
| 123 | path: tool.path, | 128 | path: tool.path, |
| 124 | sourceKey: tool.source.id, | 129 | sourceKey: tool.source.id, |
| 125 | ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}), | 130 | ...(tool.capability.surface.title |
| 131 | ? { title: tool.capability.surface.title } | ||
| 132 | : {}), | ||
| 126 | ...(tool.capability.surface.summary || tool.capability.surface.description | 133 | ...(tool.capability.surface.summary || tool.capability.surface.description |
| 127 | ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! } | 134 | ? { |
| 135 | description: | ||
| 136 | tool.capability.surface.summary ?? | ||
| 137 | tool.capability.surface.description!, | ||
| 138 | } | ||
| 128 | : {}), | 139 | : {}), |
| 129 | protocol: details.protocol, | 140 | protocol: details.protocol, |
| 130 | toolId: tool.path.split(".").at(-1) ?? tool.path, | 141 | toolId: tool.path.split(".").at(-1) ?? tool.path, |
| @@ -145,7 +156,10 @@ const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInsp · 14 unmodified lines | |||
| 145 | }; | 156 | }; |
| 146 | 157 | ||
| 147 | const nativeEncodingLanguage = (encoding: string | undefined): string => | 158 | const nativeEncodingLanguage = (encoding: string | undefined): string => |
| 148 | encoding === "graphql" || encoding === "yaml" || encoding === "json" || encoding === "text" | 159 | encoding === "graphql" || |
| 160 | encoding === "yaml" || | ||
| 161 | encoding === "json" || | ||
| 162 | encoding === "text" | ||
| 149 | ? encoding | 163 | ? encoding |
| 150 | : "json"; | 164 | : "json"; |
| 151 | 165 | ||
| @@ -171,12 +185,24 @@ export const inspectionToolDetailFromTool = ( · 19 unmodified lines | |||
| 171 | const contract = yield* buildLoadedSourceCatalogToolContract(tool); | 185 | const contract = yield* buildLoadedSourceCatalogToolContract(tool); |
| 172 | const overviewItems = [ | 186 | const overviewItems = [ |
| 173 | { label: "Protocol", value: details.protocol }, | 187 | { label: "Protocol", value: details.protocol }, |
| 174 | ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []), | 188 | ...(details.method |
| 175 | ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []), | 189 | ? [{ label: "Method", value: details.method, mono: true }] |
| 176 | ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []), | 190 | : []), |
| 177 | ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []), | 191 | ...(details.pathTemplate |
| 178 | ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []), | 192 | ? [{ label: "Target", value: details.pathTemplate, mono: true }] |
| 179 | ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []), | 193 | : []), |
| 194 | ...(details.operationId | ||
| 195 | ? [{ label: "Operation", value: details.operationId, mono: true }] | ||
| 196 | : []), | ||
| 197 | ...(details.group | ||
| 198 | ? [{ label: "Group", value: details.group, mono: true }] | ||
| 199 | : []), | ||
| 200 | ...(details.leaf | ||
| 201 | ? [{ label: "Leaf", value: details.leaf, mono: true }] | ||
| 202 | : []), | ||
| 203 | ...(details.rawToolId | ||
| 204 | ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] | ||
| 205 | : []), | ||
| 180 | { label: "Signature", value: contract.callSignature, mono: true }, | 206 | { label: "Signature", value: contract.callSignature, mono: true }, |
| 181 | { label: "Call shape", value: contract.callShapeId, mono: true }, | 207 | { label: "Call shape", value: contract.callShapeId, mono: true }, |
| 182 | ...(contract.resultShapeId | 208 | ...(contract.resultShapeId |
| @@ -211,11 +237,13 @@ export const inspectionToolDetailFromTool = ( · 28 unmodified lines | |||
| 211 | items: overviewItems, | 237 | items: overviewItems, |
| 212 | }, | 238 | }, |
| 213 | ...(summary.description | 239 | ...(summary.description |
| 214 | ? [{ | 240 | ? [ |
| 215 | kind: "markdown" as const, | 241 | { |
| 216 | title: "Description", | 242 | kind: "markdown" as const, |
| 217 | body: summary.description, | 243 | title: "Description", |
| 218 | }] | 244 | body: summary.description, |
| 245 | }, | ||
| 246 | ] | ||
| 219 | : []), | 247 | : []), |
| 220 | ...([ | 248 | ...([ |
| 221 | jsonSection("Capability", tool.capability), | 249 | jsonSection("Capability", tool.capability), |
| @@ -231,7 +259,9 @@ export const inspectionToolDetailFromTool = ( · 9 unmodified lines | |||
| 231 | summary: tool.capability.surface.summary, | 259 | summary: tool.capability.surface.summary, |
| 232 | description: tool.capability.surface.description, | 260 | description: tool.capability.surface.description, |
| 233 | }), | 261 | }), |
| 234 | ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>), | 262 | ].filter((section) => section !== null) as Array< |
| 263 | SourceInspectionToolDetail["sections"][number] | ||
| 264 | >), | ||
| 235 | ...nativeSections, | 265 | ...nativeSections, |
| 236 | ]; | 266 | ]; |
| 237 | 267 | ||
| @@ -323,7 +353,9 @@ const scoreTool = (input: { · 85 unmodified lines | |||
| 323 | const titleTokens = tokenize(summary.title ?? ""); | 353 | const titleTokens = tokenize(summary.title ?? ""); |
| 324 | const descriptionTokens = tokenize(summary.description ?? ""); | 354 | const descriptionTokens = tokenize(summary.description ?? ""); |
| 325 | const tagTokens = summary.tags.flatMap(tokenize); | 355 | const tagTokens = summary.tags.flatMap(tokenize); |
| 326 | const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`); | 356 | const methodPathTokens = tokenize( |
| 357 | `${summary.method ?? ""} ${summary.pathTemplate ?? ""}`, | ||
| 358 | ); | ||
| 327 | 359 | ||
| 328 | for (const token of input.queryTokens) { | 360 | for (const token of input.queryTokens) { |
| 329 | if (pathTokens.includes(token)) { | 361 | if (pathTokens.includes(token)) { |
| @@ -346,7 +378,10 @@ const scoreTool = (input: { · 16 unmodified lines | |||
| 346 | reasons.push(`method/path matches ${token} (+6)`); | 378 | reasons.push(`method/path matches ${token} (+6)`); |
| 347 | continue; | 379 | continue; |
| 348 | } | 380 | } |
| 349 | if (descriptionTokens.includes(token) || input.tool.searchText.includes(token)) { | 381 | if ( |
| 382 | descriptionTokens.includes(token) || | ||
| 383 | input.tool.searchText.includes(token) | ||
| 384 | ) { | ||
| 350 | score += 2; | 385 | score += 2; |
| 351 | reasons.push(`description/text matches ${token} (+2)`); | 386 | reasons.push(`description/text matches ${token} (+2)`); |
| 352 | } | 387 | } |
| @@ -360,8 +395,12 @@ const scoreTool = (input: { · 7 unmodified lines | |||
| 360 | path: input.tool.path, | 395 | path: input.tool.path, |
| 361 | score, | 396 | score, |
| 362 | ...(summary.description ? { description: summary.description } : {}), | 397 | ...(summary.description ? { description: summary.description } : {}), |
| 363 | ...(summary.inputTypePreview ? { inputTypePreview: summary.inputTypePreview } : {}), | 398 | ...(summary.inputTypePreview |
| 364 | ...(summary.outputTypePreview ? { outputTypePreview: summary.outputTypePreview } : {}), | 399 | ? { inputTypePreview: summary.inputTypePreview } |
| 400 | : {}), | ||
| 401 | ...(summary.outputTypePreview | ||
| 402 | ? { outputTypePreview: summary.outputTypePreview } | ||
| 403 | : {}), | ||
| 365 | reasons, | 404 | reasons, |
| 366 | }; | 405 | }; |
| 367 | }; | 406 | }; |
| @@ -402,7 +441,9 @@ export const getSourceInspection = (input: { · 34 unmodified lines | |||
| 402 | namespace: inspection.namespace, | 441 | namespace: inspection.namespace, |
| 403 | pipelineKind: inspection.pipelineKind, | 442 | pipelineKind: inspection.pipelineKind, |
| 404 | toolCount: inspection.tools.length, | 443 | toolCount: inspection.tools.length, |
| 405 | tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)), | 444 | tools: inspection.tools.map((tool) => |
| 445 | inspectionToolListItemFromTool(tool), | ||
| 446 | ), | ||
| 406 | } satisfies SourceInspection; | 447 | } satisfies SourceInspection; |
| 407 | }).pipe( | 448 | }).pipe( |
| 408 | Effect.mapError((cause) => | 449 | Effect.mapError((cause) => |
| @@ -410,7 +451,8 @@ export const getSourceInspection = (input: { · 1 unmodified line | |||
| 410 | sourceInspectOps.bundle, | 451 | sourceInspectOps.bundle, |
| 411 | cause, | 452 | cause, |
| 412 | "Failed building source inspection bundle", | 453 | "Failed building source inspection bundle", |
| 413 | )), | 454 | ), |
| 455 | ), | ||
| 414 | ); | 456 | ); |
| 415 | 457 | ||
| 416 | export const getSourceInspectionToolDetail = (input: { | 458 | export const getSourceInspectionToolDetail = (input: { |
| @@ -428,9 +470,9 @@ export const getSourceInspectionToolDetail = (input: { · 11 unmodified lines | |||
| 428 | 470 | ||
| 429 | if (!tool) { | 471 | if (!tool) { |
| 430 | return yield* sourceInspectOps.tool.notFound( | 472 | return yield* sourceInspectOps.tool.notFound( |
| 431 | "Tool not found", | 473 | "Tool not found", |
| 432 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, | 474 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, |
| 433 | ); | 475 | ); |
| 434 | } | 476 | } |
| 435 | 477 | ||
| 436 | return yield* inspectionToolDetailFromTool(tool); | 478 | return yield* inspectionToolDetailFromTool(tool); |
| @@ -440,7 +482,8 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines | |||
| 440 | sourceInspectOps.tool, | 482 | sourceInspectOps.tool, |
| 441 | cause, | 483 | cause, |
| 442 | "Failed building source inspection tool detail", | 484 | "Failed building source inspection tool detail", |
| 443 | )), | 485 | ), |
| 486 | ), | ||
| 444 | ); | 487 | ); |
| 445 | 488 | ||
| 446 | export const discoverSourceInspectionTools = (input: { | 489 | export const discoverSourceInspectionTools = (input: { |
| @@ -463,9 +506,12 @@ export const discoverSourceInspectionTools = (input: { · 16 unmodified lines | |||
| 463 | tool, | 506 | tool, |
| 464 | }), | 507 | }), |
| 465 | ) | 508 | ) |
| 466 | .filter((value): value is SourceInspectionDiscoverResultItem => value !== null) | 509 | .filter( |
| 467 | .sort((left, right) => | 510 | (value): value is SourceInspectionDiscoverResultItem => value !== null, |
| 468 | right.score - left.score || left.path.localeCompare(right.path), | 511 | ) |
| 512 | .sort( | ||
| 513 | (left, right) => | ||
| 514 | right.score - left.score || left.path.localeCompare(right.path), | ||
| 469 | ) | 515 | ) |
| 470 | .slice(0, input.payload.limit ?? 12); | 516 | .slice(0, input.payload.limit ?? 12); |
| 471 | 517 | ||
| @@ -482,5 +528,6 @@ export const discoverSourceInspectionTools = (input: { · 10 unmodified lines | |||
| 482 | sourceInspectOps.discover, | 528 | sourceInspectOps.discover, |
| 483 | cause, | 529 | cause, |
| 484 | "Failed building source inspection discovery", | 530 | "Failed building source inspection discovery", |
| 485 | )), | 531 | ), |
| 532 | ), | ||
| 486 | ); | 533 | ); |
| @@ -1,33 +1,30 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | CreateSourcePayload, | 2 | CreateSourcePayload, |
| 3 | UpdateSourcePayload, | 3 | UpdateSourcePayload, |
| 4 | } from "../../api/sources/api"; | 4 | } from "./contracts"; |
| 5 | import { | 5 | import { |
| 6 | type AccountId, | 6 | type AccountId, |
| 7 | SourceIdSchema, | 7 | SourceIdSchema, |
| 8 | type Source, | 8 | type Source, |
| 9 | type SourceId, | 9 | type SourceId, |
| 10 | type WorkspaceId, | 10 | type WorkspaceId, |
| 11 | } from "#schema"; | 11 | } from "../schema"; |
| 12 | import * as Either from "effect/Either"; | 12 | import * as Either from "effect/Either"; |
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | 14 | ||
| 15 | import { | 15 | import { |
| 16 | createSourceFromPayload, | 16 | createSourceFromPayload, |
| 17 | updateSourceFromPayload, | 17 | updateSourceFromPayload, |
| 18 | } from "./source-definitions"; | 18 | } from "../runtime/sources/source-definitions"; |
| 19 | import { getSourceAdapterForSource } from "./source-adapters"; | 19 | import { getSourceAdapterForSource } from "../runtime/sources/source-adapters"; |
| 20 | import { mapPersistenceError } from "../runtime/policy/operations-shared"; | ||
| 21 | import { operationErrors } from "../runtime/policy/operation-errors"; | ||
| 20 | import { | 22 | import { |
| 21 | mapPersistenceError, | 23 | ControlPlaneStore, |
| 22 | } from "../policy/operations-shared"; | 24 | type ControlPlaneStoreShape, |
| 23 | import { | 25 | } from "../runtime/store"; |
| 24 | operationErrors, | 26 | import { RuntimeSourceCatalogSyncService } from "../runtime/catalog/source/sync"; |
| 25 | } from "../policy/operation-errors"; | 27 | import { RuntimeSourceStoreService } from "../runtime/sources/source-store"; |
| 26 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store"; | ||
| 27 | import { RuntimeSourceCatalogSyncService } from "../catalog/source/sync"; | ||
| 28 | import { | ||
| 29 | RuntimeSourceStoreService, | ||
| 30 | } from "./source-store"; | ||
| 31 | 28 | ||
| 32 | const sourceOps = { | 29 | const sourceOps = { |
| 33 | list: operationErrors("sources.list"), | 30 | list: operationErrors("sources.list"), |
| @@ -45,9 +42,7 @@ const syncArtifactsForSource = (input: { · 11 unmodified lines | |||
| 45 | sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>; | 42 | sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>; |
| 46 | source: Source; | 43 | source: Source; |
| 47 | actorAccountId: AccountId; | 44 | actorAccountId: AccountId; |
| 48 | operation: | 45 | operation: typeof sourceOps.create | typeof sourceOps.update; |
| 49 | | typeof sourceOps.create | ||
| 50 | | typeof sourceOps.update; | ||
| 51 | }) => | 46 | }) => |
| 52 | Effect.gen(function* () { | 47 | Effect.gen(function* () { |
| 53 | const catalogSyncService = yield* RuntimeSourceCatalogSyncService; | 48 | const catalogSyncService = yield* RuntimeSourceCatalogSyncService; |
| @@ -96,7 +91,10 @@ const syncArtifactsForSource = (input: { · 42 unmodified lines | |||
| 96 | }), | 91 | }), |
| 97 | onLeft: (error) => | 92 | onLeft: (error) => |
| 98 | Effect.gen(function* () { | 93 | Effect.gen(function* () { |
| 99 | if (autoProbe || (input.source.enabled && input.source.status === "connected")) { | 94 | if ( |
| 95 | autoProbe || | ||
| 96 | (input.source.enabled && input.source.status === "connected") | ||
| 97 | ) { | ||
| 100 | const erroredSource = yield* updateSourceFromPayload({ | 98 | const erroredSource = yield* updateSourceFromPayload({ |
| 101 | source: input.source, | 99 | source: input.source, |
| 102 | payload: { | 100 | payload: { |
| @@ -104,11 +102,11 @@ const syncArtifactsForSource = (input: { · 1 unmodified line | |||
| 104 | lastError: error.message, | 102 | lastError: error.message, |
| 105 | }, | 103 | }, |
| 106 | now: Date.now(), | 104 | now: Date.now(), |
| 107 | }).pipe( | 105 | }).pipe( |
| 108 | Effect.mapError((cause) => | 106 | Effect.mapError((cause) => |
| 109 | input.operation.badRequest( | 107 | input.operation.badRequest( |
| 110 | "Failed indexing source tools", | 108 | "Failed indexing source tools", |
| 111 | cause instanceof Error ? cause.message : String(cause), | 109 | cause instanceof Error ? cause.message : String(cause), |
| 112 | ), | 110 | ), |
| 113 | ), | 111 | ), |
| 114 | ); | 112 | ); |
| @@ -121,7 +119,10 @@ const syncArtifactsForSource = (input: { · 6 unmodified lines | |||
| 121 | ); | 119 | ); |
| 122 | } | 120 | } |
| 123 | 121 | ||
| 124 | return yield* input.operation.unknownStorage(error, "Failed syncing source tools"); | 122 | return yield* input.operation.unknownStorage( |
| 123 | error, | ||
| 124 | "Failed syncing source tools", | ||
| 125 | ); | ||
| 125 | }), | 126 | }), |
| 126 | }); | 127 | }); |
| 127 | }); | 128 | }); |
| @@ -134,17 +135,20 @@ export const listSources = (input: { · 6 unmodified lines | |||
| 134 | Effect.gen(function* () { | 135 | Effect.gen(function* () { |
| 135 | const sourceStore = yield* RuntimeSourceStoreService; | 136 | const sourceStore = yield* RuntimeSourceStoreService; |
| 136 | 137 | ||
| 137 | return yield* sourceStore.loadSourcesInWorkspace(input.workspaceId, { | 138 | return yield* sourceStore |
| 138 | actorAccountId: input.accountId, | 139 | .loadSourcesInWorkspace(input.workspaceId, { |
| 139 | }).pipe( | 140 | actorAccountId: input.accountId, |
| 140 | Effect.mapError((error) => | 141 | }) |
| 141 | sourceOps.list.unknownStorage( | 142 | .pipe( |
| 142 | error, | 143 | Effect.mapError((error) => |
| 143 | "Failed projecting stored sources", | 144 | sourceOps.list.unknownStorage( |
| 145 | error, | ||
| 146 | "Failed projecting stored sources", | ||
| 147 | ), | ||
| 144 | ), | 148 | ), |
| 145 | ), | 149 | ); |
| 146 | ); | 150 | }), |
| 147 | })); | 151 | ); |
| 148 | 152 | ||
| 149 | export const createSource = (input: { | 153 | export const createSource = (input: { |
| 150 | workspaceId: WorkspaceId; | 154 | workspaceId: WorkspaceId; |
| @@ -186,7 +190,8 @@ export const createSource = (input: { · 35 unmodified lines | |||
| 186 | }); | 190 | }); |
| 187 | 191 | ||
| 188 | return synchronizedSource; | 192 | return synchronizedSource; |
| 189 | })); | 193 | }), |
| 194 | ); | ||
| 190 | 195 | ||
| 191 | export const getSource = (input: { | 196 | export const getSource = (input: { |
| 192 | workspaceId: WorkspaceId; | 197 | workspaceId: WorkspaceId; |
| @@ -197,24 +202,28 @@ export const getSource = (input: { · 4 unmodified lines | |||
| 197 | Effect.gen(function* () { | 202 | Effect.gen(function* () { |
| 198 | const sourceStore = yield* RuntimeSourceStoreService; | 203 | const sourceStore = yield* RuntimeSourceStoreService; |
| 199 | 204 | ||
| 200 | return yield* sourceStore.loadSourceById({ | 205 | return yield* sourceStore |
| 201 | workspaceId: input.workspaceId, | 206 | .loadSourceById({ |
| 202 | sourceId: input.sourceId, | 207 | workspaceId: input.workspaceId, |
| 203 | actorAccountId: input.accountId, | 208 | sourceId: input.sourceId, |
| 204 | }).pipe( | 209 | actorAccountId: input.accountId, |
| 205 | Effect.mapError((cause) => | 210 | }) |
| 206 | cause instanceof Error && cause.message.startsWith("Source not found:") | 211 | .pipe( |
| 207 | ? sourceOps.get.notFound( | 212 | Effect.mapError((cause) => |
| 208 | "Source not found", | 213 | cause instanceof Error && |
| 209 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | 214 | cause.message.startsWith("Source not found:") |
| 210 | ) | 215 | ? sourceOps.get.notFound( |
| 211 | : sourceOps.get.unknownStorage( | 216 | "Source not found", |
| 212 | cause, | 217 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, |
| 213 | "Failed projecting stored source", | 218 | ) |
| 214 | ), | 219 | : sourceOps.get.unknownStorage( |
| 215 | ), | 220 | cause, |
| 216 | ); | 221 | "Failed projecting stored source", |
| 217 | })); | 222 | ), |
| 223 | ), | ||
| 224 | ); | ||
| 225 | }), | ||
| 226 | ); | ||
| 218 | 227 | ||
| 219 | export const updateSource = (input: { | 228 | export const updateSource = (input: { |
| 220 | workspaceId: WorkspaceId; | 229 | workspaceId: WorkspaceId; |
| @@ -225,23 +234,26 @@ export const updateSource = (input: { · 4 unmodified lines | |||
| 225 | Effect.flatMap(ControlPlaneStore, (store) => | 234 | Effect.flatMap(ControlPlaneStore, (store) => |
| 226 | Effect.gen(function* () { | 235 | Effect.gen(function* () { |
| 227 | const sourceStore = yield* RuntimeSourceStoreService; | 236 | const sourceStore = yield* RuntimeSourceStoreService; |
| 228 | const existingSource = yield* sourceStore.loadSourceById({ | 237 | const existingSource = yield* sourceStore |
| 229 | workspaceId: input.workspaceId, | 238 | .loadSourceById({ |
| 230 | sourceId: input.sourceId, | 239 | workspaceId: input.workspaceId, |
| 231 | actorAccountId: input.accountId, | 240 | sourceId: input.sourceId, |
| 232 | }).pipe( | 241 | actorAccountId: input.accountId, |
| 233 | Effect.mapError((cause) => | 242 | }) |
| 234 | cause instanceof Error && cause.message.startsWith("Source not found:") | 243 | .pipe( |
| 235 | ? sourceOps.update.notFound( | 244 | Effect.mapError((cause) => |
| 236 | "Source not found", | 245 | cause instanceof Error && |
| 237 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | 246 | cause.message.startsWith("Source not found:") |
| 238 | ) | 247 | ? sourceOps.update.notFound( |
| 239 | : sourceOps.update.unknownStorage( | 248 | "Source not found", |
| 240 | cause, | 249 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, |
| 241 | "Failed projecting stored source", | 250 | ) |
| 242 | ), | 251 | : sourceOps.update.unknownStorage( |
| 243 | ), | 252 | cause, |
| 244 | ); | 253 | "Failed projecting stored source", |
| 254 | ), | ||
| 255 | ), | ||
| 256 | ); | ||
| 245 | 257 | ||
| 246 | const updatedSource = yield* updateSourceFromPayload({ | 258 | const updatedSource = yield* updateSourceFromPayload({ |
| 247 | source: existingSource, | 259 | source: existingSource, |
| @@ -272,7 +284,8 @@ export const updateSource = (input: { · 24 unmodified lines | |||
| 272 | }); | 284 | }); |
| 273 | 285 | ||
| 274 | return synchronizedSource; | 286 | return synchronizedSource; |
| 275 | })); | 287 | }), |
| 288 | ); | ||
| 276 | 289 | ||
| 277 | export const removeSource = (input: { | 290 | export const removeSource = (input: { |
| 278 | workspaceId: WorkspaceId; | 291 | workspaceId: WorkspaceId; |
| @@ -290,5 +303,5 @@ export const removeSource = (input: { · 11 unmodified lines | |||
| 290 | ); | 303 | ); |
| 291 | 304 | ||
| 292 | return { removed }; | 305 | return { removed }; |
| 293 | }) | 306 | }), |
| 294 | ); | 307 | ); |
File renamed without changes.
| @@ -0,0 +1,14 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": ["src/**/*.ts"], | ||
| 13 | "exclude": ["src/**/*.test.ts", "src/runtime/execution/test-http-client.ts"] | ||
| 14 | } | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | import { defineConfig } from "vitest/config"; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | test: { | ||
| 5 | include: ["src/**/*.test.ts"], | ||
| 6 | testTimeout: 30_000, | ||
| 7 | }, | ||
| 8 | }); | ||
| @@ -12,8 +12,10 @@ · 11 unmodified lines | |||
| 12 | "@effect/opentelemetry": "^0.61.0", | 12 | "@effect/opentelemetry": "^0.61.0", |
| 13 | "@effect/platform": "catalog:", | 13 | "@effect/platform": "catalog:", |
| 14 | "@effect/platform-node": "catalog:", | 14 | "@effect/platform-node": "catalog:", |
| 15 | "@executor/control-plane": "workspace:*", | ||
| 16 | "@executor/executor-mcp": "workspace:*", | 15 | "@executor/executor-mcp": "workspace:*", |
| 16 | "@executor/platform-api": "workspace:*", | ||
| 17 | "@executor/platform-internal": "workspace:*", | ||
| 18 | "@executor/platform-sdk": "workspace:*", | ||
| 17 | "@opentelemetry/api": "^1.9.0", | 19 | "@opentelemetry/api": "^1.9.0", |
| 18 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", | 20 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", |
| 19 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", | 21 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", |
| @@ -10,12 +10,15 @@ import { FileSystem, HttpApiBuilder, HttpServer } from "@effect/platform"; · 9 unmodified lines | |||
| 10 | import { NodeFileSystem } from "@effect/platform-node"; | 10 | import { NodeFileSystem } from "@effect/platform-node"; |
| 11 | import { | 11 | import { |
| 12 | createControlPlaneApiLayer, | 12 | createControlPlaneApiLayer, |
| 13 | } from "@executor/platform-api"; | ||
| 14 | import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; | ||
| 15 | import { createExecutorAdminToolMap } from "@executor/platform-internal"; | ||
| 16 | import { | ||
| 13 | createControlPlaneRuntime, | 17 | createControlPlaneRuntime, |
| 14 | type ResolveExecutionEnvironment, | 18 | type ResolveExecutionEnvironment, |
| 15 | type ResolveSecretMaterial, | 19 | type ResolveSecretMaterial, |
| 16 | type ControlPlaneRuntime, | 20 | type ControlPlaneRuntime, |
| 17 | } from "@executor/control-plane"; | 21 | } from "@executor/platform-sdk/runtime"; |
| 18 | import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; | ||
| 19 | import * as Effect from "effect/Effect"; | 22 | import * as Effect from "effect/Effect"; |
| 20 | import * as Layer from "effect/Layer"; | 23 | import * as Layer from "effect/Layer"; |
| 21 | import type * as Scope from "effect/Scope"; | 24 | import type * as Scope from "effect/Scope"; |
| @@ -110,6 +113,7 @@ const createRuntime = ( · 88 unmodified lines | |||
| 110 | createControlPlaneRuntime({ | 113 | createControlPlaneRuntime({ |
| 111 | workspaceRoot: options.workspaceRoot, | 114 | workspaceRoot: options.workspaceRoot, |
| 112 | executionResolver: options.executionResolver, | 115 | executionResolver: options.executionResolver, |
| 116 | createInternalToolMap: createExecutorAdminToolMap, | ||
| 113 | resolveSecretMaterial: options.resolveSecretMaterial, | 117 | resolveSecretMaterial: options.resolveSecretMaterial, |
| 114 | getLocalServerBaseUrl, | 118 | getLocalServerBaseUrl, |
| 115 | localDataDir, | 119 | localDataDir, |
| @@ -20,7 +20,7 @@ const readFixture = (name: string) => · 19 unmodified lines | |||
| 20 | Effect.flatMap((fs) => | 20 | Effect.flatMap((fs) => |
| 21 | fs.readFileString( | 21 | fs.readFileString( |
| 22 | fileURLToPath( | 22 | fileURLToPath( |
| 23 | new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url), | 23 | new URL(`../../../platform/sdk/src/runtime/fixtures/${name}`, import.meta.url), |
| 24 | ), | 24 | ), |
| 25 | "utf8", | 25 | "utf8", |
| 26 | ) | 26 | ) |
| @@ -15,7 +15,7 @@ const readFixture = (name: string) => · 14 unmodified lines | |||
| 15 | Effect.flatMap((fs) => | 15 | Effect.flatMap((fs) => |
| 16 | fs.readFileString( | 16 | fs.readFileString( |
| 17 | fileURLToPath( | 17 | fileURLToPath( |
| 18 | new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url), | 18 | new URL(`../../../platform/sdk/src/runtime/fixtures/${name}`, import.meta.url), |
| 19 | ), | 19 | ), |
| 20 | "utf8", | 20 | "utf8", |
| 21 | ) | 21 | ) |