Unified · Split
| @@ -1,25 +1,9 @@ | ||
| 1 | 1 | { |
| 2 | − "sources": { | |
| 3 | − "vercel": { | |
| 4 | − "name": "Vercel API", | |
| 5 | − "connection": { | |
| 6 | − "endpoint": "https://api.vercel.com/" | |
| 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 | − } | |
| 2 | + "policies": { | |
| 3 | + "demo.*": { | |
| 4 | + "match": "demo.*", | |
| 5 | + "action": "allow", | |
| 6 | + "approval": "auto" | |
| 23 | 7 | } |
| 24 | 8 | } |
| 25 | 9 | } |
| @@ -88,7 +88,7 @@ Responsibilities: · 87 unmodified lines | ||
| 88 | 88 | |
| 89 | 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 | 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 | 408 | - `README.md` for the product view and usage guidance |
| 409 | 409 | - `apps/executor/src/cli/main.ts` for the CLI surface |
| 410 | 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 | 329 | - `apps/executor`: packaged CLI entrypoint and daemon lifecycle commands |
| 330 | 330 | - `apps/web`: local React web UI |
| 331 | 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 | 335 | - `packages/kernel/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution |
| 334 | 336 | - `packages/kernel/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution |
| 335 | 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 | 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 | 352 | - Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`. |
| 351 | 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 | 356 | ## Project status |
| 355 | 357 | |
| @@ -41,7 +41,7 @@ At a high level, the CLI: · 40 unmodified lines | ||
| 41 | 41 | |
| 42 | 42 | 1. ensures the local daemon is available |
| 43 | 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 | 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 | 41 | - mount the local MCP server at `/mcp` |
| 42 | 42 | - serve the web UI |
| 43 | 43 | |
| 44 | −### `packages/platform/control-plane` | |
| 44 | +### `packages/platform/sdk` | |
| 45 | 45 | |
| 46 | 46 | The product core. |
| 47 | 47 | |
| @@ -37,8 +37,10 @@ · 36 unmodified lines | ||
| 37 | 37 | "@effect/platform": "catalog:", |
| 38 | 38 | "@effect/platform-node": "catalog:", |
| 39 | 39 | "@executor/codemode-core": "workspace:*", |
| 40 | − "@executor/control-plane": "workspace:*", | |
| 41 | 40 | "@executor/executor-mcp": "workspace:*", |
| 41 | + "@executor/platform-api": "workspace:*", | |
| 42 | + "@executor/platform-internal": "workspace:*", | |
| 43 | + "@executor/platform-sdk": "workspace:*", | |
| 42 | 44 | "@executor/server": "workspace:*", |
| 43 | 45 | "effect": "catalog:" |
| 44 | 46 | }, |
| @@ -1,7 +1,5 @@ | ||
| 1 | −import { | |
| 2 | − type ControlPlaneClient, | |
| 3 | − WorkspaceIdSchema, | |
| 4 | −} from "@executor/control-plane"; | |
| 1 | +import type { ControlPlaneClient } from "@executor/platform-api"; | |
| 2 | +import { WorkspaceIdSchema } from "@executor/platform-sdk/schema"; | |
| 5 | 3 | import * as Effect from "effect/Effect"; |
| 6 | 4 | |
| 7 | 5 | const readBindingString = (binding: Record<string, unknown>, key: string): string | null => |
| @@ -9,17 +9,22 @@ import { · 8 unmodified lines | ||
| 9 | 9 | NodePath, |
| 10 | 10 | NodeRuntime, |
| 11 | 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 | 17 | import { |
| 13 | 18 | EXECUTOR_SOURCES_ADD_HELP_LINES, |
| 14 | − ExecutionIdSchema, | |
| 15 | 19 | RuntimeExecutionResolverService, |
| 16 | − createControlPlaneClient, | |
| 17 | 20 | createControlPlaneRuntime, |
| 18 | − type ControlPlaneClient, | |
| 21 | + type ControlPlaneRuntime, | |
| 22 | +} from "@executor/platform-sdk/runtime"; | |
| 23 | +import { | |
| 24 | + ExecutionIdSchema, | |
| 19 | 25 | type ExecutionEnvelope, |
| 20 | 26 | type ExecutionInteraction, |
| 21 | − type ControlPlaneRuntime, | |
| 22 | −} from "@executor/control-plane"; | |
| 27 | +} from "@executor/platform-sdk/schema"; | |
| 23 | 28 | import type { ToolCatalog } from "@executor/codemode-core"; |
| 24 | 29 | |
| 25 | 30 | import * as Effect from "effect/Effect"; |
| @@ -272,6 +277,7 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 246 unmodified lines | ||
| 272 | 277 | Effect.acquireUseRelease( |
| 273 | 278 | createControlPlaneRuntime({ |
| 274 | 279 | localDataDir: DEFAULT_LOCAL_DATA_DIR, |
| 280 | + createInternalToolMap: createExecutorAdminToolMap, | |
| 275 | 281 | }).pipe(Effect.mapError(toError)), |
| 276 | 282 | (runtime) => |
| 277 | 283 | Effect.gen(function* () { |
| @@ -1,5 +1,5 @@ | ||
| 1 | 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 | 4 | import { |
| 5 | 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 | 3 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 4 | 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 | 6 | import { |
| 7 | 7 | createControlPlaneClient, |
| 8 | − type SourceDiscoveryKind, | |
| 9 | −} from "@executor/control-plane"; | |
| 8 | +} from "@executor/platform-api"; | |
| 9 | +import { type SourceDiscoveryKind } from "@executor/platform-sdk/schema"; | |
| 10 | 10 | import { createLocalExecutorServer } from "@executor/server"; |
| 11 | 11 | |
| 12 | 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 | 20 | import { |
| 21 | 21 | createControlPlaneClient, |
| 22 | 22 | controlPlaneOpenApiSpec, |
| 23 | +} from "@executor/platform-api"; | |
| 24 | +import { | |
| 23 | 25 | buildLocalSourceArtifact, |
| 24 | 26 | catalogSyncResultFromMcpManifest, |
| 25 | 27 | deriveLocalInstallation, |
| 26 | − type ResolveExecutionEnvironment, | |
| 27 | 28 | resolveLocalWorkspaceContext, |
| 28 | − SourceIdSchema, | |
| 29 | 29 | writeLocalSourceArtifact, |
| 30 | 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 | 34 | import { makeSesExecutor } from "@executor/runtime-ses"; |
| 33 | 35 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 34 | 36 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| @@ -19,7 +19,7 @@ · 18 unmodified lines | ||
| 19 | 19 | }, |
| 20 | 20 | "apps/executor": { |
| 21 | 21 | "name": "executor", |
| 22 | − "version": "1.2.3", | |
| 22 | + "version": "1.2.4-beta.4", | |
| 23 | 23 | "bin": { |
| 24 | 24 | "executor": "bin/executor", |
| 25 | 25 | }, |
| @@ -28,8 +28,10 @@ · 2 unmodified lines | ||
| 28 | 28 | "@effect/platform": "catalog:", |
| 29 | 29 | "@effect/platform-node": "catalog:", |
| 30 | 30 | "@executor/codemode-core": "workspace:*", |
| 31 | − "@executor/control-plane": "workspace:*", | |
| 32 | 31 | "@executor/executor-mcp": "workspace:*", |
| 32 | + "@executor/platform-api": "workspace:*", | |
| 33 | + "@executor/platform-internal": "workspace:*", | |
| 34 | + "@executor/platform-sdk": "workspace:*", | |
| 33 | 35 | "@executor/server": "workspace:*", |
| 34 | 36 | "effect": "catalog:", |
| 35 | 37 | }, |
| @@ -129,7 +131,8 @@ · 93 unmodified lines | ||
| 129 | 131 | "dependencies": { |
| 130 | 132 | "@effect-atom/atom": "0.5.3", |
| 131 | 133 | "@effect-atom/atom-react": "0.5.0", |
| 132 | − "@executor/control-plane": "workspace:*", | |
| 134 | + "@executor/platform-api": "workspace:*", | |
| 135 | + "@executor/platform-sdk": "workspace:*", | |
| 133 | 136 | "effect": "catalog:", |
| 134 | 137 | }, |
| 135 | 138 | "devDependencies": { |
| @@ -156,7 +159,7 @@ · 20 unmodified lines | ||
| 156 | 159 | "@effect/platform": "catalog:", |
| 157 | 160 | "@executor/ai-sdk-adapter": "workspace:*", |
| 158 | 161 | "@executor/codemode-core": "workspace:*", |
| 159 | − "@executor/control-plane": "workspace:*", | |
| 162 | + "@executor/platform-sdk": "workspace:*", | |
| 160 | 163 | "@executor/runtime-ses": "workspace:*", |
| 161 | 164 | "@executor/source-openapi": "workspace:*", |
| 162 | 165 | "effect": "catalog:", |
| @@ -187,7 +190,7 @@ · 24 unmodified lines | ||
| 187 | 190 | "name": "@executor/executor-mcp", |
| 188 | 191 | "version": "0.0.0", |
| 189 | 192 | "dependencies": { |
| 190 | − "@executor/control-plane": "workspace:*", | |
| 193 | + "@executor/platform-sdk": "workspace:*", | |
| 191 | 194 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 192 | 195 | "effect": "catalog:", |
| 193 | 196 | "zod": "catalog:", |
| @@ -270,11 +273,41 @@ · 76 unmodified lines | ||
| 270 | 273 | "bun-types": "catalog:", |
| 271 | 274 | }, |
| 272 | 275 | }, |
| 273 | − "packages/platform/control-plane": { | |
| 274 | − "name": "@executor/control-plane", | |
| 276 | + "packages/platform/api": { | |
| 277 | + "name": "@executor/platform-api", | |
| 275 | 278 | "version": "0.0.0", |
| 276 | 279 | "dependencies": { |
| 277 | 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 | 311 | "@executor/auth-mcp-oauth": "workspace:*", |
| 279 | 312 | "@executor/auth-oauth2": "workspace:*", |
| 280 | 313 | "@executor/codemode-core": "workspace:*", |
| @@ -289,20 +322,20 @@ · 8 unmodified lines | ||
| 289 | 322 | "@executor/source-mcp": "workspace:*", |
| 290 | 323 | "@executor/source-openapi": "workspace:*", |
| 291 | 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 | 326 | "devDependencies": { |
| 299 | − "@effect/platform-node": "catalog:", | |
| 300 | 327 | "@effect/vitest": "catalog:", |
| 328 | + "@executor/platform-api": "workspace:*", | |
| 329 | + "@executor/platform-internal": "workspace:*", | |
| 301 | 330 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 302 | 331 | "@types/node": "catalog:", |
| 303 | 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 | 338 | "vitest": "catalog:", |
| 305 | − "zod": "catalog:", | |
| 306 | 339 | }, |
| 307 | 340 | }, |
| 308 | 341 | "packages/platform/server": { |
| @@ -312,8 +345,10 @@ · 3 unmodified lines | ||
| 312 | 345 | "@effect/opentelemetry": "^0.61.0", |
| 313 | 346 | "@effect/platform": "catalog:", |
| 314 | 347 | "@effect/platform-node": "catalog:", |
| 315 | − "@executor/control-plane": "workspace:*", | |
| 316 | 348 | "@executor/executor-mcp": "workspace:*", |
| 349 | + "@executor/platform-api": "workspace:*", | |
| 350 | + "@executor/platform-internal": "workspace:*", | |
| 351 | + "@executor/platform-sdk": "workspace:*", | |
| 317 | 352 | "@opentelemetry/api": "^1.9.0", |
| 318 | 353 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", |
| 319 | 354 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", |
| @@ -641,8 +676,6 @@ · 321 unmodified lines | ||
| 641 | 676 | |
| 642 | 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 | 679 | "@executor/docs": ["@executor/docs@workspace:apps/docs"], |
| 647 | 680 | |
| 648 | 681 | "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/hosts/mcp"], |
| @@ -653,6 +686,12 @@ · 4 unmodified lines | ||
| 653 | 686 | |
| 654 | 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 | 695 | "@executor/react": ["@executor/react@workspace:packages/clients/react"], |
| 657 | 696 | |
| 658 | 697 | "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/kernel/runtime-deno-subprocess"], |
| @@ -35,7 +35,7 @@ · 34 unmodified lines | ||
| 35 | 35 | "release:pre:exit": "changeset pre exit", |
| 36 | 36 | "release:beta:start": "changeset pre enter beta", |
| 37 | 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 | 39 | "typecheck": "bun run --filter='*' typecheck", |
| 40 | 40 | "knip": "knip" |
| 41 | 41 | }, |
| @@ -13,7 +13,8 @@ · 12 unmodified lines | ||
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@effect-atom/atom": "0.5.3", |
| 15 | 15 | "@effect-atom/atom-react": "0.5.0", |
| 16 | − "@executor/control-plane": "workspace:*", | |
| 16 | + "@executor/platform-api": "workspace:*", | |
| 17 | + "@executor/platform-sdk": "workspace:*", | |
| 17 | 18 | "effect": "catalog:" |
| 18 | 19 | }, |
| 19 | 20 | "peerDependencies": { |
| @@ -5,11 +5,15 @@ import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node"; · 4 unmodified lines | ||
| 5 | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | 6 | import { |
| 7 | 7 | createControlPlaneApiLayer, |
| 8 | +} from "@executor/platform-api"; | |
| 9 | +import { | |
| 8 | 10 | createControlPlaneRuntime, |
| 9 | − type LocalInstallation, | |
| 10 | − type Source, | |
| 11 | 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 | 17 | import * as Context from "effect/Context"; |
| 14 | 18 | import * as Effect from "effect/Effect"; |
| 15 | 19 | import * as Exit from "effect/Exit"; |
| @@ -1,35 +1,37 @@ | ||
| 1 | 1 | import { Atom, Result } from "@effect-atom/atom"; |
| 2 | 2 | import type * as Registry from "@effect-atom/atom/Registry"; |
| 3 | 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 | 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 | 28 | Source, |
| 22 | 29 | SourceDiscoveryResult, |
| 23 | 30 | SourceInspection, |
| 24 | 31 | SourceInspectionDiscoverResult, |
| 25 | 32 | SourceInspectionToolDetail, |
| 26 | − StartSourceOAuthPayload, | |
| 27 | − StartSourceOAuthResult, | |
| 28 | − UpdateSecretPayload, | |
| 29 | − UpdateSecretResult, | |
| 30 | − UpdateSourcePayload, | |
| 31 | 33 | WorkspaceOauthClient, |
| 32 | −} from "@executor/control-plane"; | |
| 34 | +} from "@executor/platform-sdk/schema"; | |
| 33 | 35 | import * as Cause from "effect/Cause"; |
| 34 | 36 | import * as Effect from "effect/Effect"; |
| 35 | 37 | import * as Exit from "effect/Exit"; |
| @@ -12,9 +12,9 @@ · 11 unmodified lines | ||
| 12 | 12 | "@effect/platform": "catalog:", |
| 13 | 13 | "@executor/ai-sdk-adapter": "workspace:*", |
| 14 | 14 | "@executor/codemode-core": "workspace:*", |
| 15 | − "@executor/source-openapi": "workspace:*", | |
| 16 | − "@executor/control-plane": "workspace:*", | |
| 15 | + "@executor/platform-sdk": "workspace:*", | |
| 17 | 16 | "@executor/runtime-ses": "workspace:*", |
| 17 | + "@executor/source-openapi": "workspace:*", | |
| 18 | 18 | "effect": "catalog:" |
| 19 | 19 | }, |
| 20 | 20 | "devDependencies": { |
| @@ -17,7 +17,7 @@ import { · 16 unmodified lines | ||
| 17 | 17 | type Source, |
| 18 | 18 | type SourceId, |
| 19 | 19 | type WorkspaceId, |
| 20 | −} from "@executor/control-plane"; | |
| 20 | +} from "@executor/platform-sdk/schema"; | |
| 21 | 21 | import { |
| 22 | 22 | createToolCatalogDiscovery, |
| 23 | 23 | createToolCatalogFromTools, |
| @@ -11,7 +11,7 @@ · 10 unmodified lines | ||
| 11 | 11 | "test:watch": "vitest" |
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | − "@executor/control-plane": "workspace:*", | |
| 14 | + "@executor/platform-sdk": "workspace:*", | |
| 15 | 15 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 16 | 16 | "effect": "catalog:", |
| 17 | 17 | "zod": "catalog:" |
| @@ -1,15 +1,15 @@ | ||
| 1 | 1 | import type { |
| 2 | 2 | ExecutionEnvelope, |
| 3 | − ControlPlaneRuntime, | |
| 4 | −} from "@executor/control-plane"; | |
| 3 | +} from "@executor/platform-sdk/schema"; | |
| 5 | 4 | import { |
| 6 | 5 | EXECUTOR_SOURCES_ADD_HELP_LINES, |
| 7 | − ExecutionIdSchema, | |
| 8 | 6 | RuntimeExecutionResolverService, |
| 9 | 7 | createExecution, |
| 10 | 8 | getExecution, |
| 11 | 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 | 13 | import * as Effect from "effect/Effect"; |
| 14 | 14 | import * as Cause from "effect/Cause"; |
| 15 | 15 | import * as Exit from "effect/Exit"; |
| @@ -1,5 +1,8 @@ | ||
| 1 | 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 | 7 | import { |
| 5 | 8 | buildPausedResultText, |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import type { |
| 2 | 2 | ExecutionEnvelope, |
| 3 | 3 | ExecutionInteraction, |
| 4 | −} from "@executor/control-plane"; | |
| 4 | +} from "@executor/platform-sdk/schema"; | |
| 5 | 5 | |
| 6 | 6 | export type ParsedInteractionPayload = { |
| 7 | 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 | 4 | } from "@effect/platform"; |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | |
| 7 | −import { ControlPlaneApi } from "./api/api"; | |
| 7 | +import { ControlPlaneApi } from "./api"; | |
| 8 | 8 | |
| 9 | 9 | export const createControlPlaneClient = (input: { |
| 10 | 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 | 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 | 10 | import { |
| 3 | 11 | ExecutionIdSchema, |
| 4 | 12 | ExecutionEnvelopeSchema, |
| 5 | 13 | WorkspaceIdSchema, |
| 6 | −} from "#schema"; | |
| 7 | −import * as Schema from "effect/Schema"; | |
| 14 | +} from "@executor/platform-sdk/schema"; | |
| 8 | 15 | |
| 9 | 16 | import { |
| 10 | 17 | ControlPlaneBadRequestError, |
| @@ -12,22 +19,12 @@ import { · 1 unmodified line | ||
| 12 | 19 | ControlPlaneNotFoundError, |
| 13 | 20 | ControlPlaneStorageError, |
| 14 | 21 | ControlPlaneUnauthorizedError, |
| 15 | −} from "../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 | −}); | |
| 22 | +} from "@executor/platform-sdk/errors"; | |
| 29 | 23 | |
| 30 | −export type ResumeExecutionPayload = typeof ResumeExecutionPayloadSchema.Type; | |
| 24 | +export { | |
| 25 | + CreateExecutionPayloadSchema, | |
| 26 | + ResumeExecutionPayloadSchema, | |
| 27 | +}; | |
| 31 | 28 | |
| 32 | 29 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 33 | 30 | const executionIdParam = HttpApiSchema.param("executionId", ExecutionIdSchema); |
| @@ -4,7 +4,7 @@ import { · 3 unmodified lines | ||
| 4 | 4 | createExecution, |
| 5 | 5 | getExecution, |
| 6 | 6 | resumeExecution, |
| 7 | −} from "../../runtime/execution/service"; | |
| 7 | +} from "@executor/platform-sdk/runtime"; | |
| 8 | 8 | |
| 9 | 9 | import { ControlPlaneApi } from "../api"; |
| 10 | 10 | import { resolveRequestedLocalWorkspace } from "../local-context"; |
File renamed without changes.
| @@ -2,8 +2,12 @@ export { · 1 unmodified line | ||
| 2 | 2 | ControlPlaneApi, |
| 3 | 3 | controlPlaneOpenApiSpec, |
| 4 | 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 | 12 | export { |
| 9 | 13 | ControlPlaneBadRequestError, |
| @@ -22,10 +26,12 @@ export { · 12 unmodified lines | ||
| 22 | 26 | |
| 23 | 27 | export { |
| 24 | 28 | CreateExecutionPayloadSchema, |
| 29 | + ExecutionsApi, | |
| 25 | 30 | ResumeExecutionPayloadSchema, |
| 26 | 31 | type CreateExecutionPayload, |
| 27 | 32 | type ResumeExecutionPayload, |
| 28 | 33 | } from "./executions/api"; |
| 34 | +export { ControlPlaneExecutionsLive } from "./executions/http"; | |
| 29 | 35 | |
| 30 | 36 | export { |
| 31 | 37 | LocalApi, |
| @@ -38,16 +44,22 @@ export { · 6 unmodified lines | ||
| 38 | 44 | type UpdateSecretResult, |
| 39 | 45 | type DeleteSecretResult, |
| 40 | 46 | } from "./local/api"; |
| 47 | +export { ControlPlaneLocalLive } from "./local/http"; | |
| 41 | 48 | |
| 42 | 49 | export { |
| 43 | 50 | OAuthApi, |
| 44 | 51 | StartSourceOAuthPayloadSchema, |
| 45 | 52 | StartSourceOAuthResultSchema, |
| 46 | 53 | CompleteSourceOAuthResultSchema, |
| 54 | + SourceOAuthPopupFailureResultSchema, | |
| 55 | + SourceOAuthPopupResultSchema, | |
| 56 | + SourceOAuthPopupSuccessResultSchema, | |
| 47 | 57 | type StartSourceOAuthPayload, |
| 48 | 58 | type StartSourceOAuthResult, |
| 49 | 59 | type CompleteSourceOAuthResult, |
| 60 | + type SourceOAuthPopupResult, | |
| 50 | 61 | } from "./oauth/api"; |
| 62 | +export { ControlPlaneOAuthLive } from "./oauth/http"; | |
| 51 | 63 | |
| 52 | 64 | export { |
| 53 | 65 | ConnectSourceBatchPayloadSchema, |
| @@ -57,6 +69,7 @@ export { · 3 unmodified lines | ||
| 57 | 69 | CreateWorkspaceOauthClientPayloadSchema, |
| 58 | 70 | CreateSourcePayloadSchema, |
| 59 | 71 | DiscoverSourcePayloadSchema, |
| 72 | + SourcesApi, | |
| 60 | 73 | UpdateSourcePayloadSchema, |
| 61 | 74 | type ConnectSourceBatchPayload, |
| 62 | 75 | type ConnectSourceBatchResult, |
| @@ -67,10 +80,15 @@ export { · 4 unmodified lines | ||
| 67 | 80 | type DiscoverSourcePayload, |
| 68 | 81 | type UpdateSourcePayload, |
| 69 | 82 | } from "./sources/api"; |
| 83 | +export { ControlPlaneSourcesLive } from "./sources/http"; | |
| 70 | 84 | |
| 71 | 85 | export { |
| 72 | 86 | CreatePolicyPayloadSchema, |
| 87 | + PoliciesApi, | |
| 73 | 88 | UpdatePolicyPayloadSchema, |
| 74 | 89 | type CreatePolicyPayload, |
| 75 | 90 | type UpdatePolicyPayload, |
| 76 | 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 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | |
| 4 | 4 | import { |
| 5 | 5 | RuntimeLocalWorkspaceMismatchError, |
| 6 | −} from "../runtime/local/errors"; | |
| 7 | −import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context"; | |
| 6 | + requireRuntimeLocalWorkspace, | |
| 7 | +} from "@executor/platform-sdk/runtime"; | |
| 8 | 8 | import { ControlPlaneForbiddenError } from "./errors"; |
| 9 | 9 | |
| 10 | 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 | 5 | SourceTransportSchema, |
| 6 | 6 | StringMapSchema, |
| 7 | 7 | WorkspaceIdSchema, |
| 8 | −} from "#schema"; | |
| 8 | +} from "@executor/platform-sdk/schema"; | |
| 9 | 9 | import * as Schema from "effect/Schema"; |
| 10 | 10 | |
| 11 | 11 | import { |
| @@ -13,8 +13,8 @@ import { · 1 unmodified line | ||
| 13 | 13 | ControlPlaneForbiddenError, |
| 14 | 14 | ControlPlaneStorageError, |
| 15 | 15 | ControlPlaneUnauthorizedError, |
| 16 | −} from "../errors"; | |
| 17 | −import { TrimmedNonEmptyStringSchema } from "../string-schemas"; | |
| 16 | +} from "@executor/platform-sdk/errors"; | |
| 17 | +import { TrimmedNonEmptyStringSchema } from "@executor/platform-sdk/string-schemas"; | |
| 18 | 18 | |
| 19 | 19 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 20 | 20 | |
| @@ -3,13 +3,13 @@ import { · 2 unmodified lines | ||
| 3 | 3 | HttpServerRequest, |
| 4 | 4 | HttpServerResponse, |
| 5 | 5 | } from "@effect/platform"; |
| 6 | −import type { WorkspaceId } from "#schema"; | |
| 6 | +import type { WorkspaceId } from "@executor/platform-sdk/schema"; | |
| 7 | 7 | import * as Cause from "effect/Cause"; |
| 8 | 8 | import * as Either from "effect/Either"; |
| 9 | 9 | import * as Effect from "effect/Effect"; |
| 10 | 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 | 13 | import { ControlPlaneApi } from "../api"; |
| 14 | 14 | import { |
| 15 | 15 | ControlPlaneBadRequestError, |
File renamed without changes.
| @@ -1,37 +1,31 @@ | ||
| 1 | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | 2 | import { |
| 3 | − LocalWorkspacePolicyApprovalModeSchema, | |
| 4 | − LocalWorkspacePolicyEffectSchema, | |
| 5 | − LocalWorkspacePolicySchema, | |
| 6 | − PolicyIdSchema, | |
| 7 | − WorkspaceIdSchema, | |
| 8 | −} from "#schema"; | |
| 9 | −import * as Schema from "effect/Schema"; | |
| 10 | − | |
| 3 | + CreatePolicyPayloadSchema, | |
| 4 | + UpdatePolicyPayloadSchema, | |
| 5 | +} from "@executor/platform-sdk/policies/contracts"; | |
| 11 | 6 | import { |
| 12 | 7 | ControlPlaneBadRequestError, |
| 13 | 8 | ControlPlaneForbiddenError, |
| 14 | 9 | ControlPlaneNotFoundError, |
| 15 | 10 | ControlPlaneStorageError, |
| 16 | 11 | ControlPlaneUnauthorizedError, |
| 17 | −} from "../errors"; | |
| 18 | −import { OptionalTrimmedNonEmptyStringSchema } from "../string-schemas"; | |
| 19 | − | |
| 20 | −const LocalWorkspacePolicyPayloadSchema = Schema.Struct({ | |
| 21 | − resourcePattern: OptionalTrimmedNonEmptyStringSchema, | |
| 22 | − effect: Schema.optional(LocalWorkspacePolicyEffectSchema), | |
| 23 | − approvalMode: Schema.optional(LocalWorkspacePolicyApprovalModeSchema), | |
| 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; | |
| 12 | +} from "@executor/platform-sdk/errors"; | |
| 13 | +import { | |
| 14 | + LocalWorkspacePolicySchema, | |
| 15 | + PolicyIdSchema, | |
| 16 | + WorkspaceIdSchema, | |
| 17 | +} from "@executor/platform-sdk/schema"; | |
| 18 | +import * as Schema from "effect/Schema"; | |
| 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 | 30 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 37 | 31 | const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema); |
| @@ -6,7 +6,7 @@ import { · 5 unmodified lines | ||
| 6 | 6 | listPolicies, |
| 7 | 7 | removePolicy, |
| 8 | 8 | updatePolicy, |
| 9 | −} from "../../runtime/policy/policies-operations"; | |
| 9 | +} from "@executor/platform-sdk"; | |
| 10 | 10 | |
| 11 | 11 | import { ControlPlaneApi } from "../api"; |
| 12 | 12 | import { resolveRequestedLocalWorkspace } from "../local-context"; |
| @@ -18,13 +18,14 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 5 unmodified lines | ||
| 18 | 18 | handlers |
| 19 | 19 | .handle("list", ({ path }) => |
| 20 | 20 | resolveRequestedLocalWorkspace("policies.list", path.workspaceId).pipe( |
| 21 | − Effect.zipRight( | |
| 22 | − listPolicies(path.workspaceId), | |
| 23 | − ), | |
| 21 | + Effect.zipRight(listPolicies(path.workspaceId)), | |
| 24 | 22 | ), |
| 25 | 23 | ) |
| 26 | 24 | .handle("create", ({ path, payload }) => |
| 27 | − resolveRequestedLocalWorkspace("policies.create", path.workspaceId).pipe( | |
| 25 | + resolveRequestedLocalWorkspace( | |
| 26 | + "policies.create", | |
| 27 | + path.workspaceId, | |
| 28 | + ).pipe( | |
| 28 | 29 | Effect.zipRight( |
| 29 | 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 | 44 | .handle("update", ({ path, payload }) => |
| 44 | − resolveRequestedLocalWorkspace("policies.update", path.workspaceId).pipe( | |
| 45 | + resolveRequestedLocalWorkspace( | |
| 46 | + "policies.update", | |
| 47 | + path.workspaceId, | |
| 48 | + ).pipe( | |
| 45 | 49 | Effect.zipRight( |
| 46 | 50 | updatePolicy({ |
| 47 | 51 | workspaceId: path.workspaceId, |
| @@ -52,7 +56,10 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 4 unmodified lines | ||
| 52 | 56 | ), |
| 53 | 57 | ) |
| 54 | 58 | .handle("remove", ({ path }) => |
| 55 | − resolveRequestedLocalWorkspace("policies.remove", path.workspaceId).pipe( | |
| 59 | + resolveRequestedLocalWorkspace( | |
| 60 | + "policies.remove", | |
| 61 | + path.workspaceId, | |
| 62 | + ).pipe( | |
| 56 | 63 | Effect.zipRight( |
| 57 | 64 | removePolicy({ |
| 58 | 65 | workspaceId: path.workspaceId, |
| @@ -1,189 +1,70 @@ | ||
| 1 | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | 2 | import { |
| 3 | − SourceOauthClientInputSchema, | |
| 4 | − ExecutionInteractionIdSchema, | |
| 5 | − JsonObjectSchema, | |
| 3 | + ConnectSourceBatchPayloadSchema, | |
| 4 | + ConnectSourceBatchResultSchema, | |
| 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 | 24 | ProviderAuthGrantIdSchema, |
| 7 | − SourceAuthSchema, | |
| 8 | − SourceAuthSessionIdSchema, | |
| 9 | 25 | SourceDiscoveryResultSchema, |
| 10 | 26 | SourceIdSchema, |
| 11 | 27 | SourceInspectionDiscoverPayloadSchema, |
| 12 | 28 | SourceInspectionDiscoverResultSchema, |
| 13 | 29 | SourceInspectionSchema, |
| 14 | 30 | SourceInspectionToolDetailSchema, |
| 15 | − SourceImportAuthPolicySchema, | |
| 16 | − SourceKindSchema, | |
| 17 | − SourceProbeAuthSchema, | |
| 18 | 31 | SourceSchema, |
| 19 | − SourceStatusSchema, | |
| 20 | 32 | WorkspaceOauthClientIdSchema, |
| 21 | 33 | WorkspaceOauthClientSchema, |
| 22 | 34 | WorkspaceIdSchema, |
| 23 | −} from "#schema"; | |
| 35 | +} from "@executor/platform-sdk/schema"; | |
| 24 | 36 | import * as Schema from "effect/Schema"; |
| 25 | 37 | |
| 26 | −import { | |
| 27 | − ControlPlaneBadRequestError, | |
| 28 | − ControlPlaneForbiddenError, | |
| 29 | − ControlPlaneNotFoundError, | |
| 30 | − ControlPlaneStorageError, | |
| 31 | − ControlPlaneUnauthorizedError, | |
| 32 | −} from "../errors"; | |
| 33 | −import { | |
| 34 | − OptionalTrimmedNonEmptyStringSchema, | |
| 35 | − TrimmedNonEmptyStringSchema, | |
| 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; | |
| 38 | +export type { | |
| 39 | + ConnectSourceBatchPayload, | |
| 40 | + ConnectSourceBatchResult, | |
| 41 | + ConnectSourcePayload, | |
| 42 | + ConnectSourceResult, | |
| 43 | + CreateSourcePayload, | |
| 44 | + CreateWorkspaceOauthClientPayload, | |
| 45 | + DiscoverSourcePayload, | |
| 46 | + UpdateSourcePayload, | |
| 47 | +} from "@executor/platform-sdk/sources/contracts"; | |
| 176 | 48 | |
| 177 | 49 | export { |
| 178 | − ConnectSourcePayloadSchema, | |
| 179 | 50 | ConnectSourceBatchPayloadSchema, |
| 180 | 51 | ConnectSourceBatchResultSchema, |
| 52 | + ConnectSourcePayloadSchema, | |
| 181 | 53 | ConnectSourceResultSchema, |
| 54 | + CreateSourcePayloadSchema, | |
| 182 | 55 | CreateWorkspaceOauthClientPayloadSchema, |
| 183 | 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 | 69 | const HtmlSchema = HttpApiSchema.Text({ |
| 189 | 70 | contentType: "text/html", |
| @@ -3,32 +3,32 @@ import { · 2 unmodified lines | ||
| 3 | 3 | HttpServerRequest, |
| 4 | 4 | HttpServerResponse, |
| 5 | 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 | 11 | import * as Effect from "effect/Effect"; |
| 8 | 12 | |
| 9 | 13 | import { |
| 14 | + completeSourceCredentialSetup, | |
| 10 | 15 | createSource, |
| 11 | − getSource, | |
| 12 | − listSources, | |
| 13 | − removeSource, | |
| 14 | − updateSource, | |
| 15 | −} from "../../runtime/sources/sources-operations"; | |
| 16 | −import { | |
| 16 | + discoverSource, | |
| 17 | 17 | discoverSourceInspectionTools, |
| 18 | + getSource, | |
| 19 | + getSourceCredentialInteraction, | |
| 18 | 20 | getSourceInspection, |
| 19 | 21 | getSourceInspectionToolDetail, |
| 20 | −} from "../../runtime/sources/source-inspection"; | |
| 21 | −import { | |
| 22 | − completeSourceCredentialSetup, | |
| 23 | − getSourceCredentialInteraction, | |
| 22 | + listSources, | |
| 23 | + removeSource, | |
| 24 | 24 | submitSourceCredentialInteraction, |
| 25 | −} from "../../runtime/local/operations"; | |
| 26 | −import { discoverSource } from "../../runtime/sources/source-discovery"; | |
| 27 | −import { sourceAdapterRequiresInteractiveConnect } from "../../runtime/sources/source-adapters"; | |
| 25 | + updateSource, | |
| 26 | +} from "@executor/platform-sdk"; | |
| 28 | 27 | import { |
| 28 | + sourceAdapterRequiresInteractiveConnect, | |
| 29 | 29 | RuntimeSourceAuthServiceTag, |
| 30 | 30 | type ExecutorAddSourceInput, |
| 31 | −} from "../../runtime/sources/source-auth-service"; | |
| 31 | +} from "@executor/platform-sdk/runtime"; | |
| 32 | 32 | |
| 33 | 33 | import { |
| 34 | 34 | ControlPlaneBadRequestError, |
| @@ -58,7 +58,10 @@ const readHeader = (headers: unknown, name: string): string | null => { · 23 unmodified lines | ||
| 58 | 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 | 65 | try { |
| 63 | 66 | return new URL(request.url).origin; |
| 64 | 67 | } catch { |
| @@ -69,7 +72,8 @@ const resolveRequestOrigin = (request: { url: string; headers: unknown }): strin · 4 unmodified lines | ||
| 69 | 72 | } |
| 70 | 73 | |
| 71 | 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 | 77 | return `${protocol}://${host}`; |
| 74 | 78 | } |
| 75 | 79 | }; |
| @@ -86,7 +90,8 @@ const toBadRequestError = (operation: string, cause: unknown) => { · 10 unmodified lines | ||
| 86 | 90 | const isInteractiveConnectPayload = ( |
| 87 | 91 | payload: ConnectSourcePayload, |
| 88 | 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 | 96 | const escapeHtml = (value: string): string => |
| 92 | 97 | value |
| @@ -415,11 +420,7 @@ const credentialPageDocument = (input: { · 322 unmodified lines | ||
| 415 | 420 | |
| 416 | 421 | <hr /> |
| 417 | 422 | |
| 418 | − ${ | |
| 419 | − input.error | |
| 420 | − ? `<p class="alert">${escapeHtml(input.error)}</p>` | |
| 421 | − : "" | |
| 422 | − } | |
| 423 | + ${input.error ? `<p class="alert">${escapeHtml(input.error)}</p>` : ""} | |
| 423 | 424 | ${ |
| 424 | 425 | input.state === "stored" |
| 425 | 426 | ? `<div class="result-card success"> |
| @@ -431,17 +432,17 @@ const credentialPageDocument = (input: { · 5 unmodified lines | ||
| 431 | 432 | <p class="result-heading">Continuing without auth</p> |
| 432 | 433 | <p>Executor resumed the source connection without stored credentials. You can close this window.</p> |
| 433 | 434 | </div>` |
| 434 | − : input.state === "cancelled" | |
| 435 | − ? `<div class="result-card neutral"> | |
| 435 | + : input.state === "cancelled" | |
| 436 | + ? `<div class="result-card neutral"> | |
| 436 | 437 | <p class="result-heading">Request cancelled</p> |
| 437 | 438 | <p>Executor was notified. You can close this window.</p> |
| 438 | 439 | </div>` |
| 439 | − : input.state === "inactive" | |
| 440 | − ? `<div class="result-card neutral"> | |
| 440 | + : input.state === "inactive" | |
| 441 | + ? `<div class="result-card neutral"> | |
| 441 | 442 | <p class="result-heading">No longer active</p> |
| 442 | 443 | <p>This credential request has expired. Start a new source connection flow if needed.</p> |
| 443 | 444 | </div>` |
| 444 | − : `<form method="post"> | |
| 445 | + : `<form method="post"> | |
| 445 | 446 | <div class="field"> |
| 446 | 447 | <label for="token">Personal access token</label> |
| 447 | 448 | <input |
| @@ -474,8 +475,8 @@ const sourceOAuthPopupResultDocument = (input: { · 26 unmodified lines | ||
| 474 | 475 | sessionId?: string | null; |
| 475 | 476 | payload: |
| 476 | 477 | | { |
| 477 | − type: "executor:source-oauth-result"; | |
| 478 | − ok: true; | |
| 478 | + type: "executor:source-oauth-result"; | |
| 479 | + ok: true; | |
| 479 | 480 | sourceId: Source["id"]; |
| 480 | 481 | } |
| 481 | 482 | | { |
| @@ -595,9 +596,7 @@ const sourceOAuthPopupResultDocument = (input: { · 113 unmodified lines | ||
| 595 | 596 | </html>`; |
| 596 | 597 | |
| 597 | 598 | const htmlResponse = (html: string, status = 200) => |
| 598 | − HttpServerResponse.html(html).pipe( | |
| 599 | − HttpServerResponse.setStatus(status), | |
| 600 | − ); | |
| 599 | + HttpServerResponse.html(html).pipe(HttpServerResponse.setStatus(status)); | |
| 601 | 600 | |
| 602 | 601 | const credentialErrorResponse = (input: { |
| 603 | 602 | title: string; |
| @@ -619,7 +618,10 @@ const credentialErrorResponse = (input: { · 15 unmodified lines | ||
| 619 | 618 | ); |
| 620 | 619 | |
| 621 | 620 | const credentialErrorStatus = ( |
| 622 | − error: ControlPlaneBadRequestError | ControlPlaneNotFoundError | ControlPlaneStorageError, | |
| 621 | + error: | |
| 622 | + | ControlPlaneBadRequestError | |
| 623 | + | ControlPlaneNotFoundError | |
| 624 | + | ControlPlaneStorageError, | |
| 623 | 625 | ) => |
| 624 | 626 | error instanceof ControlPlaneNotFoundError |
| 625 | 627 | ? 404 |
| @@ -679,15 +681,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 53 unmodified lines | ||
| 679 | 681 | ), |
| 680 | 682 | ) |
| 681 | 683 | .handle("connect", ({ path, payload }) => |
| 682 | − resolveRequestedLocalWorkspace("sources.connect", path.workspaceId).pipe( | |
| 684 | + resolveRequestedLocalWorkspace( | |
| 685 | + "sources.connect", | |
| 686 | + path.workspaceId, | |
| 687 | + ).pipe( | |
| 683 | 688 | Effect.flatMap((runtimeLocalWorkspace) => |
| 684 | 689 | Effect.gen(function* () { |
| 685 | 690 | const request = yield* HttpServerRequest.HttpServerRequest; |
| 686 | 691 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 687 | 692 | const baseUrl = resolveRequestOrigin(request); |
| 688 | 693 | |
| 689 | − if (isInteractiveConnectPayload(payload)) { | |
| 690 | − return yield* sourceAuthService.connectMcpSource({ | |
| 694 | + if (isInteractiveConnectPayload(payload)) { | |
| 695 | + return yield* sourceAuthService.connectMcpSource({ | |
| 691 | 696 | workspaceId: path.workspaceId, |
| 692 | 697 | actorAccountId: runtimeLocalWorkspace.installation.accountId, |
| 693 | 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( | |
| 708 | − ({ | |
| 709 | − workspaceId: path.workspaceId, | |
| 710 | − actorAccountId: runtimeLocalWorkspace.installation.accountId, | |
| 711 | − executionId: null, | |
| 712 | − interactionId: null, | |
| 713 | − ...(payload as Record<string, unknown>), | |
| 714 | − } as ExecutorAddSourceInput), | |
| 715 | − { baseUrl }, | |
| 716 | − ); | |
| 712 | + return yield* sourceAuthService.addExecutorSource( | |
| 713 | + { | |
| 714 | + workspaceId: path.workspaceId, | |
| 715 | + actorAccountId: runtimeLocalWorkspace.installation.accountId, | |
| 716 | + executionId: null, | |
| 717 | + interactionId: null, | |
| 718 | + ...(payload as Record<string, unknown>), | |
| 719 | + } as ExecutorAddSourceInput, | |
| 720 | + { baseUrl }, | |
| 721 | + ); | |
| 717 | 722 | }).pipe( |
| 718 | 723 | Effect.catchAll((cause) => |
| 719 | 724 | Effect.fail(toBadRequestError("sources.connect", cause)), |
| 720 | 725 | ), |
| 721 | − ) | |
| 726 | + ), | |
| 722 | 727 | ), |
| 723 | 728 | ), |
| 724 | 729 | ) |
| 725 | 730 | .handle("connectBatch", ({ path, payload }) => |
| 726 | − resolveRequestedLocalWorkspace("sources.connectBatch", path.workspaceId).pipe( | |
| 731 | + resolveRequestedLocalWorkspace( | |
| 732 | + "sources.connectBatch", | |
| 733 | + path.workspaceId, | |
| 734 | + ).pipe( | |
| 727 | 735 | Effect.flatMap((runtimeLocalWorkspace) => |
| 728 | 736 | Effect.gen(function* () { |
| 729 | 737 | const request = yield* HttpServerRequest.HttpServerRequest; |
| @@ -748,7 +756,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 18 unmodified lines | ||
| 748 | 756 | ), |
| 749 | 757 | ) |
| 750 | 758 | .handle("listWorkspaceOauthClients", ({ path, urlParams }) => |
| 751 | − resolveRequestedLocalWorkspace("sources.listWorkspaceOauthClients", path.workspaceId).pipe( | |
| 759 | + resolveRequestedLocalWorkspace( | |
| 760 | + "sources.listWorkspaceOauthClients", | |
| 761 | + path.workspaceId, | |
| 762 | + ).pipe( | |
| 752 | 763 | Effect.flatMap(() => |
| 753 | 764 | Effect.gen(function* () { |
| 754 | 765 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| @@ -758,14 +769,19 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | ||
| 758 | 769 | }); |
| 759 | 770 | }).pipe( |
| 760 | 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 | 780 | .handle("createWorkspaceOauthClient", ({ path, payload }) => |
| 768 | − resolveRequestedLocalWorkspace("sources.createWorkspaceOauthClient", path.workspaceId).pipe( | |
| 781 | + resolveRequestedLocalWorkspace( | |
| 782 | + "sources.createWorkspaceOauthClient", | |
| 783 | + path.workspaceId, | |
| 784 | + ).pipe( | |
| 769 | 785 | Effect.flatMap(() => |
| 770 | 786 | Effect.gen(function* () { |
| 771 | 787 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| @@ -777,44 +793,68 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines | ||
| 777 | 793 | }); |
| 778 | 794 | }).pipe( |
| 779 | 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 | 807 | .handle("removeWorkspaceOauthClient", ({ path }) => |
| 787 | − resolveRequestedLocalWorkspace("sources.removeWorkspaceOauthClient", path.workspaceId).pipe( | |
| 808 | + resolveRequestedLocalWorkspace( | |
| 809 | + "sources.removeWorkspaceOauthClient", | |
| 810 | + path.workspaceId, | |
| 811 | + ).pipe( | |
| 788 | 812 | Effect.flatMap(() => |
| 789 | 813 | Effect.gen(function* () { |
| 790 | 814 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 791 | − const removed = yield* sourceAuthService.removeWorkspaceOauthClient({ | |
| 792 | − workspaceId: path.workspaceId, | |
| 793 | − oauthClientId: path.oauthClientId, | |
| 794 | − }); | |
| 815 | + const removed = | |
| 816 | + yield* sourceAuthService.removeWorkspaceOauthClient({ | |
| 817 | + workspaceId: path.workspaceId, | |
| 818 | + oauthClientId: path.oauthClientId, | |
| 819 | + }); | |
| 795 | 820 | |
| 796 | 821 | return { removed }; |
| 797 | 822 | }).pipe( |
| 798 | 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 | 835 | .handle("removeProviderAuthGrant", ({ path }) => |
| 806 | − resolveRequestedLocalWorkspace("sources.removeProviderAuthGrant", path.workspaceId).pipe( | |
| 836 | + resolveRequestedLocalWorkspace( | |
| 837 | + "sources.removeProviderAuthGrant", | |
| 838 | + path.workspaceId, | |
| 839 | + ).pipe( | |
| 807 | 840 | Effect.flatMap(() => |
| 808 | 841 | Effect.gen(function* () { |
| 809 | 842 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 810 | − const removed = yield* sourceAuthService.removeProviderAuthGrant({ | |
| 811 | − workspaceId: path.workspaceId, | |
| 812 | − grantId: path.grantId, | |
| 813 | − }).pipe( | |
| 814 | − Effect.catchAll((cause) => | |
| 815 | − Effect.fail(toBadRequestError("sources.removeProviderAuthGrant", cause)), | |
| 816 | − ), | |
| 817 | − ); | |
| 843 | + const removed = yield* sourceAuthService | |
| 844 | + .removeProviderAuthGrant({ | |
| 845 | + workspaceId: path.workspaceId, | |
| 846 | + grantId: path.grantId, | |
| 847 | + }) | |
| 848 | + .pipe( | |
| 849 | + Effect.catchAll((cause) => | |
| 850 | + Effect.fail( | |
| 851 | + toBadRequestError( | |
| 852 | + "sources.removeProviderAuthGrant", | |
| 853 | + cause, | |
| 854 | + ), | |
| 855 | + ), | |
| 856 | + ), | |
| 857 | + ); | |
| 818 | 858 | |
| 819 | 859 | return { removed }; |
| 820 | 860 | }), |
| @@ -827,7 +867,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 6 unmodified lines | ||
| 827 | 867 | listSources({ |
| 828 | 868 | workspaceId: path.workspaceId, |
| 829 | 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 | 878 | workspaceId: path.workspaceId, |
| 839 | 879 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 840 | 880 | payload, |
| 841 | − }) | |
| 881 | + }), | |
| 842 | 882 | ), |
| 843 | 883 | ), |
| 844 | 884 | ) |
| @@ -849,12 +889,15 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | ||
| 849 | 889 | workspaceId: path.workspaceId, |
| 850 | 890 | sourceId: path.sourceId, |
| 851 | 891 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 852 | − }) | |
| 892 | + }), | |
| 853 | 893 | ), |
| 854 | 894 | ), |
| 855 | 895 | ) |
| 856 | 896 | .handle("inspection", ({ path }) => |
| 857 | − resolveRequestedLocalWorkspace("sources.inspection", path.workspaceId).pipe( | |
| 897 | + resolveRequestedLocalWorkspace( | |
| 898 | + "sources.inspection", | |
| 899 | + path.workspaceId, | |
| 900 | + ).pipe( | |
| 858 | 901 | Effect.zipRight( |
| 859 | 902 | getSourceInspection({ |
| 860 | 903 | workspaceId: path.workspaceId, |
| @@ -864,7 +907,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | ||
| 864 | 907 | ), |
| 865 | 908 | ) |
| 866 | 909 | .handle("inspectionTool", ({ path }) => |
| 867 | − resolveRequestedLocalWorkspace("sources.inspection_tool", path.workspaceId).pipe( | |
| 910 | + resolveRequestedLocalWorkspace( | |
| 911 | + "sources.inspection_tool", | |
| 912 | + path.workspaceId, | |
| 913 | + ).pipe( | |
| 868 | 914 | Effect.zipRight( |
| 869 | 915 | getSourceInspectionToolDetail({ |
| 870 | 916 | workspaceId: path.workspaceId, |
| @@ -875,7 +921,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | ||
| 875 | 921 | ), |
| 876 | 922 | ) |
| 877 | 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 | 928 | Effect.zipRight( |
| 880 | 929 | discoverSourceInspectionTools({ |
| 881 | 930 | workspaceId: path.workspaceId, |
| @@ -893,7 +942,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 11 unmodified lines | ||
| 893 | 942 | sourceId: path.sourceId, |
| 894 | 943 | accountId: runtimeLocalWorkspace.installation.accountId, |
| 895 | 944 | payload, |
| 896 | − }) | |
| 945 | + }), | |
| 897 | 946 | ), |
| 898 | 947 | ), |
| 899 | 948 | ) |
| @@ -922,9 +971,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 22 unmodified lines | ||
| 922 | 971 | sourceLabel: interaction.sourceLabel, |
| 923 | 972 | endpoint: interaction.endpoint, |
| 924 | 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 | 979 | Effect.catchTags({ |
| 930 | 980 | ControlPlaneNotFoundError: (error) => |
| @@ -967,14 +1017,14 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 36 unmodified lines | ||
| 967 | 1017 | ? "Source Credential Stored" |
| 968 | 1018 | : result.kind === "continued" |
| 969 | 1019 | ? "Source Access Updated" |
| 970 | − : "Source Credential Request Cancelled", | |
| 1020 | + : "Source Credential Request Cancelled", | |
| 971 | 1021 | eyebrow: "Executor", |
| 972 | 1022 | message: |
| 973 | 1023 | result.kind === "stored" |
| 974 | 1024 | ? "Executor stored the credential and resumed the source connection." |
| 975 | 1025 | : result.kind === "continued" |
| 976 | 1026 | ? "Executor resumed the source connection without stored credentials." |
| 977 | − : "Executor cancelled the source credential request.", | |
| 1027 | + : "Executor cancelled the source credential request.", | |
| 978 | 1028 | sourceLabel: result.sourceLabel, |
| 979 | 1029 | endpoint: result.endpoint, |
| 980 | 1030 | interactionId: urlParams.interactionId, |
| @@ -985,7 +1035,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines | ||
| 985 | 1035 | ? "continued" |
| 986 | 1036 | : "cancelled", |
| 987 | 1037 | }), |
| 988 | − ) | |
| 1038 | + ), | |
| 989 | 1039 | ), |
| 990 | 1040 | Effect.catchTags({ |
| 991 | 1041 | ControlPlaneBadRequestError: (error) => |
| @@ -1044,12 +1094,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 52 unmodified lines | ||
| 1044 | 1094 | htmlResponse( |
| 1045 | 1095 | sourceOAuthPopupResultDocument({ |
| 1046 | 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 | 1101 | state: "cancelled", |
| 1049 | 1102 | payload: { |
| 1050 | 1103 | type: "executor:source-oauth-result", |
| 1051 | 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 | 1111 | 500, |
| @@ -1059,18 +1115,22 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines | ||
| 1059 | 1115 | ), |
| 1060 | 1116 | ) |
| 1061 | 1117 | .handle("providerOauthComplete", ({ path, urlParams }) => |
| 1062 | − resolveRequestedLocalWorkspace("sources.providerOauthComplete", path.workspaceId).pipe( | |
| 1118 | + resolveRequestedLocalWorkspace( | |
| 1119 | + "sources.providerOauthComplete", | |
| 1120 | + path.workspaceId, | |
| 1121 | + ).pipe( | |
| 1063 | 1122 | Effect.flatMap((runtimeLocalWorkspace) => |
| 1064 | 1123 | Effect.gen(function* () { |
| 1065 | 1124 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 1066 | − const completed = yield* sourceAuthService.completeProviderOauthCallback({ | |
| 1067 | − workspaceId: path.workspaceId, | |
| 1068 | − actorAccountId: runtimeLocalWorkspace.installation.accountId, | |
| 1069 | − state: urlParams.state, | |
| 1070 | − code: urlParams.code, | |
| 1071 | − error: urlParams.error, | |
| 1072 | − errorDescription: urlParams.error_description, | |
| 1073 | − }); | |
| 1125 | + const completed = | |
| 1126 | + yield* sourceAuthService.completeProviderOauthCallback({ | |
| 1127 | + workspaceId: path.workspaceId, | |
| 1128 | + actorAccountId: runtimeLocalWorkspace.installation.accountId, | |
| 1129 | + state: urlParams.state, | |
| 1130 | + code: urlParams.code, | |
| 1131 | + error: urlParams.error, | |
| 1132 | + errorDescription: urlParams.error_description, | |
| 1133 | + }); | |
| 1074 | 1134 | const primarySource = completed.sources[0] ?? null; |
| 1075 | 1135 | |
| 1076 | 1136 | return htmlResponse( |
| @@ -1082,7 +1142,9 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines | ||
| 1082 | 1142 | payload: { |
| 1083 | 1143 | type: "executor:source-oauth-result", |
| 1084 | 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 | 1154 | htmlResponse( |
| 1093 | 1155 | sourceOAuthPopupResultDocument({ |
| 1094 | 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 | 1161 | state: "cancelled", |
| 1097 | 1162 | payload: { |
| 1098 | 1163 | type: "executor:source-oauth-result", |
| 1099 | 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 | 1171 | 500, |
| @@ -1111,12 +1179,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 7 unmodified lines | ||
| 1111 | 1179 | htmlResponse( |
| 1112 | 1180 | sourceOAuthPopupResultDocument({ |
| 1113 | 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 | 1186 | state: "cancelled", |
| 1116 | 1187 | payload: { |
| 1117 | 1188 | type: "executor:source-oauth-result", |
| 1118 | 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 | 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 | 9 | "lib": ["ES2022"], |
| 10 | 10 | "types": ["bun-types", "node"] |
| 11 | 11 | }, |
| 12 | − "include": [ | |
| 13 | − "src/**/*.ts", | |
| 14 | − "src/**/*.d.ts" | |
| 15 | − ], | |
| 16 | − "exclude": [ | |
| 17 | − "src/**/*.test.ts", | |
| 18 | − "src/persistence/**/*.ts" | |
| 19 | − ] | |
| 12 | + "include": ["src/**/*.ts"] | |
| 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 | 3 | "private": true, |
| 4 | 4 | "type": "module", |
| 5 | 5 | "exports": { |
| 6 | 6 | ".": "./src/index.ts", |
| 7 | − "./client": "./src/client.ts", | |
| 8 | − "./schema": "./src/schema/index.ts" | |
| 7 | + "./errors": "./src/errors.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 | 22 | "imports": { |
| 11 | − "#schema": "./src/schema/index.ts", | |
| 12 | − "#domain": "./src/domain/index.ts", | |
| 13 | − "#api": "./src/api/index.ts", | |
| 14 | − "#runtime": "./src/runtime/index.ts" | |
| 23 | + "#runtime": "./src/runtime/index.ts", | |
| 24 | + "#schema": "./src/schema/index.ts" | |
| 15 | 25 | }, |
| 16 | 26 | "scripts": { |
| 17 | 27 | "fixture:release:capture": "bun run ./src/runtime/local/capture-release-workspace-fixture.ts", |
| 18 | − "typecheck": "bunx tsc --noEmit -p tsconfig.json", | |
| 19 | 28 | "test": "vitest run", |
| 20 | − "test:watch": "vitest" | |
| 29 | + "test:watch": "vitest", | |
| 30 | + "typecheck": "bunx tsc --noEmit -p tsconfig.json" | |
| 21 | 31 | }, |
| 22 | 32 | "dependencies": { |
| 23 | 33 | "@effect/platform": "catalog:", |
| 24 | − "@executor/source-core": "workspace:*", | |
| 25 | − "@executor/source-builtins": "workspace:*", | |
| 34 | + "@effect/platform-node": "catalog:", | |
| 26 | 35 | "@executor/auth-mcp-oauth": "workspace:*", |
| 27 | 36 | "@executor/auth-oauth2": "workspace:*", |
| 28 | 37 | "@executor/codemode-core": "workspace:*", |
| 29 | − "@executor/source-google-discovery": "workspace:*", | |
| 30 | − "@executor/source-graphql": "workspace:*", | |
| 31 | 38 | "@executor/ir": "workspace:*", |
| 32 | − "@executor/source-mcp": "workspace:*", | |
| 33 | − "@executor/source-openapi": "workspace:*", | |
| 34 | 39 | "@executor/runtime-deno-subprocess": "workspace:*", |
| 35 | 40 | "@executor/runtime-quickjs": "workspace:*", |
| 36 | 41 | "@executor/runtime-ses": "workspace:*", |
| 37 | − "effect": "catalog:", | |
| 38 | − "graphql": "^16.12.0", | |
| 39 | − "jsonc-parser": "3.3.1", | |
| 40 | − "postgres": "^3.4.8", | |
| 41 | − "prettier": "^3.8.1", | |
| 42 | − "typescript": "^5.9.3" | |
| 42 | + "@executor/source-core": "workspace:*", | |
| 43 | + "@executor/source-builtins": "workspace:*", | |
| 44 | + "@executor/source-google-discovery": "workspace:*", | |
| 45 | + "@executor/source-graphql": "workspace:*", | |
| 46 | + "@executor/source-mcp": "workspace:*", | |
| 47 | + "@executor/source-openapi": "workspace:*", | |
| 48 | + "effect": "catalog:" | |
| 43 | 49 | }, |
| 44 | 50 | "devDependencies": { |
| 45 | 51 | "@effect/vitest": "catalog:", |
| 46 | − "@types/node": "catalog:", | |
| 47 | − "@effect/platform-node": "catalog:", | |
| 52 | + "@executor/platform-api": "workspace:*", | |
| 53 | + "@executor/platform-internal": "workspace:*", | |
| 48 | 54 | "@modelcontextprotocol/sdk": "^1.26.0", |
| 55 | + "@types/node": "catalog:", | |
| 49 | 56 | "bun-types": "catalog:", |
| 50 | − "vitest": "catalog:", | |
| 51 | − "zod": "catalog:" | |
| 57 | + "graphql": "^16.12.0", | |
| 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 | 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"; | |
| 2 | −import { LocalInstallationSchema, SecretMaterialPurposeSchema } from "#schema"; | |
| 1 | +import { SecretMaterialPurposeSchema } from "../schema"; | |
| 3 | 2 | import * as Schema from "effect/Schema"; |
| 4 | 3 | |
| 5 | −import { | |
| 6 | − ControlPlaneBadRequestError, | |
| 7 | − ControlPlaneNotFoundError, | |
| 8 | − ControlPlaneStorageError, | |
| 9 | −} from "../errors"; | |
| 10 | − | |
| 11 | 4 | export const SecretProviderSchema = Schema.Struct({ |
| 12 | 5 | id: Schema.String, |
| 13 | 6 | name: Schema.String, |
| @@ -23,8 +16,6 @@ export const InstanceConfigSchema = Schema.Struct({ · 9 unmodified lines | ||
| 23 | 16 | export type SecretProvider = typeof SecretProviderSchema.Type; |
| 24 | 17 | export type InstanceConfig = typeof InstanceConfigSchema.Type; |
| 25 | 18 | |
| 26 | −// -- Secrets CRUD schemas --------------------------------------------------- | |
| 27 | − | |
| 28 | 19 | export const SecretLinkedSourceSchema = Schema.Struct({ |
| 29 | 20 | sourceId: Schema.String, |
| 30 | 21 | sourceName: Schema.String, |
| @@ -87,45 +78,3 @@ export const DeleteSecretResultSchema = Schema.Struct({ · 56 unmodified lines | ||
| 87 | 78 | }); |
| 88 | 79 | |
| 89 | 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 | 3 | ExecutionInteraction, |
| 4 | 4 | Source, |
| 5 | 5 | WorkspaceId, |
| 6 | −} from "#schema"; | |
| 6 | +} from "../schema"; | |
| 7 | 7 | import { |
| 8 | 8 | SourceIdSchema, |
| 9 | 9 | WorkspaceIdSchema, |
| 10 | −} from "#schema"; | |
| 10 | +} from "../schema"; | |
| 11 | 11 | import * as Effect from "effect/Effect"; |
| 12 | 12 | import * as Option from "effect/Option"; |
| 13 | 13 | |
| 14 | −import { submitExecutionInteractionResponse } from "../execution/service"; | |
| 15 | −import { LiveExecutionManagerService } from "../execution/live"; | |
| 14 | +import { submitExecutionInteractionResponse } from "../runtime/execution/service"; | |
| 15 | +import { LiveExecutionManagerService } from "../runtime/execution/live"; | |
| 16 | +import { operationErrors } from "../runtime/policy/operation-errors"; | |
| 17 | +import { resolveLocalWorkspaceContext } from "../runtime/local/config"; | |
| 16 | 18 | import { |
| 17 | − operationErrors, | |
| 18 | −} from "../policy/operation-errors"; | |
| 19 | −import { resolveLocalWorkspaceContext } from "./config"; | |
| 20 | −import { InstallationStore, LocalInstallationStore } from "./storage"; | |
| 21 | −import { getRuntimeLocalWorkspaceOption } from "./runtime-context"; | |
| 22 | −import { RuntimeSourceAuthServiceTag } from "../sources/source-auth-service"; | |
| 19 | + InstallationStore, | |
| 20 | + LocalInstallationStore, | |
| 21 | +} from "../runtime/local/storage"; | |
| 22 | +import { getRuntimeLocalWorkspaceOption } from "../runtime/local/runtime-context"; | |
| 23 | +import { RuntimeSourceAuthServiceTag } from "../runtime/sources/source-auth-service"; | |
| 23 | 24 | import { |
| 24 | 25 | createSourceCredentialSelectionBearerContent, |
| 25 | 26 | createSourceCredentialSelectionNoneContent, |
| 26 | −} from "../sources/source-credential-interactions"; | |
| 27 | −import { ControlPlaneStore } from "../store"; | |
| 27 | +} from "../runtime/sources/source-credential-interactions"; | |
| 28 | +import { ControlPlaneStore } from "../runtime/store"; | |
| 28 | 29 | |
| 29 | 30 | const localOps = { |
| 30 | 31 | installation: operationErrors("local.installation.get"), |
| @@ -59,13 +60,15 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 28 unmodified lines | ||
| 59 | 60 | return trimmed.length > 0 ? trimmed : null; |
| 60 | 61 | }; |
| 61 | 62 | |
| 62 | −const decodeSourceCredentialInteraction = ( | |
| 63 | +export const decodeSourceCredentialInteraction = ( | |
| 63 | 64 | interaction: ExecutionInteraction, |
| 64 | 65 | ): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => { |
| 65 | 66 | try { |
| 66 | − if (interaction.purpose !== "source_connect_oauth2" | |
| 67 | − && interaction.purpose !== "source_connect_secret" | |
| 68 | − && interaction.purpose !== "elicitation") { | |
| 67 | + if ( | |
| 68 | + interaction.purpose !== "source_connect_oauth2" && | |
| 69 | + interaction.purpose !== "source_connect_secret" && | |
| 70 | + interaction.purpose !== "elicitation" | |
| 71 | + ) { | |
| 69 | 72 | return null; |
| 70 | 73 | } |
| 71 | 74 | |
| @@ -80,18 +83,25 @@ const decodeSourceCredentialInteraction = ( · 8 unmodified lines | ||
| 80 | 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 | 87 | return null; |
| 85 | 88 | } |
| 86 | − const effectivePurpose = interaction.purpose === "elicitation" | |
| 87 | − ? elicitation.mode === "url" | |
| 88 | − ? "source_connect_oauth2" | |
| 89 | − : "source_connect_secret" | |
| 90 | − : interaction.purpose; | |
| 91 | − if (effectivePurpose === "source_connect_oauth2" && elicitation.mode !== "url") { | |
| 89 | + const effectivePurpose = | |
| 90 | + interaction.purpose === "elicitation" | |
| 91 | + ? elicitation.mode === "url" | |
| 92 | + ? "source_connect_oauth2" | |
| 93 | + : "source_connect_secret" | |
| 94 | + : interaction.purpose; | |
| 95 | + if ( | |
| 96 | + effectivePurpose === "source_connect_oauth2" && | |
| 97 | + elicitation.mode !== "url" | |
| 98 | + ) { | |
| 92 | 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 | 105 | return null; |
| 96 | 106 | } |
| 97 | 107 | |
| @@ -127,45 +137,49 @@ const loadSourceCredentialInteraction = (input: { · 29 unmodified lines | ||
| 127 | 137 | const store = yield* ControlPlaneStore; |
| 128 | 138 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 129 | 139 | |
| 130 | − const stored = yield* store.executionInteractions.getById(input.interactionId).pipe( | |
| 131 | − Effect.mapError((error) => | |
| 132 | − input.operation.unknownStorage( | |
| 133 | − error, | |
| 134 | − `Failed loading execution interaction ${input.interactionId}`, | |
| 140 | + const stored = yield* store.executionInteractions | |
| 141 | + .getById(input.interactionId) | |
| 142 | + .pipe( | |
| 143 | + Effect.mapError((error) => | |
| 144 | + input.operation.unknownStorage( | |
| 145 | + error, | |
| 146 | + `Failed loading execution interaction ${input.interactionId}`, | |
| 147 | + ), | |
| 135 | 148 | ), |
| 136 | − ), | |
| 137 | − ); | |
| 149 | + ); | |
| 138 | 150 | |
| 139 | 151 | if (Option.isNone(stored)) { |
| 140 | 152 | return yield* input.operation.notFound( |
| 141 | − "Source credential request not found", | |
| 142 | − `interactionId=${input.interactionId}`, | |
| 143 | − ); | |
| 153 | + "Source credential request not found", | |
| 154 | + `interactionId=${input.interactionId}`, | |
| 155 | + ); | |
| 144 | 156 | } |
| 145 | 157 | |
| 146 | 158 | const decoded = decodeSourceCredentialInteraction(stored.value); |
| 147 | 159 | if ( |
| 148 | − decoded === null | |
| 149 | − || decoded.workspaceId !== input.workspaceId | |
| 150 | − || decoded.sourceId !== input.sourceId | |
| 160 | + decoded === null || | |
| 161 | + decoded.workspaceId !== input.workspaceId || | |
| 162 | + decoded.sourceId !== input.sourceId | |
| 151 | 163 | ) { |
| 152 | 164 | return yield* input.operation.notFound( |
| 153 | − "Source credential request not found", | |
| 154 | − `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`, | |
| 155 | − ); | |
| 165 | + "Source credential request not found", | |
| 166 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`, | |
| 167 | + ); | |
| 156 | 168 | } |
| 157 | 169 | |
| 158 | − const source = yield* sourceAuthService.getSourceById({ | |
| 159 | − workspaceId: input.workspaceId, | |
| 160 | − sourceId: input.sourceId, | |
| 161 | − }).pipe( | |
| 162 | − Effect.mapError((error) => | |
| 163 | − input.operation.unknownStorage( | |
| 164 | − error, | |
| 165 | − `Failed loading source ${input.sourceId}`, | |
| 170 | + const source = yield* sourceAuthService | |
| 171 | + .getSourceById({ | |
| 172 | + workspaceId: input.workspaceId, | |
| 173 | + sourceId: input.sourceId, | |
| 174 | + }) | |
| 175 | + .pipe( | |
| 176 | + Effect.mapError((error) => | |
| 177 | + input.operation.unknownStorage( | |
| 178 | + error, | |
| 179 | + `Failed loading source ${input.sourceId}`, | |
| 180 | + ), | |
| 166 | 181 | ), |
| 167 | − ), | |
| 168 | − ); | |
| 182 | + ); | |
| 169 | 183 | |
| 170 | 184 | return { |
| 171 | 185 | ...decoded, |
| @@ -177,23 +191,23 @@ const loadSourceCredentialInteraction = (input: { · 5 unmodified lines | ||
| 177 | 191 | export const getLocalInstallation = () => |
| 178 | 192 | Effect.gen(function* () { |
| 179 | 193 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 180 | − const context = runtimeLocalWorkspace?.context | |
| 181 | − ?? (yield* resolveLocalWorkspaceContext().pipe( | |
| 194 | + const context = | |
| 195 | + runtimeLocalWorkspace?.context ?? | |
| 196 | + (yield* resolveLocalWorkspaceContext().pipe( | |
| 182 | 197 | Effect.mapError((error) => |
| 183 | 198 | localOps.installation.unknownStorage( |
| 184 | 199 | error, |
| 185 | 200 | "Failed resolving local workspace context", |
| 186 | 201 | ), |
| 187 | − ), | |
| 202 | + ), | |
| 188 | 203 | )); |
| 189 | 204 | |
| 190 | − const installationStore = runtimeLocalWorkspace !== null | |
| 191 | − ? yield* InstallationStore | |
| 192 | − : LocalInstallationStore; | |
| 205 | + const installationStore = | |
| 206 | + runtimeLocalWorkspace !== null | |
| 207 | + ? yield* InstallationStore | |
| 208 | + : LocalInstallationStore; | |
| 193 | 209 | |
| 194 | − return yield* installationStore.load( | |
| 195 | − context, | |
| 196 | − ); | |
| 210 | + return yield* installationStore.load(context); | |
| 197 | 211 | }); |
| 198 | 212 | |
| 199 | 213 | export const getSourceCredentialInteraction = (input: { |
| @@ -223,9 +237,9 @@ export const submitSourceCredentialInteraction = (input: { · 23 unmodified lines | ||
| 223 | 237 | |
| 224 | 238 | if (interaction.status !== "pending") { |
| 225 | 239 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 226 | − "Source credential request is no longer active", | |
| 227 | − `interactionId=${interaction.interactionId} status=${interaction.status}`, | |
| 228 | − ); | |
| 240 | + "Source credential request is no longer active", | |
| 241 | + `interactionId=${interaction.interactionId} status=${interaction.status}`, | |
| 242 | + ); | |
| 229 | 243 | } |
| 230 | 244 | |
| 231 | 245 | const liveExecutionManager = yield* LiveExecutionManagerService; |
| @@ -249,15 +263,15 @@ export const submitSourceCredentialInteraction = (input: { · 17 unmodified lines | ||
| 249 | 263 | localOps.sourceCredentialSubmit.unknownStorage( |
| 250 | 264 | error, |
| 251 | 265 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 252 | − ) | |
| 266 | + ), | |
| 253 | 267 | ), |
| 254 | 268 | ); |
| 255 | 269 | |
| 256 | 270 | if (!persisted) { |
| 257 | 271 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 258 | − "Source credential request is no longer resumable", | |
| 259 | − `interactionId=${interaction.interactionId}`, | |
| 260 | − ); | |
| 272 | + "Source credential request is no longer resumable", | |
| 273 | + `interactionId=${interaction.interactionId}`, | |
| 274 | + ); | |
| 261 | 275 | } |
| 262 | 276 | } |
| 263 | 277 | |
| @@ -289,15 +303,15 @@ export const submitSourceCredentialInteraction = (input: { · 25 unmodified lines | ||
| 289 | 303 | localOps.sourceCredentialSubmit.unknownStorage( |
| 290 | 304 | error, |
| 291 | 305 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 292 | − ) | |
| 306 | + ), | |
| 293 | 307 | ), |
| 294 | 308 | ); |
| 295 | 309 | |
| 296 | 310 | if (!persisted) { |
| 297 | 311 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 298 | − "Source credential request is no longer resumable", | |
| 299 | − `interactionId=${interaction.interactionId}`, | |
| 300 | − ); | |
| 312 | + "Source credential request is no longer resumable", | |
| 313 | + `interactionId=${interaction.interactionId}`, | |
| 314 | + ); | |
| 301 | 315 | } |
| 302 | 316 | } |
| 303 | 317 | |
| @@ -311,23 +325,25 @@ export const submitSourceCredentialInteraction = (input: { · 7 unmodified lines | ||
| 311 | 325 | const token = trimOrNull(input.token); |
| 312 | 326 | if (token === null) { |
| 313 | 327 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 314 | − "Missing token", | |
| 315 | − `interactionId=${interaction.interactionId}`, | |
| 316 | − ); | |
| 328 | + "Missing token", | |
| 329 | + `interactionId=${interaction.interactionId}`, | |
| 330 | + ); | |
| 317 | 331 | } |
| 318 | 332 | |
| 319 | 333 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 320 | − const tokenRef = yield* sourceAuthService.storeSecretMaterial({ | |
| 321 | − purpose: "auth_material", | |
| 322 | − value: token, | |
| 323 | − }).pipe( | |
| 324 | − Effect.mapError((error) => | |
| 325 | − localOps.sourceCredentialSubmit.unknownStorage( | |
| 326 | − error, | |
| 327 | − `Failed storing credential material for interaction ${interaction.interactionId}`, | |
| 334 | + const tokenRef = yield* sourceAuthService | |
| 335 | + .storeSecretMaterial({ | |
| 336 | + purpose: "auth_material", | |
| 337 | + value: token, | |
| 338 | + }) | |
| 339 | + .pipe( | |
| 340 | + Effect.mapError((error) => | |
| 341 | + localOps.sourceCredentialSubmit.unknownStorage( | |
| 342 | + error, | |
| 343 | + `Failed storing credential material for interaction ${interaction.interactionId}`, | |
| 344 | + ), | |
| 328 | 345 | ), |
| 329 | − ), | |
| 330 | − ); | |
| 346 | + ); | |
| 331 | 347 | const resumed = yield* liveExecutionManager.resolveInteraction({ |
| 332 | 348 | executionId: interaction.executionId, |
| 333 | 349 | response: { |
| @@ -348,15 +364,15 @@ export const submitSourceCredentialInteraction = (input: { · 14 unmodified lines | ||
| 348 | 364 | localOps.sourceCredentialSubmit.unknownStorage( |
| 349 | 365 | error, |
| 350 | 366 | `Failed resuming execution for interaction ${interaction.interactionId}`, |
| 351 | − ) | |
| 367 | + ), | |
| 352 | 368 | ), |
| 353 | 369 | ); |
| 354 | 370 | |
| 355 | 371 | if (!persisted) { |
| 356 | 372 | return yield* localOps.sourceCredentialSubmit.badRequest( |
| 357 | − "Source credential request is no longer resumable", | |
| 358 | − `interactionId=${interaction.interactionId}`, | |
| 359 | − ); | |
| 373 | + "Source credential request is no longer resumable", | |
| 374 | + `interactionId=${interaction.interactionId}`, | |
| 375 | + ); | |
| 360 | 376 | } |
| 361 | 377 | } |
| 362 | 378 | |
| @@ -378,12 +394,14 @@ export const completeSourceCredentialSetup = (input: { · 15 unmodified lines | ||
| 378 | 394 | Effect.gen(function* () { |
| 379 | 395 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 380 | 396 | |
| 381 | − return yield* sourceAuthService.completeSourceCredentialSetup(input).pipe( | |
| 382 | − Effect.mapError((error) => | |
| 383 | − localOps.sourceCredentialComplete.unknownStorage( | |
| 384 | − error, | |
| 385 | − "Failed completing source credential setup", | |
| 397 | + return yield* sourceAuthService | |
| 398 | + .completeSourceCredentialSetup(input) | |
| 399 | + .pipe( | |
| 400 | + Effect.mapError((error) => | |
| 401 | + localOps.sourceCredentialComplete.unknownStorage( | |
| 402 | + error, | |
| 403 | + "Failed completing source credential setup", | |
| 404 | + ), | |
| 386 | 405 | ), |
| 387 | − ), | |
| 388 | − ); | |
| 406 | + ); | |
| 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 | 3 | import type { |
| 4 | 4 | CreatePolicyPayload, |
| 5 | 5 | UpdatePolicyPayload, |
| 6 | −} from "../../api/policies/api"; | |
| 6 | +} from "./contracts"; | |
| 7 | 7 | import { |
| 8 | 8 | PolicyIdSchema, |
| 9 | 9 | type LocalExecutorConfig, |
| 10 | 10 | type LocalWorkspacePolicy, |
| 11 | 11 | type PolicyId, |
| 12 | 12 | type WorkspaceId, |
| 13 | −} from "#schema"; | |
| 13 | +} from "../schema"; | |
| 14 | 14 | import * as Effect from "effect/Effect"; |
| 15 | 15 | |
| 16 | −import { | |
| 17 | − requireRuntimeLocalWorkspace, | |
| 18 | −} from "../local/runtime-context"; | |
| 16 | +import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context"; | |
| 19 | 17 | import type { |
| 20 | 18 | WorkspaceConfigStoreShape, |
| 21 | 19 | WorkspaceStateStoreShape, |
| 22 | −} from "../local/storage"; | |
| 20 | +} from "../runtime/local/storage"; | |
| 23 | 21 | import { |
| 24 | 22 | WorkspaceConfigStore, |
| 25 | 23 | WorkspaceStateStore, |
| 26 | −} from "../local/storage"; | |
| 27 | −import type { ResolvedLocalWorkspaceContext } from "../local/config"; | |
| 28 | −import { | |
| 29 | − type LocalWorkspaceState, | |
| 30 | −} from "../local/workspace-state"; | |
| 31 | −import { | |
| 32 | − derivePolicyConfigKey, | |
| 33 | −} from "../local/workspace-sync"; | |
| 24 | +} from "../runtime/local/storage"; | |
| 25 | +import type { ResolvedLocalWorkspaceContext } from "../runtime/local/config"; | |
| 26 | +import { type LocalWorkspaceState } from "../runtime/local/workspace-state"; | |
| 27 | +import { derivePolicyConfigKey } from "../runtime/local/workspace-sync"; | |
| 34 | 28 | import { |
| 35 | 29 | type OperationErrors, |
| 36 | 30 | operationErrors, |
| 37 | −} from "./operation-errors"; | |
| 31 | +} from "../runtime/policy/operation-errors"; | |
| 38 | 32 | |
| 39 | 33 | const policyOps = { |
| 40 | 34 | list: operationErrors("policies.list"), |
| @@ -44,8 +38,7 @@ const policyOps = { · 3 unmodified lines | ||
| 44 | 38 | remove: operationErrors("policies.remove"), |
| 45 | 39 | } as const; |
| 46 | 40 | |
| 47 | −const cloneJson = <T>(value: T): T => | |
| 48 | − JSON.parse(JSON.stringify(value)) as T; | |
| 41 | +const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T; | |
| 49 | 42 | |
| 50 | 43 | const localPolicyIdForKey = (input: { |
| 51 | 44 | workspaceRoot: string; |
| @@ -62,10 +55,12 @@ const toLocalWorkspacePolicy = (input: { · 10 unmodified lines | ||
| 62 | 55 | policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string]; |
| 63 | 56 | state: LocalWorkspaceState["policies"][string] | undefined; |
| 64 | 57 | }): LocalWorkspacePolicy => ({ |
| 65 | − id: input.state?.id ?? localPolicyIdForKey({ | |
| 66 | − workspaceRoot: input.workspaceRoot, | |
| 67 | − key: input.key, | |
| 68 | − }), | |
| 58 | + id: | |
| 59 | + input.state?.id ?? | |
| 60 | + localPolicyIdForKey({ | |
| 61 | + workspaceRoot: input.workspaceRoot, | |
| 62 | + key: input.key, | |
| 63 | + }), | |
| 69 | 64 | key: input.key, |
| 70 | 65 | workspaceId: input.workspaceId, |
| 71 | 66 | resourcePattern: input.policyConfig.match.trim(), |
| @@ -79,7 +74,8 @@ const toLocalWorkspacePolicy = (input: { · 7 unmodified lines | ||
| 79 | 74 | |
| 80 | 75 | export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => |
| 81 | 76 | Effect.gen(function* () { |
| 82 | − const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); | |
| 77 | + const runtimeLocalWorkspace = | |
| 78 | + yield* requireRuntimeLocalWorkspace(workspaceId); | |
| 83 | 79 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 84 | 80 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 85 | 81 | const loadedConfig = yield* workspaceConfigStore.load( |
| @@ -89,14 +85,16 @@ export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => · 3 unmodified lines | ||
| 89 | 85 | runtimeLocalWorkspace.context, |
| 90 | 86 | ); |
| 91 | 87 | |
| 92 | − const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(([key, policyConfig]) => | |
| 93 | − toLocalWorkspacePolicy({ | |
| 94 | − workspaceId, | |
| 95 | − workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 96 | − key, | |
| 97 | − policyConfig, | |
| 98 | − state: workspaceState.policies[key], | |
| 99 | − })); | |
| 88 | + const policies = Object.entries(loadedConfig.config?.policies ?? {}).map( | |
| 89 | + ([key, policyConfig]) => | |
| 90 | + toLocalWorkspacePolicy({ | |
| 91 | + workspaceId, | |
| 92 | + workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 93 | + key, | |
| 94 | + policyConfig, | |
| 95 | + state: workspaceState.policies[key], | |
| 96 | + }), | |
| 97 | + ); | |
| 100 | 98 | |
| 101 | 99 | return { |
| 102 | 100 | runtimeLocalWorkspace, |
| @@ -114,16 +112,19 @@ const writeLocalPolicyFiles = (input: { · 11 unmodified lines | ||
| 114 | 112 | projectConfig: LocalExecutorConfig; |
| 115 | 113 | workspaceState: LocalWorkspaceState; |
| 116 | 114 | }) => |
| 117 | − Effect.all([ | |
| 118 | − input.workspaceConfigStore.writeProject({ | |
| 119 | − context: input.context, | |
| 120 | − config: input.projectConfig, | |
| 121 | − }), | |
| 122 | − input.workspaceStateStore.write({ | |
| 123 | − context: input.context, | |
| 124 | − state: input.workspaceState, | |
| 125 | − }), | |
| 126 | − ], { discard: true }).pipe( | |
| 115 | + Effect.all( | |
| 116 | + [ | |
| 117 | + input.workspaceConfigStore.writeProject({ | |
| 118 | + context: input.context, | |
| 119 | + config: input.projectConfig, | |
| 120 | + }), | |
| 121 | + input.workspaceStateStore.write({ | |
| 122 | + context: input.context, | |
| 123 | + state: input.workspaceState, | |
| 124 | + }), | |
| 125 | + ], | |
| 126 | + { discard: true }, | |
| 127 | + ).pipe( | |
| 127 | 128 | Effect.mapError((cause) => |
| 128 | 129 | input.operation.unknownStorage( |
| 129 | 130 | cause, |
| @@ -148,7 +149,9 @@ const loadWorkspacePolicyContext = ( · 18 unmodified lines | ||
| 148 | 149 | export const listPolicies = (workspaceId: WorkspaceId) => |
| 149 | 150 | Effect.gen(function* () { |
| 150 | 151 | yield* loadWorkspacePolicyContext(policyOps.list, workspaceId); |
| 151 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(workspaceId).pipe( | |
| 152 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( | |
| 153 | + workspaceId, | |
| 154 | + ).pipe( | |
| 152 | 155 | Effect.mapError((cause) => |
| 153 | 156 | policyOps.list.unknownStorage( |
| 154 | 157 | cause, |
| @@ -164,10 +167,15 @@ export const createPolicy = (input: { · 9 unmodified lines | ||
| 164 | 167 | payload: CreatePolicyPayload; |
| 165 | 168 | }) => |
| 166 | 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 | 174 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 169 | 175 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 170 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 176 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( | |
| 177 | + input.workspaceId, | |
| 178 | + ).pipe( | |
| 171 | 179 | Effect.mapError((cause) => |
| 172 | 180 | policyOps.create.unknownStorage( |
| 173 | 181 | cause, |
| @@ -176,7 +184,9 @@ export const createPolicy = (input: { · 2 unmodified lines | ||
| 176 | 184 | ), |
| 177 | 185 | ); |
| 178 | 186 | const now = Date.now(); |
| 179 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 187 | + const projectConfig = cloneJson( | |
| 188 | + localWorkspace.loadedConfig.projectConfig ?? {}, | |
| 189 | + ); | |
| 180 | 190 | const policies = { ...projectConfig.policies }; |
| 181 | 191 | const key = derivePolicyConfigKey( |
| 182 | 192 | { |
| @@ -190,9 +200,14 @@ export const createPolicy = (input: { · 7 unmodified lines | ||
| 190 | 200 | policies[key] = { |
| 191 | 201 | match: input.payload.resourcePattern ?? "*", |
| 192 | 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 | 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 | 213 | const existingState = localWorkspace.workspaceState.policies[key]; |
| @@ -201,10 +216,12 @@ export const createPolicy = (input: { · 2 unmodified lines | ||
| 201 | 216 | policies: { |
| 202 | 217 | ...localWorkspace.workspaceState.policies, |
| 203 | 218 | [key]: { |
| 204 | − id: existingState?.id ?? localPolicyIdForKey({ | |
| 205 | − workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 206 | − key, | |
| 207 | − }), | |
| 219 | + id: | |
| 220 | + existingState?.id ?? | |
| 221 | + localPolicyIdForKey({ | |
| 222 | + workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 223 | + key, | |
| 224 | + }), | |
| 208 | 225 | createdAt: existingState?.createdAt ?? now, |
| 209 | 226 | updatedAt: now, |
| 210 | 227 | }, |
| @@ -238,7 +255,9 @@ export const getPolicy = (input: { · 27 unmodified lines | ||
| 238 | 255 | }) => |
| 239 | 256 | Effect.gen(function* () { |
| 240 | 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 | 261 | Effect.mapError((cause) => |
| 243 | 262 | policyOps.get.unknownStorage( |
| 244 | 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 | 272 | if (policy === null) { |
| 251 | 273 | return yield* policyOps.get.notFound( |
| 252 | − "Policy not found", | |
| 253 | − `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | |
| 254 | − ); | |
| 274 | + "Policy not found", | |
| 275 | + `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | |
| 276 | + ); | |
| 255 | 277 | } |
| 256 | 278 | return policy; |
| 257 | 279 | }); |
| @@ -262,10 +284,15 @@ export const updatePolicy = (input: { · 4 unmodified lines | ||
| 262 | 284 | payload: UpdatePolicyPayload; |
| 263 | 285 | }) => |
| 264 | 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 | 291 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 267 | 292 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 268 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 293 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( | |
| 294 | + input.workspaceId, | |
| 295 | + ).pipe( | |
| 269 | 296 | Effect.mapError((cause) => |
| 270 | 297 | policyOps.update.unknownStorage( |
| 271 | 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 | 307 | if (existing === null) { |
| 278 | 308 | return yield* policyOps.update.notFound( |
| 279 | − "Policy not found", | |
| 280 | − `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | |
| 281 | − ); | |
| 309 | + "Policy not found", | |
| 310 | + `workspaceId=${input.workspaceId} policyId=${input.policyId}`, | |
| 311 | + ); | |
| 282 | 312 | } |
| 283 | 313 | |
| 284 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 314 | + const projectConfig = cloneJson( | |
| 315 | + localWorkspace.loadedConfig.projectConfig ?? {}, | |
| 316 | + ); | |
| 285 | 317 | const policies = { ...projectConfig.policies }; |
| 286 | 318 | const existingConfig = policies[existing.key]!; |
| 287 | 319 | |
| 288 | 320 | policies[existing.key] = { |
| 289 | 321 | ...existingConfig, |
| 290 | − ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}), | |
| 291 | − ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}), | |
| 322 | + ...(input.payload.resourcePattern !== undefined | |
| 323 | + ? { match: input.payload.resourcePattern } | |
| 324 | + : {}), | |
| 325 | + ...(input.payload.effect !== undefined | |
| 326 | + ? { action: input.payload.effect } | |
| 327 | + : {}), | |
| 292 | 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 | 342 | const updatedAt = Date.now(); |
| @@ -336,10 +379,15 @@ export const removePolicy = (input: { · 36 unmodified lines | ||
| 336 | 379 | policyId: PolicyId; |
| 337 | 380 | }) => |
| 338 | 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 | 386 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 341 | 387 | const workspaceStateStore = yield* WorkspaceStateStore; |
| 342 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 388 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies( | |
| 389 | + input.workspaceId, | |
| 390 | + ).pipe( | |
| 343 | 391 | Effect.mapError((cause) => |
| 344 | 392 | policyOps.remove.unknownStorage( |
| 345 | 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 | 402 | if (existing === null) { |
| 352 | 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 | 409 | const policies = { ...projectConfig.policies }; |
| 357 | 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 | 414 | yield* writeLocalPolicyFiles({ |
| 361 | 415 | operation: policyOps.remove, |
| 362 | 416 | context: runtimeLocalWorkspace.context, |
| @@ -35,4 +35,4 @@ On releases where you want upgrade coverage: · 34 unmodified lines | ||
| 35 | 35 | 1. Start from a real workspace that has a fully synced source on the released version. |
| 36 | 36 | 2. Run `bun run fixture:release:capture -- ...` for that source. |
| 37 | 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 | 1 | import { |
| 2 | 2 | ControlPlaneNotFoundError, |
| 3 | 3 | ControlPlaneStorageError, |
| 4 | −} from "../../api/errors"; | |
| 4 | +} from "../../errors"; | |
| 5 | 5 | import type { |
| 6 | 6 | ElicitationResponse, |
| 7 | 7 | OnElicitation, |
| @@ -11,7 +11,7 @@ import type { · 3 unmodified lines | ||
| 11 | 11 | import type { |
| 12 | 12 | CreateExecutionPayload, |
| 13 | 13 | ResumeExecutionPayload, |
| 14 | −} from "../../api/executions/api"; | |
| 14 | +} from "../../executions/contracts"; | |
| 15 | 15 | import { |
| 16 | 16 | ExecutionIdSchema, |
| 17 | 17 | ExecutionInteractionIdSchema, |
File renamed without changes.
| @@ -3,13 +3,12 @@ import { · 2 unmodified lines | ||
| 3 | 3 | HttpApiClient, |
| 4 | 4 | } from "@effect/platform"; |
| 5 | 5 | import { NodeHttpServer } from "@effect/platform-node"; |
| 6 | −import * as Effect from "effect/Effect"; | |
| 7 | −import * as Layer from "effect/Layer"; | |
| 8 | − | |
| 9 | 6 | import { |
| 10 | 7 | ControlPlaneApi, |
| 11 | 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 | 13 | import { |
| 15 | 14 | type ControlPlaneRuntime, |
File renamed without changes.
| @@ -15,16 +15,11 @@ import { · 14 unmodified lines | ||
| 15 | 15 | resolveConfiguredExecutionRuntime, |
| 16 | 16 | } from "../runtime"; |
| 17 | 17 | import { createWorkspaceToolInvoker } from "./tool-invoker"; |
| 18 | −import { | |
| 19 | − RuntimeSourceAuthServiceTag, | |
| 20 | −} from "../../sources/source-auth-service"; | |
| 21 | −import { | |
| 22 | − RuntimeSourceCatalogStoreService, | |
| 23 | −} from "../../catalog/source/runtime"; | |
| 18 | +import { RuntimeSourceAuthServiceTag } from "../../sources/source-auth-service"; | |
| 19 | +import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime"; | |
| 24 | 20 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; |
| 25 | −import { | |
| 26 | − getRuntimeLocalWorkspaceOption, | |
| 27 | −} from "../../local/runtime-context"; | |
| 21 | +import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync"; | |
| 22 | +import { getRuntimeLocalWorkspaceOption } from "../../local/runtime-context"; | |
| 28 | 23 | import { |
| 29 | 24 | LocalToolRuntimeLoaderService, |
| 30 | 25 | type LocalToolRuntimeLoaderShape, |
| @@ -38,6 +33,9 @@ import { · 7 unmodified lines | ||
| 38 | 33 | WorkspaceStateStore, |
| 39 | 34 | type WorkspaceStateStoreShape, |
| 40 | 35 | } from "../../local/storage"; |
| 36 | +import { ControlPlaneStore } from "../../store"; | |
| 37 | +import { RuntimeSourceStoreService } from "../../sources/source-store"; | |
| 38 | +import type { CreateWorkspaceInternalToolMap } from "./tool-invoker"; | |
| 41 | 39 | export { |
| 42 | 40 | createCodeExecutorForRuntime, |
| 43 | 41 | resolveConfiguredExecutionRuntime, |
| @@ -50,29 +48,49 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 6 unmodified lines | ||
| 50 | 48 | toolPaths: new Set<string>(), |
| 51 | 49 | }); |
| 52 | 50 | |
| 53 | −export const createWorkspaceExecutionEnvironmentResolver = (input: { | |
| 54 | − sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | |
| 55 | − sourceAuthService: Effect.Effect.Success<typeof RuntimeSourceAuthServiceTag>; | |
| 56 | − sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 57 | − localToolRuntimeLoader: LocalToolRuntimeLoaderShape; | |
| 58 | − workspaceConfigStore: WorkspaceConfigStoreShape; | |
| 59 | − workspaceStateStore: WorkspaceStateStoreShape; | |
| 60 | − sourceArtifactStore: SourceArtifactStoreShape; | |
| 61 | −}): ResolveExecutionEnvironment => | |
| 51 | +export const createWorkspaceExecutionEnvironmentResolver = | |
| 52 | + (input: { | |
| 53 | + controlPlaneStore: Effect.Effect.Success<typeof ControlPlaneStore>; | |
| 54 | + sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>; | |
| 55 | + sourceCatalogSyncService: Effect.Effect.Success< | |
| 56 | + typeof RuntimeSourceCatalogSyncService | |
| 57 | + >; | |
| 58 | + sourceAuthMaterialService: Effect.Effect.Success< | |
| 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 | 73 | ({ workspaceId, accountId, onElicitation }) => |
| 63 | 74 | Effect.gen(function* () { |
| 64 | 75 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 65 | 76 | const loadedConfig = |
| 66 | 77 | runtimeLocalWorkspace === null |
| 67 | 78 | ? null |
| 68 | − : yield* input.workspaceConfigStore.load(runtimeLocalWorkspace.context); | |
| 79 | + : yield* input.workspaceConfigStore.load( | |
| 80 | + runtimeLocalWorkspace.context, | |
| 81 | + ); | |
| 69 | 82 | const localToolRuntime = |
| 70 | 83 | runtimeLocalWorkspace === null |
| 71 | 84 | ? createEmptyLocalToolRuntime() |
| 72 | − : yield* input.localToolRuntimeLoader.load(runtimeLocalWorkspace.context); | |
| 85 | + : yield* input.localToolRuntimeLoader.load( | |
| 86 | + runtimeLocalWorkspace.context, | |
| 87 | + ); | |
| 73 | 88 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 74 | 89 | workspaceId, |
| 75 | 90 | accountId, |
| 91 | + controlPlaneStore: input.controlPlaneStore, | |
| 92 | + sourceStore: input.sourceStore, | |
| 93 | + sourceCatalogSyncService: input.sourceCatalogSyncService, | |
| 76 | 94 | sourceCatalogStore: input.sourceCatalogStore, |
| 77 | 95 | workspaceConfigStore: input.workspaceConfigStore, |
| 78 | 96 | workspaceStateStore: input.workspaceStateStore, |
| @@ -81,6 +99,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 2 unmodified lines | ||
| 81 | 99 | sourceAuthService: input.sourceAuthService, |
| 82 | 100 | runtimeLocalWorkspace, |
| 83 | 101 | localToolRuntime, |
| 102 | + createInternalToolMap: input.createInternalToolMap, | |
| 84 | 103 | onElicitation, |
| 85 | 104 | }); |
| 86 | 105 | |
| @@ -105,14 +124,20 @@ export class RuntimeExecutionResolverService extends Context.Tag( · 18 unmodified lines | ||
| 105 | 124 | export const RuntimeExecutionResolverLive = ( |
| 106 | 125 | input: { |
| 107 | 126 | executionResolver?: ResolveExecutionEnvironment; |
| 127 | + createInternalToolMap?: CreateWorkspaceInternalToolMap; | |
| 108 | 128 | } = {}, |
| 109 | 129 | ) => |
| 110 | 130 | input.executionResolver |
| 111 | 131 | ? Layer.succeed(RuntimeExecutionResolverService, input.executionResolver) |
| 112 | − : Layer.effect( | |
| 132 | + : Layer.effect( | |
| 113 | 133 | RuntimeExecutionResolverService, |
| 114 | 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 | 141 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 117 | 142 | const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService; |
| 118 | 143 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; |
| @@ -121,6 +146,9 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines | ||
| 121 | 146 | const sourceArtifactStore = yield* SourceArtifactStore; |
| 122 | 147 | |
| 123 | 148 | return createWorkspaceExecutionEnvironmentResolver({ |
| 149 | + controlPlaneStore, | |
| 150 | + sourceStore, | |
| 151 | + sourceCatalogSyncService, | |
| 124 | 152 | sourceAuthService, |
| 125 | 153 | sourceAuthMaterialService, |
| 126 | 154 | sourceCatalogStore, |
| @@ -128,6 +156,7 @@ export const RuntimeExecutionResolverLive = ( · 1 unmodified line | ||
| 128 | 156 | workspaceConfigStore, |
| 129 | 157 | workspaceStateStore, |
| 130 | 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 | 5 | mergeToolCatalogs, |
| 6 | 6 | mergeToolMaps, |
| 7 | 7 | type ToolCatalog, |
| 8 | + type ToolMap, | |
| 8 | 9 | type ToolInvoker, |
| 9 | 10 | } from "@executor/codemode-core"; |
| 10 | 11 | import type { AccountId, Source } from "#schema"; |
| @@ -13,19 +14,18 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines | ||
| 13 | 14 | import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material"; |
| 14 | 15 | import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime"; |
| 15 | 16 | import type { RuntimeLocalWorkspaceState } from "../../local/runtime-context"; |
| 16 | −import { | |
| 17 | − type LocalToolRuntime, | |
| 18 | −} from "../../local/tools"; | |
| 17 | +import { type LocalToolRuntime } from "../../local/tools"; | |
| 19 | 18 | import { |
| 20 | 19 | makeWorkspaceStorageLayer, |
| 21 | 20 | type SourceArtifactStoreShape, |
| 22 | 21 | type WorkspaceConfigStoreShape, |
| 23 | 22 | type WorkspaceStateStoreShape, |
| 24 | 23 | } from "../../local/storage"; |
| 25 | −import { | |
| 26 | − RuntimeSourceAuthService, | |
| 27 | −} from "../../sources/source-auth-service"; | |
| 24 | +import type { ControlPlaneStoreShape } from "../../store"; | |
| 25 | +import { RuntimeSourceAuthService } from "../../sources/source-auth-service"; | |
| 26 | +import { type RuntimeSourceStore } from "../../sources/source-store"; | |
| 28 | 27 | import { createExecutorToolMap } from "../../sources/executor-tools"; |
| 28 | +import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync"; | |
| 29 | 29 | import { invokeIrTool } from "../ir-execution"; |
| 30 | 30 | import { |
| 31 | 31 | authorizePersistedToolInvocation, |
| @@ -38,17 +38,46 @@ import { · 6 unmodified lines | ||
| 38 | 38 | } from "./source-catalog"; |
| 39 | 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 | 60 | export const createWorkspaceToolInvoker = (input: { |
| 42 | 61 | workspaceId: Source["workspaceId"]; |
| 43 | 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 | 71 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 46 | 72 | workspaceStateStore: WorkspaceStateStoreShape; |
| 47 | 73 | sourceArtifactStore: SourceArtifactStoreShape; |
| 48 | − sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | |
| 74 | + sourceAuthMaterialService: Effect.Effect.Success< | |
| 75 | + typeof RuntimeSourceAuthMaterialService | |
| 76 | + >; | |
| 49 | 77 | sourceAuthService: RuntimeSourceAuthService; |
| 50 | 78 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 51 | 79 | localToolRuntime: LocalToolRuntime; |
| 80 | + createInternalToolMap?: CreateWorkspaceInternalToolMap; | |
| 52 | 81 | onElicitation?: Parameters< |
| 53 | 82 | typeof makeToolInvokerFromTools |
| 54 | 83 | >[0]["onElicitation"]; |
| @@ -70,6 +99,19 @@ export const createWorkspaceToolInvoker = (input: { · 15 unmodified lines | ||
| 70 | 99 | sourceAuthService: input.sourceAuthService, |
| 71 | 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 | 115 | const sourceCatalog = createWorkspaceSourceCatalog({ |
| 74 | 116 | workspaceId: input.workspaceId, |
| 75 | 117 | accountId: input.accountId, |
| @@ -92,6 +134,7 @@ export const createWorkspaceToolInvoker = (input: { · 16 unmodified lines | ||
| 92 | 134 | const authoredTools = mergeToolMaps([ |
| 93 | 135 | systemTools, |
| 94 | 136 | executorTools, |
| 137 | + internalTools, | |
| 95 | 138 | input.localToolRuntime.tools, |
| 96 | 139 | ]); |
| 97 | 140 | const authoredCatalog = createToolCatalogFromTools({ |
| @@ -112,41 +155,46 @@ export const createWorkspaceToolInvoker = (input: { · 14 unmodified lines | ||
| 112 | 155 | context?: Record<string, unknown>; |
| 113 | 156 | }) => |
| 114 | 157 | provideRuntimeLocalWorkspace( |
| 115 | − provideWorkspaceStorage(Effect.gen(function* () { | |
| 116 | − const catalogTool = yield* loadWorkspaceCatalogToolByPath({ | |
| 117 | − workspaceId: input.workspaceId, | |
| 118 | − accountId: input.accountId, | |
| 119 | − sourceCatalogStore: input.sourceCatalogStore, | |
| 120 | − path: invocation.path, | |
| 121 | − includeSchemas: false, | |
| 122 | − }); | |
| 123 | − if (!catalogTool) { | |
| 124 | − return yield* runtimeEffectError("execution/workspace/tool-invoker", `Unknown tool path: ${invocation.path}`); | |
| 125 | − } | |
| 158 | + provideWorkspaceStorage( | |
| 159 | + Effect.gen(function* () { | |
| 160 | + const catalogTool = yield* loadWorkspaceCatalogToolByPath({ | |
| 161 | + workspaceId: input.workspaceId, | |
| 162 | + accountId: input.accountId, | |
| 163 | + sourceCatalogStore: input.sourceCatalogStore, | |
| 164 | + path: invocation.path, | |
| 165 | + includeSchemas: false, | |
| 166 | + }); | |
| 167 | + if (!catalogTool) { | |
| 168 | + return yield* runtimeEffectError( | |
| 169 | + "execution/workspace/tool-invoker", | |
| 170 | + `Unknown tool path: ${invocation.path}`, | |
| 171 | + ); | |
| 172 | + } | |
| 126 | 173 | |
| 127 | − yield* authorizePersistedToolInvocation({ | |
| 128 | − workspaceId: input.workspaceId, | |
| 129 | − tool: catalogTool, | |
| 130 | − args: invocation.args, | |
| 131 | − context: invocation.context, | |
| 132 | − onElicitation: input.onElicitation, | |
| 133 | − }); | |
| 174 | + yield* authorizePersistedToolInvocation({ | |
| 175 | + workspaceId: input.workspaceId, | |
| 176 | + tool: catalogTool, | |
| 177 | + args: invocation.args, | |
| 178 | + context: invocation.context, | |
| 179 | + onElicitation: input.onElicitation, | |
| 180 | + }); | |
| 134 | 181 | |
| 135 | − const auth = yield* input.sourceAuthMaterialService.resolve({ | |
| 136 | − source: catalogTool.source, | |
| 137 | − actorAccountId: input.accountId, | |
| 138 | − context: toSecretResolutionContext(invocation.context), | |
| 139 | − }); | |
| 140 | − return yield* invokeIrTool({ | |
| 141 | − workspaceId: input.workspaceId, | |
| 142 | − accountId: input.accountId, | |
| 143 | − tool: catalogTool, | |
| 144 | − auth, | |
| 145 | − args: invocation.args, | |
| 146 | − onElicitation: input.onElicitation, | |
| 147 | − context: invocation.context, | |
| 148 | − }); | |
| 149 | − })), | |
| 182 | + const auth = yield* input.sourceAuthMaterialService.resolve({ | |
| 183 | + source: catalogTool.source, | |
| 184 | + actorAccountId: input.accountId, | |
| 185 | + context: toSecretResolutionContext(invocation.context), | |
| 186 | + }); | |
| 187 | + return yield* invokeIrTool({ | |
| 188 | + workspaceId: input.workspaceId, | |
| 189 | + accountId: input.accountId, | |
| 190 | + tool: catalogTool, | |
| 191 | + auth, | |
| 192 | + args: invocation.args, | |
| 193 | + onElicitation: input.onElicitation, | |
| 194 | + context: invocation.context, | |
| 195 | + }); | |
| 196 | + }), | |
| 197 | + ), | |
| 150 | 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 | 6 | import * as Scope from "effect/Scope"; |
| 7 | 7 | import { NodeFileSystem } from "@effect/platform-node"; |
| 8 | 8 | import { clearAllMcpConnectionPools } from "@executor/source-mcp"; |
| 9 | − | |
| 10 | −import { type ControlPlaneApiRuntimeContext } from "#api"; | |
| 11 | 9 | import type { LocalInstallation } from "#schema"; |
| 12 | 10 | |
| 13 | 11 | import { type ResolveExecutionEnvironment } from "./execution/state"; |
| @@ -20,9 +18,7 @@ import { · 6 unmodified lines | ||
| 20 | 18 | type LocalControlPlanePersistence, |
| 21 | 19 | } from "./local/control-plane-store"; |
| 22 | 20 | |
| 23 | −import { | |
| 24 | − resolveLocalWorkspaceContext, | |
| 25 | −} from "./local/config"; | |
| 21 | +import { resolveLocalWorkspaceContext } from "./local/config"; | |
| 26 | 22 | import { |
| 27 | 23 | LocalStorageLive, |
| 28 | 24 | LocalInstallationStore, |
| @@ -40,36 +36,57 @@ import { RuntimeSourceCatalogStoreLive } from "./catalog/source/runtime"; · 11 unmodified lines | ||
| 40 | 36 | import { reconcileMissingSourceCatalogArtifacts } from "./catalog/source/reconcile"; |
| 41 | 37 | import { RuntimeSourceAuthMaterialLive } from "./auth/source-auth-material"; |
| 42 | 38 | import { RuntimeSourceCatalogSyncLive } from "./catalog/source/sync"; |
| 43 | −import { | |
| 44 | − RuntimeSourceAuthServiceLive, | |
| 45 | −} from "./sources/source-auth-service"; | |
| 39 | +import { RuntimeSourceAuthServiceLive } from "./sources/source-auth-service"; | |
| 46 | 40 | import type { ResolveSecretMaterial } from "./local/secret-material-providers"; |
| 47 | 41 | import { SecretMaterialLive } from "./local/secret-material-providers"; |
| 48 | −import { | |
| 49 | − RuntimeExecutionResolverLive, | |
| 50 | −} from "./execution/workspace/environment"; | |
| 42 | +import { RuntimeExecutionResolverLive } from "./execution/workspace/environment"; | |
| 43 | +import type { CreateWorkspaceInternalToolMap } from "./execution/workspace/tool-invoker"; | |
| 51 | 44 | |
| 52 | 45 | export * from "./execution/state"; |
| 53 | 46 | export * from "./sources/executor-tools"; |
| 54 | 47 | export * from "./execution/live"; |
| 55 | 48 | export * from "./local/config"; |
| 49 | +export * from "./local/errors"; | |
| 56 | 50 | export * from "./local/installation"; |
| 51 | +export * from "./local/runtime-context"; | |
| 57 | 52 | export * from "./local/storage"; |
| 58 | 53 | export * from "./local/source-artifacts"; |
| 59 | 54 | export * from "./local/tools"; |
| 60 | 55 | export * from "./catalog/schema-type-signature"; |
| 56 | +export * from "./catalog/source/runtime"; | |
| 57 | +export * from "./catalog/source/sync"; | |
| 61 | 58 | export * from "./sources/source-auth-service"; |
| 62 | 59 | export * from "./local/secret-material-providers"; |
| 63 | 60 | export * from "./sources/source-credential-interactions"; |
| 64 | 61 | export * from "./sources/source-adapters/mcp"; |
| 62 | +export * from "./sources/source-store"; | |
| 65 | 63 | export * from "./store"; |
| 66 | 64 | export * from "./execution/workspace/environment"; |
| 67 | −export * from "./sources/source-inspection"; | |
| 68 | −export * from "./sources/source-discovery"; | |
| 65 | +export * from "../sources/inspection"; | |
| 66 | +export * from "../sources/discovery"; | |
| 69 | 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 | 87 | export type RuntimeControlPlaneOptions = { |
| 72 | 88 | executionResolver?: ResolveExecutionEnvironment; |
| 89 | + createInternalToolMap?: CreateWorkspaceInternalToolMap; | |
| 73 | 90 | resolveSecretMaterial?: ResolveSecretMaterial; |
| 74 | 91 | getLocalServerBaseUrl?: () => string | undefined; |
| 75 | 92 | localDataDir?: string; |
| @@ -81,9 +98,7 @@ export type RuntimeControlPlaneOptions = { · 5 unmodified lines | ||
| 81 | 98 | const detailsFromCause = (cause: unknown): string => |
| 82 | 99 | cause instanceof Error ? cause.message : String(cause); |
| 83 | 100 | |
| 84 | −const toLocalRuntimeBootstrapError = ( | |
| 85 | − cause: unknown, | |
| 86 | −): Error => { | |
| 101 | +const toLocalRuntimeBootstrapError = (cause: unknown): Error => { | |
| 87 | 102 | const details = detailsFromCause(cause); |
| 88 | 103 | return new Error(`Failed initializing local runtime: ${details}`); |
| 89 | 104 | }; |
| @@ -91,11 +106,7 @@ const toLocalRuntimeBootstrapError = ( · 1 unmodified line | ||
| 91 | 106 | const closeScope = (scope: Scope.CloseableScope) => |
| 92 | 107 | Scope.close(scope, Exit.void).pipe(Effect.orDie); |
| 93 | 108 | |
| 94 | −export type RuntimeControlPlaneLayer = Layer.Layer< | |
| 95 | − ControlPlaneApiRuntimeContext, | |
| 96 | − never, | |
| 97 | − never | |
| 98 | −>; | |
| 109 | +export type RuntimeControlPlaneLayer = Layer.Layer<any, never, never>; | |
| 99 | 110 | |
| 100 | 111 | export const createRuntimeControlPlaneLayer = ( |
| 101 | 112 | input: RuntimeControlPlaneOptions & { |
| @@ -105,9 +116,7 @@ export const createRuntimeControlPlaneLayer = ( · 3 unmodified lines | ||
| 105 | 116 | }, |
| 106 | 117 | ) => { |
| 107 | 118 | const platformLayer = NodeFileSystem.layer; |
| 108 | − const storageLayer = LocalStorageLive.pipe( | |
| 109 | − Layer.provide(platformLayer), | |
| 110 | − ); | |
| 119 | + const storageLayer = LocalStorageLive.pipe(Layer.provide(platformLayer)); | |
| 111 | 120 | const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe( |
| 112 | 121 | Layer.provide(platformLayer), |
| 113 | 122 | ); |
| @@ -157,11 +166,13 @@ export const createRuntimeControlPlaneLayer = ( · 43 unmodified lines | ||
| 157 | 166 | |
| 158 | 167 | const executionResolverLayer = RuntimeExecutionResolverLive({ |
| 159 | 168 | executionResolver: input.executionResolver, |
| 169 | + createInternalToolMap: input.createInternalToolMap, | |
| 160 | 170 | }).pipe( |
| 161 | 171 | Layer.provide( |
| 162 | 172 | Layer.mergeAll( |
| 163 | 173 | baseLayer, |
| 164 | 174 | secretMaterialLayer, |
| 175 | + sourceStoreLayer, | |
| 165 | 176 | sourceAuthMaterialLayer, |
| 166 | 177 | sourceCatalogSyncLayer, |
| 167 | 178 | sourceAuthLayer, |
| @@ -187,7 +198,7 @@ export const createRuntimeControlPlaneLayer = ( · 19 unmodified lines | ||
| 187 | 198 | export type ControlPlaneRuntime = { |
| 188 | 199 | persistence: LocalControlPlanePersistence; |
| 189 | 200 | localInstallation: LocalInstallation; |
| 190 | − managedRuntime: ManagedRuntime.ManagedRuntime<ControlPlaneApiRuntimeContext, never>; | |
| 201 | + managedRuntime: ManagedRuntime.ManagedRuntime<any, never>; | |
| 191 | 202 | runtimeLayer: RuntimeControlPlaneLayer; |
| 192 | 203 | close: () => Promise<void>; |
| 193 | 204 | }; |
| @@ -197,13 +208,13 @@ export type CreateControlPlaneRuntimeOptions = RuntimeControlPlaneOptions; · 3 unmodified lines | ||
| 197 | 208 | export const provideControlPlaneRuntime = <A, E, R>( |
| 198 | 209 | effect: Effect.Effect<A, E, R>, |
| 199 | 210 | runtime: ControlPlaneRuntime, |
| 200 | −): Effect.Effect<A, E | never, Exclude<R, ControlPlaneApiRuntimeContext>> => | |
| 211 | +): Effect.Effect<A, E | never, any> => | |
| 201 | 212 | effect.pipe(Effect.provide(runtime.managedRuntime)); |
| 202 | 213 | |
| 203 | 214 | export const createControlPlaneRuntime = ( |
| 204 | 215 | options: CreateControlPlaneRuntimeOptions, |
| 205 | 216 | ): Effect.Effect<ControlPlaneRuntime, Error> => |
| 206 | − Effect.gen(function* () { | |
| 217 | + (Effect.gen(function* () { | |
| 207 | 218 | const scope = yield* Scope.make(); |
| 208 | 219 | |
| 209 | 220 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ |
| @@ -217,19 +228,20 @@ export const createControlPlaneRuntime = ( · 7 unmodified lines | ||
| 217 | 228 | ), |
| 218 | 229 | ); |
| 219 | 230 | |
| 220 | − | |
| 221 | 231 | const installationStore = LocalInstallationStore; |
| 222 | 232 | const workspaceConfigStore = LocalWorkspaceConfigStore; |
| 223 | 233 | const fileSystem = yield* FileSystem.FileSystem; |
| 224 | 234 | |
| 225 | − const localInstallation = yield* installationStore.getOrProvision({ | |
| 226 | − context: localWorkspaceContext, | |
| 227 | − }).pipe( | |
| 228 | − Effect.mapError(toLocalRuntimeBootstrapError), | |
| 229 | − Effect.catchAll((error) => | |
| 230 | − closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 231 | − ), | |
| 232 | − ); | |
| 235 | + const localInstallation = yield* installationStore | |
| 236 | + .getOrProvision({ | |
| 237 | + context: localWorkspaceContext, | |
| 238 | + }) | |
| 239 | + .pipe( | |
| 240 | + Effect.mapError(toLocalRuntimeBootstrapError), | |
| 241 | + Effect.catchAll((error) => | |
| 242 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 243 | + ), | |
| 244 | + ); | |
| 233 | 245 | |
| 234 | 246 | const persistence = createLocalControlPlanePersistence( |
| 235 | 247 | localWorkspaceContext, |
| @@ -237,14 +249,14 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | ||
| 237 | 249 | ); |
| 238 | 250 | const rows = persistence.rows; |
| 239 | 251 | |
| 240 | − const loadedLocalConfig = yield* workspaceConfigStore.load( | |
| 241 | − localWorkspaceContext, | |
| 242 | − ).pipe( | |
| 243 | − Effect.mapError(toLocalRuntimeBootstrapError), | |
| 244 | − Effect.catchAll((error) => | |
| 245 | − closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 246 | − ), | |
| 247 | − ); | |
| 252 | + const loadedLocalConfig = yield* workspaceConfigStore | |
| 253 | + .load(localWorkspaceContext) | |
| 254 | + .pipe( | |
| 255 | + Effect.mapError(toLocalRuntimeBootstrapError), | |
| 256 | + Effect.catchAll((error) => | |
| 257 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 258 | + ), | |
| 259 | + ); | |
| 248 | 260 | |
| 249 | 261 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ |
| 250 | 262 | context: localWorkspaceContext, |
| @@ -291,8 +303,13 @@ export const createControlPlaneRuntime = ( · 40 unmodified lines | ||
| 291 | 303 | managedRuntime, |
| 292 | 304 | runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer, |
| 293 | 305 | close: async () => { |
| 294 | − await Effect.runPromise(clearAllMcpConnectionPools()).catch(() => undefined); | |
| 306 | + await Effect.runPromise(clearAllMcpConnectionPools()).catch( | |
| 307 | + () => undefined, | |
| 308 | + ); | |
| 295 | 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 | 2 | ControlPlaneBadRequestError, |
| 3 | 3 | ControlPlaneNotFoundError, |
| 4 | 4 | ControlPlaneStorageError, |
| 5 | −} from "../../api/errors"; | |
| 5 | +} from "../../errors"; | |
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | |
| 8 | 8 | export type OperationErrors<TOperation extends string = string> = { |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | ControlPlaneBadRequestError, |
| 3 | 3 | ControlPlaneStorageError, |
| 4 | −} from "../../api/errors"; | |
| 4 | +} from "../../errors"; | |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | |
| 7 | 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 | 3 | import type { |
| 4 | 4 | CreateSourcePayload, |
| 5 | 5 | UpdateSourcePayload, |
| 6 | −} from "#api"; | |
| 6 | +} from "../../sources/contracts"; | |
| 7 | 7 | import type { |
| 8 | 8 | AccountId, |
| 9 | 9 | AuthArtifact, |
| @@ -26,7 +26,7 @@ import { · 16 unmodified lines | ||
| 26 | 26 | SourceCatalogRevisionIdSchema, |
| 27 | 27 | } from "#schema"; |
| 28 | 28 | import * as Effect from "effect/Effect"; |
| 29 | − | |
| 29 | + | |
| 30 | 30 | import { getSourceAdapter, getSourceAdapterForSource } from "./source-adapters"; |
| 31 | 31 | import { |
| 32 | 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 | 9 | type SourceDiscoveryResult, |
| 10 | 10 | } from "@executor/source-core"; |
| 11 | 11 | |
| 12 | −import { builtInSourceAdapters } from "./source-adapters"; | |
| 12 | +import { builtInSourceAdapters } from "../runtime/sources/source-adapters"; | |
| 13 | 13 | |
| 14 | 14 | type DiscoverableSourceAdapter = SourceAdapter & { |
| 15 | 15 | detectSource: NonNullable<SourceAdapter["detectSource"]>; |
| @@ -36,8 +36,8 @@ export const discoverSource = (input: { · 20 unmodified lines | ||
| 36 | 36 | |
| 37 | 37 | const adaptersByPriority = [...discoverableSourceAdapters].sort( |
| 38 | 38 | (left, right) => |
| 39 | − (right.discoveryPriority?.({ normalizedUrl }) ?? 0) | |
| 40 | − - (left.discoveryPriority?.({ normalizedUrl }) ?? 0), | |
| 39 | + (right.discoveryPriority?.({ normalizedUrl }) ?? 0) - | |
| 40 | + (left.discoveryPriority?.({ normalizedUrl }) ?? 0), | |
| 41 | 41 | ); |
| 42 | 42 | |
| 43 | 43 | for (const adapter of adaptersByPriority) { |
| @@ -9,23 +9,23 @@ import type { · 8 unmodified lines | ||
| 9 | 9 | SourceInspectionToolListItem, |
| 10 | 10 | SourceInspectionToolSummary, |
| 11 | 11 | WorkspaceId, |
| 12 | −} from "#schema"; | |
| 12 | +} from "../schema"; | |
| 13 | 13 | import { |
| 14 | 14 | ControlPlaneNotFoundError, |
| 15 | 15 | ControlPlaneStorageError, |
| 16 | −} from "../../api/errors"; | |
| 16 | +} from "../errors"; | |
| 17 | 17 | import * as Effect from "effect/Effect"; |
| 18 | 18 | |
| 19 | −import { LocalSourceArtifactMissingError } from "../local/errors"; | |
| 20 | −import { operationErrors } from "../policy/operation-errors"; | |
| 19 | +import { LocalSourceArtifactMissingError } from "../runtime/local/errors"; | |
| 20 | +import { operationErrors } from "../runtime/policy/operation-errors"; | |
| 21 | 21 | import { |
| 22 | 22 | buildLoadedSourceCatalogToolContract, |
| 23 | 23 | expandCatalogToolByPath, |
| 24 | 24 | expandCatalogTools, |
| 25 | 25 | loadSourceWithCatalog, |
| 26 | 26 | type LoadedSourceCatalogTool, |
| 27 | −} from "../catalog/source/runtime"; | |
| 28 | −import { RuntimeSourceStoreService } from "./source-store"; | |
| 27 | +} from "../runtime/catalog/source/runtime"; | |
| 28 | +import { RuntimeSourceStoreService } from "../runtime/sources/source-store"; | |
| 29 | 29 | |
| 30 | 30 | const sourceInspectOps = { |
| 31 | 31 | bundle: operationErrors("sources.inspect.bundle"), |
| @@ -41,9 +41,9 @@ const tokenize = (value: string): Array<string> => · 9 unmodified lines | ||
| 41 | 41 | .filter(Boolean); |
| 42 | 42 | |
| 43 | 43 | const canInspectSourceWithoutCatalog = (source: Source): boolean => |
| 44 | − source.status === "draft" | |
| 45 | − || source.status === "probing" | |
| 46 | − || source.status === "auth_required"; | |
| 44 | + source.status === "draft" || | |
| 45 | + source.status === "probing" || | |
| 46 | + source.status === "auth_required"; | |
| 47 | 47 | |
| 48 | 48 | const loadSourceForMissingCatalog = (input: { |
| 49 | 49 | workspaceId: WorkspaceId; |
| @@ -88,7 +88,8 @@ const loadSourceCatalogOrEmpty = (input: { · 38 unmodified lines | ||
| 88 | 88 | kind: "empty" as const, |
| 89 | 89 | source, |
| 90 | 90 | }; |
| 91 | − })), | |
| 91 | + }), | |
| 92 | + ), | |
| 92 | 93 | ); |
| 93 | 94 | |
| 94 | 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 | 112 | path: tool.path, |
| 110 | 113 | method: executableDetails(tool).method, |
| 111 | 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 | 125 | const details = executableDetails(tool); |
| 121 | 126 | |
| 122 | 127 | return { |
| 123 | 128 | path: tool.path, |
| 124 | 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 | 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 | 140 | protocol: details.protocol, |
| 130 | 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 | 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 | 163 | ? encoding |
| 150 | 164 | : "json"; |
| 151 | 165 | |
| @@ -171,12 +185,24 @@ export const inspectionToolDetailFromTool = ( · 19 unmodified lines | ||
| 171 | 185 | const contract = yield* buildLoadedSourceCatalogToolContract(tool); |
| 172 | 186 | const overviewItems = [ |
| 173 | 187 | { label: "Protocol", value: details.protocol }, |
| 174 | − ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []), | |
| 175 | − ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []), | |
| 176 | − ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []), | |
| 177 | − ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []), | |
| 178 | − ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []), | |
| 179 | − ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []), | |
| 188 | + ...(details.method | |
| 189 | + ? [{ label: "Method", value: details.method, mono: true }] | |
| 190 | + : []), | |
| 191 | + ...(details.pathTemplate | |
| 192 | + ? [{ label: "Target", value: details.pathTemplate, 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 | 206 | { label: "Signature", value: contract.callSignature, mono: true }, |
| 181 | 207 | { label: "Call shape", value: contract.callShapeId, mono: true }, |
| 182 | 208 | ...(contract.resultShapeId |
| @@ -211,11 +237,13 @@ export const inspectionToolDetailFromTool = ( · 28 unmodified lines | ||
| 211 | 237 | items: overviewItems, |
| 212 | 238 | }, |
| 213 | 239 | ...(summary.description |
| 214 | − ? [{ | |
| 215 | − kind: "markdown" as const, | |
| 216 | − title: "Description", | |
| 217 | − body: summary.description, | |
| 218 | − }] | |
| 240 | + ? [ | |
| 241 | + { | |
| 242 | + kind: "markdown" as const, | |
| 243 | + title: "Description", | |
| 244 | + body: summary.description, | |
| 245 | + }, | |
| 246 | + ] | |
| 219 | 247 | : []), |
| 220 | 248 | ...([ |
| 221 | 249 | jsonSection("Capability", tool.capability), |
| @@ -231,7 +259,9 @@ export const inspectionToolDetailFromTool = ( · 9 unmodified lines | ||
| 231 | 259 | summary: tool.capability.surface.summary, |
| 232 | 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 | 265 | ...nativeSections, |
| 236 | 266 | ]; |
| 237 | 267 | |
| @@ -323,7 +353,9 @@ const scoreTool = (input: { · 85 unmodified lines | ||
| 323 | 353 | const titleTokens = tokenize(summary.title ?? ""); |
| 324 | 354 | const descriptionTokens = tokenize(summary.description ?? ""); |
| 325 | 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 | 360 | for (const token of input.queryTokens) { |
| 329 | 361 | if (pathTokens.includes(token)) { |
| @@ -346,7 +378,10 @@ const scoreTool = (input: { · 16 unmodified lines | ||
| 346 | 378 | reasons.push(`method/path matches ${token} (+6)`); |
| 347 | 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 | 385 | score += 2; |
| 351 | 386 | reasons.push(`description/text matches ${token} (+2)`); |
| 352 | 387 | } |
| @@ -360,8 +395,12 @@ const scoreTool = (input: { · 7 unmodified lines | ||
| 360 | 395 | path: input.tool.path, |
| 361 | 396 | score, |
| 362 | 397 | ...(summary.description ? { description: summary.description } : {}), |
| 363 | − ...(summary.inputTypePreview ? { inputTypePreview: summary.inputTypePreview } : {}), | |
| 364 | − ...(summary.outputTypePreview ? { outputTypePreview: summary.outputTypePreview } : {}), | |
| 398 | + ...(summary.inputTypePreview | |
| 399 | + ? { inputTypePreview: summary.inputTypePreview } | |
| 400 | + : {}), | |
| 401 | + ...(summary.outputTypePreview | |
| 402 | + ? { outputTypePreview: summary.outputTypePreview } | |
| 403 | + : {}), | |
| 365 | 404 | reasons, |
| 366 | 405 | }; |
| 367 | 406 | }; |
| @@ -402,7 +441,9 @@ export const getSourceInspection = (input: { · 34 unmodified lines | ||
| 402 | 441 | namespace: inspection.namespace, |
| 403 | 442 | pipelineKind: inspection.pipelineKind, |
| 404 | 443 | toolCount: inspection.tools.length, |
| 405 | − tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)), | |
| 444 | + tools: inspection.tools.map((tool) => | |
| 445 | + inspectionToolListItemFromTool(tool), | |
| 446 | + ), | |
| 406 | 447 | } satisfies SourceInspection; |
| 407 | 448 | }).pipe( |
| 408 | 449 | Effect.mapError((cause) => |
| @@ -410,7 +451,8 @@ export const getSourceInspection = (input: { · 1 unmodified line | ||
| 410 | 451 | sourceInspectOps.bundle, |
| 411 | 452 | cause, |
| 412 | 453 | "Failed building source inspection bundle", |
| 413 | − )), | |
| 454 | + ), | |
| 455 | + ), | |
| 414 | 456 | ); |
| 415 | 457 | |
| 416 | 458 | export const getSourceInspectionToolDetail = (input: { |
| @@ -428,9 +470,9 @@ export const getSourceInspectionToolDetail = (input: { · 11 unmodified lines | ||
| 428 | 470 | |
| 429 | 471 | if (!tool) { |
| 430 | 472 | return yield* sourceInspectOps.tool.notFound( |
| 431 | − "Tool not found", | |
| 432 | − `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, | |
| 433 | − ); | |
| 473 | + "Tool not found", | |
| 474 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, | |
| 475 | + ); | |
| 434 | 476 | } |
| 435 | 477 | |
| 436 | 478 | return yield* inspectionToolDetailFromTool(tool); |
| @@ -440,7 +482,8 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines | ||
| 440 | 482 | sourceInspectOps.tool, |
| 441 | 483 | cause, |
| 442 | 484 | "Failed building source inspection tool detail", |
| 443 | − )), | |
| 485 | + ), | |
| 486 | + ), | |
| 444 | 487 | ); |
| 445 | 488 | |
| 446 | 489 | export const discoverSourceInspectionTools = (input: { |
| @@ -463,9 +506,12 @@ export const discoverSourceInspectionTools = (input: { · 16 unmodified lines | ||
| 463 | 506 | tool, |
| 464 | 507 | }), |
| 465 | 508 | ) |
| 466 | − .filter((value): value is SourceInspectionDiscoverResultItem => value !== null) | |
| 467 | − .sort((left, right) => | |
| 468 | − right.score - left.score || left.path.localeCompare(right.path), | |
| 509 | + .filter( | |
| 510 | + (value): value is SourceInspectionDiscoverResultItem => value !== null, | |
| 511 | + ) | |
| 512 | + .sort( | |
| 513 | + (left, right) => | |
| 514 | + right.score - left.score || left.path.localeCompare(right.path), | |
| 469 | 515 | ) |
| 470 | 516 | .slice(0, input.payload.limit ?? 12); |
| 471 | 517 | |
| @@ -482,5 +528,6 @@ export const discoverSourceInspectionTools = (input: { · 10 unmodified lines | ||
| 482 | 528 | sourceInspectOps.discover, |
| 483 | 529 | cause, |
| 484 | 530 | "Failed building source inspection discovery", |
| 485 | − )), | |
| 531 | + ), | |
| 532 | + ), | |
| 486 | 533 | ); |
| @@ -1,33 +1,30 @@ | ||
| 1 | 1 | import type { |
| 2 | 2 | CreateSourcePayload, |
| 3 | 3 | UpdateSourcePayload, |
| 4 | −} from "../../api/sources/api"; | |
| 4 | +} from "./contracts"; | |
| 5 | 5 | import { |
| 6 | 6 | type AccountId, |
| 7 | 7 | SourceIdSchema, |
| 8 | 8 | type Source, |
| 9 | 9 | type SourceId, |
| 10 | 10 | type WorkspaceId, |
| 11 | −} from "#schema"; | |
| 11 | +} from "../schema"; | |
| 12 | 12 | import * as Either from "effect/Either"; |
| 13 | 13 | import * as Effect from "effect/Effect"; |
| 14 | 14 | |
| 15 | 15 | import { |
| 16 | 16 | createSourceFromPayload, |
| 17 | 17 | updateSourceFromPayload, |
| 18 | −} from "./source-definitions"; | |
| 19 | −import { getSourceAdapterForSource } from "./source-adapters"; | |
| 18 | +} from "../runtime/sources/source-definitions"; | |
| 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 | 22 | import { |
| 21 | − mapPersistenceError, | |
| 22 | −} from "../policy/operations-shared"; | |
| 23 | −import { | |
| 24 | − operationErrors, | |
| 25 | −} from "../policy/operation-errors"; | |
| 26 | −import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store"; | |
| 27 | −import { RuntimeSourceCatalogSyncService } from "../catalog/source/sync"; | |
| 28 | −import { | |
| 29 | − RuntimeSourceStoreService, | |
| 30 | −} from "./source-store"; | |
| 23 | + ControlPlaneStore, | |
| 24 | + type ControlPlaneStoreShape, | |
| 25 | +} from "../runtime/store"; | |
| 26 | +import { RuntimeSourceCatalogSyncService } from "../runtime/catalog/source/sync"; | |
| 27 | +import { RuntimeSourceStoreService } from "../runtime/sources/source-store"; | |
| 31 | 28 | |
| 32 | 29 | const sourceOps = { |
| 33 | 30 | list: operationErrors("sources.list"), |
| @@ -45,9 +42,7 @@ const syncArtifactsForSource = (input: { · 11 unmodified lines | ||
| 45 | 42 | sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>; |
| 46 | 43 | source: Source; |
| 47 | 44 | actorAccountId: AccountId; |
| 48 | − operation: | |
| 49 | − | typeof sourceOps.create | |
| 50 | − | typeof sourceOps.update; | |
| 45 | + operation: typeof sourceOps.create | typeof sourceOps.update; | |
| 51 | 46 | }) => |
| 52 | 47 | Effect.gen(function* () { |
| 53 | 48 | const catalogSyncService = yield* RuntimeSourceCatalogSyncService; |
| @@ -96,7 +91,10 @@ const syncArtifactsForSource = (input: { · 42 unmodified lines | ||
| 96 | 91 | }), |
| 97 | 92 | onLeft: (error) => |
| 98 | 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 | 98 | const erroredSource = yield* updateSourceFromPayload({ |
| 101 | 99 | source: input.source, |
| 102 | 100 | payload: { |
| @@ -104,11 +102,11 @@ const syncArtifactsForSource = (input: { · 1 unmodified line | ||
| 104 | 102 | lastError: error.message, |
| 105 | 103 | }, |
| 106 | 104 | now: Date.now(), |
| 107 | − }).pipe( | |
| 108 | − Effect.mapError((cause) => | |
| 109 | − input.operation.badRequest( | |
| 110 | − "Failed indexing source tools", | |
| 111 | − cause instanceof Error ? cause.message : String(cause), | |
| 105 | + }).pipe( | |
| 106 | + Effect.mapError((cause) => | |
| 107 | + input.operation.badRequest( | |
| 108 | + "Failed indexing source tools", | |
| 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 | 135 | Effect.gen(function* () { |
| 135 | 136 | const sourceStore = yield* RuntimeSourceStoreService; |
| 136 | 137 | |
| 137 | − return yield* sourceStore.loadSourcesInWorkspace(input.workspaceId, { | |
| 138 | − actorAccountId: input.accountId, | |
| 139 | − }).pipe( | |
| 140 | − Effect.mapError((error) => | |
| 141 | − sourceOps.list.unknownStorage( | |
| 142 | − error, | |
| 143 | − "Failed projecting stored sources", | |
| 138 | + return yield* sourceStore | |
| 139 | + .loadSourcesInWorkspace(input.workspaceId, { | |
| 140 | + actorAccountId: input.accountId, | |
| 141 | + }) | |
| 142 | + .pipe( | |
| 143 | + Effect.mapError((error) => | |
| 144 | + sourceOps.list.unknownStorage( | |
| 145 | + error, | |
| 146 | + "Failed projecting stored sources", | |
| 147 | + ), | |
| 144 | 148 | ), |
| 145 | − ), | |
| 146 | − ); | |
| 147 | − })); | |
| 149 | + ); | |
| 150 | + }), | |
| 151 | + ); | |
| 148 | 152 | |
| 149 | 153 | export const createSource = (input: { |
| 150 | 154 | workspaceId: WorkspaceId; |
| @@ -186,7 +190,8 @@ export const createSource = (input: { · 35 unmodified lines | ||
| 186 | 190 | }); |
| 187 | 191 | |
| 188 | 192 | return synchronizedSource; |
| 189 | − })); | |
| 193 | + }), | |
| 194 | + ); | |
| 190 | 195 | |
| 191 | 196 | export const getSource = (input: { |
| 192 | 197 | workspaceId: WorkspaceId; |
| @@ -197,24 +202,28 @@ export const getSource = (input: { · 4 unmodified lines | ||
| 197 | 202 | Effect.gen(function* () { |
| 198 | 203 | const sourceStore = yield* RuntimeSourceStoreService; |
| 199 | 204 | |
| 200 | − return yield* sourceStore.loadSourceById({ | |
| 201 | − workspaceId: input.workspaceId, | |
| 202 | − sourceId: input.sourceId, | |
| 203 | − actorAccountId: input.accountId, | |
| 204 | − }).pipe( | |
| 205 | − Effect.mapError((cause) => | |
| 206 | − cause instanceof Error && cause.message.startsWith("Source not found:") | |
| 207 | − ? sourceOps.get.notFound( | |
| 208 | − "Source not found", | |
| 209 | − `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | |
| 210 | − ) | |
| 211 | − : sourceOps.get.unknownStorage( | |
| 212 | − cause, | |
| 213 | − "Failed projecting stored source", | |
| 214 | − ), | |
| 215 | − ), | |
| 216 | − ); | |
| 217 | − })); | |
| 205 | + return yield* sourceStore | |
| 206 | + .loadSourceById({ | |
| 207 | + workspaceId: input.workspaceId, | |
| 208 | + sourceId: input.sourceId, | |
| 209 | + actorAccountId: input.accountId, | |
| 210 | + }) | |
| 211 | + .pipe( | |
| 212 | + Effect.mapError((cause) => | |
| 213 | + cause instanceof Error && | |
| 214 | + cause.message.startsWith("Source not found:") | |
| 215 | + ? sourceOps.get.notFound( | |
| 216 | + "Source not found", | |
| 217 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | |
| 218 | + ) | |
| 219 | + : sourceOps.get.unknownStorage( | |
| 220 | + cause, | |
| 221 | + "Failed projecting stored source", | |
| 222 | + ), | |
| 223 | + ), | |
| 224 | + ); | |
| 225 | + }), | |
| 226 | + ); | |
| 218 | 227 | |
| 219 | 228 | export const updateSource = (input: { |
| 220 | 229 | workspaceId: WorkspaceId; |
| @@ -225,23 +234,26 @@ export const updateSource = (input: { · 4 unmodified lines | ||
| 225 | 234 | Effect.flatMap(ControlPlaneStore, (store) => |
| 226 | 235 | Effect.gen(function* () { |
| 227 | 236 | const sourceStore = yield* RuntimeSourceStoreService; |
| 228 | − const existingSource = yield* sourceStore.loadSourceById({ | |
| 229 | − workspaceId: input.workspaceId, | |
| 230 | − sourceId: input.sourceId, | |
| 231 | − actorAccountId: input.accountId, | |
| 232 | − }).pipe( | |
| 233 | − Effect.mapError((cause) => | |
| 234 | − cause instanceof Error && cause.message.startsWith("Source not found:") | |
| 235 | − ? sourceOps.update.notFound( | |
| 236 | − "Source not found", | |
| 237 | − `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | |
| 238 | − ) | |
| 239 | − : sourceOps.update.unknownStorage( | |
| 240 | − cause, | |
| 241 | − "Failed projecting stored source", | |
| 242 | − ), | |
| 243 | − ), | |
| 244 | − ); | |
| 237 | + const existingSource = yield* sourceStore | |
| 238 | + .loadSourceById({ | |
| 239 | + workspaceId: input.workspaceId, | |
| 240 | + sourceId: input.sourceId, | |
| 241 | + actorAccountId: input.accountId, | |
| 242 | + }) | |
| 243 | + .pipe( | |
| 244 | + Effect.mapError((cause) => | |
| 245 | + cause instanceof Error && | |
| 246 | + cause.message.startsWith("Source not found:") | |
| 247 | + ? sourceOps.update.notFound( | |
| 248 | + "Source not found", | |
| 249 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | |
| 250 | + ) | |
| 251 | + : sourceOps.update.unknownStorage( | |
| 252 | + cause, | |
| 253 | + "Failed projecting stored source", | |
| 254 | + ), | |
| 255 | + ), | |
| 256 | + ); | |
| 245 | 257 | |
| 246 | 258 | const updatedSource = yield* updateSourceFromPayload({ |
| 247 | 259 | source: existingSource, |
| @@ -272,7 +284,8 @@ export const updateSource = (input: { · 24 unmodified lines | ||
| 272 | 284 | }); |
| 273 | 285 | |
| 274 | 286 | return synchronizedSource; |
| 275 | − })); | |
| 287 | + }), | |
| 288 | + ); | |
| 276 | 289 | |
| 277 | 290 | export const removeSource = (input: { |
| 278 | 291 | workspaceId: WorkspaceId; |
| @@ -290,5 +303,5 @@ export const removeSource = (input: { · 11 unmodified lines | ||
| 290 | 303 | ); |
| 291 | 304 | |
| 292 | 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 | 12 | "@effect/opentelemetry": "^0.61.0", |
| 13 | 13 | "@effect/platform": "catalog:", |
| 14 | 14 | "@effect/platform-node": "catalog:", |
| 15 | − "@executor/control-plane": "workspace:*", | |
| 16 | 15 | "@executor/executor-mcp": "workspace:*", |
| 16 | + "@executor/platform-api": "workspace:*", | |
| 17 | + "@executor/platform-internal": "workspace:*", | |
| 18 | + "@executor/platform-sdk": "workspace:*", | |
| 17 | 19 | "@opentelemetry/api": "^1.9.0", |
| 18 | 20 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", |
| 19 | 21 | "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", |
| @@ -10,12 +10,15 @@ import { FileSystem, HttpApiBuilder, HttpServer } from "@effect/platform"; · 9 unmodified lines | ||
| 10 | 10 | import { NodeFileSystem } from "@effect/platform-node"; |
| 11 | 11 | import { |
| 12 | 12 | createControlPlaneApiLayer, |
| 13 | +} from "@executor/platform-api"; | |
| 14 | +import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; | |
| 15 | +import { createExecutorAdminToolMap } from "@executor/platform-internal"; | |
| 16 | +import { | |
| 13 | 17 | createControlPlaneRuntime, |
| 14 | 18 | type ResolveExecutionEnvironment, |
| 15 | 19 | type ResolveSecretMaterial, |
| 16 | 20 | type ControlPlaneRuntime, |
| 17 | −} from "@executor/control-plane"; | |
| 18 | −import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; | |
| 21 | +} from "@executor/platform-sdk/runtime"; | |
| 19 | 22 | import * as Effect from "effect/Effect"; |
| 20 | 23 | import * as Layer from "effect/Layer"; |
| 21 | 24 | import type * as Scope from "effect/Scope"; |
| @@ -110,6 +113,7 @@ const createRuntime = ( · 88 unmodified lines | ||
| 110 | 113 | createControlPlaneRuntime({ |
| 111 | 114 | workspaceRoot: options.workspaceRoot, |
| 112 | 115 | executionResolver: options.executionResolver, |
| 116 | + createInternalToolMap: createExecutorAdminToolMap, | |
| 113 | 117 | resolveSecretMaterial: options.resolveSecretMaterial, |
| 114 | 118 | getLocalServerBaseUrl, |
| 115 | 119 | localDataDir, |
| @@ -20,7 +20,7 @@ const readFixture = (name: string) => · 19 unmodified lines | ||
| 20 | 20 | Effect.flatMap((fs) => |
| 21 | 21 | fs.readFileString( |
| 22 | 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 | 25 | "utf8", |
| 26 | 26 | ) |
| @@ -15,7 +15,7 @@ const readFixture = (name: string) => · 14 unmodified lines | ||
| 15 | 15 | Effect.flatMap((fs) => |
| 16 | 16 | fs.readFileString( |
| 17 | 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 | 20 | "utf8", |
| 21 | 21 | ) |