History

Refactor platform core into sdk api and internal packages

Rhys Sullivan committed 7 months ago

Commit d03db821 parent 747f7a3205 files · +4875 −3170Browse files

Unified · Split

205 files
  1. .executor/executor.jsonc +5 −21
  2. ARCHITECTURE.md +2 −2
  3. README.md +4 −2
  4. apps/docs/developer/cli.mdx +1 −1
  5. apps/docs/developer/overview.mdx +1 −1
  6. apps/executor/package.json +3 −1
  7. apps/executor/src/cli/dev.ts +2 −4
  8. apps/executor/src/cli/main.ts +11 −5
  9. apps/executor/src/cli/pending-interaction-output.test.ts +1 −1
  10. apps/executor/src/cli/pending-interaction-output.ts +1 −1
  11. apps/executor/src/server/server.real-ingestion.test.ts +2 −2
  12. apps/executor/src/server/server.test.ts +5 −3
  13. bun.lock +56 −17
  14. package.json +1 −1
  15. packages/clients/react/package.json +2 −1
  16. packages/clients/react/src/index.test.tsx +7 −3
  17. packages/clients/react/src/index.ts +24 −22
  18. packages/dev/kitchen-sink/package.json +2 −2
  19. packages/dev/kitchen-sink/src/source-runtime.test.ts +1 −1
  20. packages/hosts/mcp/package.json +1 −1
  21. packages/hosts/mcp/src/index.ts +4 −4
  22. packages/hosts/mcp/src/paused-result.test.ts +4 −1
  23. packages/hosts/mcp/src/paused-result.ts +1 −1
  24. packages/platform/api/package.json +40 −0
  25. packages/platform/api/src/api.ts +0 −0
  26. packages/platform/api/src/client.ts +1 −1
  27. packages/platform/api/src/errors.ts +7 −0
  28. packages/platform/api/src/executions/api.ts +14 −17
  29. packages/platform/api/src/executions/http.ts +1 −1
  30. packages/platform/api/src/http.ts +0 −0
  31. packages/platform/api/src/index.ts +19 −1
  32. packages/platform/api/src/local-context.ts +3 −3
  33. packages/platform/api/src/local/api.ts +69 −0
  34. packages/platform/api/src/local/http.ts +29 −0
  35. packages/platform/api/src/oauth/api.ts +3 −3
  36. packages/platform/api/src/oauth/http.ts +2 −2
  37. packages/platform/api/src/payload-schemas.test.ts +0 −0
  38. packages/platform/api/src/policies/api.ts +18 −24
  39. packages/platform/api/src/policies/http.ts +14 −7
  40. packages/platform/api/src/sources/api.ts +43 −162
  41. packages/platform/api/src/sources/http.ts +170 −96
  42. packages/platform/api/src/string-schemas.ts +4 −0
  43. packages/platform/api/tsconfig.json +1 −8
  44. packages/platform/api/vitest.config.ts +0 −0
  45. packages/platform/control-plane/CHANGELOG.md +0 −13
  46. packages/platform/control-plane/src/api/local/http.ts +0 −255
  47. packages/platform/control-plane/src/index.ts +0 −4
  48. packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts +0 −1991
  49. packages/platform/internal/package.json +22 −0
  50. packages/platform/internal/src/index.ts +1 −0
  51. packages/platform/internal/src/tools/executor-admin.ts +692 −0
  52. packages/platform/internal/tsconfig.json +13 −0
  53. packages/platform/sdk/package.json +36 −25
  54. packages/platform/sdk/src/errors.ts +0 −0
  55. packages/platform/sdk/src/executions/contracts.ts +21 −0
  56. packages/platform/sdk/src/index.ts +12 −0
  57. packages/platform/sdk/src/local/contracts.ts +1 −52
  58. packages/platform/sdk/src/local/operations.ts +112 −94
  59. packages/platform/sdk/src/local/secrets.ts +283 −0
  60. packages/platform/sdk/src/policies/contracts.ts +23 −0
  61. packages/platform/sdk/src/policies/operations.ts +126 −72
  62. packages/platform/sdk/src/runtime/__fixtures__/README.md +1 −1
  63. packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar.json +0 −0
  64. packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar/documents/doc_3c24a6731267de91.txt +0 −0
  65. packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/executor.jsonc +0 −0
  66. packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/state/workspace-state.json +0 −0
  67. packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/fixture.json +0 −0
  68. packages/platform/sdk/src/runtime/auth/auth-artifacts.ts +0 −0
  69. packages/platform/sdk/src/runtime/auth/auth-leases.ts +0 −0
  70. packages/platform/sdk/src/runtime/auth/mcp-auth-provider.ts +0 −0
  71. packages/platform/sdk/src/runtime/auth/mcp-oauth.ts +0 −0
  72. packages/platform/sdk/src/runtime/auth/oauth-loopback.test.ts +0 −0
  73. packages/platform/sdk/src/runtime/auth/oauth-loopback.ts +0 −0
  74. packages/platform/sdk/src/runtime/auth/oauth2-pkce.ts +0 −0
  75. packages/platform/sdk/src/runtime/auth/provider-grant-lifecycle.ts +0 −0
  76. packages/platform/sdk/src/runtime/auth/source-auth-material.test.ts +0 −0
  77. packages/platform/sdk/src/runtime/auth/source-auth-material.ts +0 −0
  78. packages/platform/sdk/src/runtime/catalog/catalog-typescript.ts +0 −0
  79. packages/platform/sdk/src/runtime/catalog/prettier-format.ts +0 −0
  80. packages/platform/sdk/src/runtime/catalog/schema-type-signature.ts +0 −0
  81. packages/platform/sdk/src/runtime/catalog/source/reconcile.ts +0 −0
  82. packages/platform/sdk/src/runtime/catalog/source/runtime.test.ts +0 −0
  83. packages/platform/sdk/src/runtime/catalog/source/runtime.ts +0 −0
  84. packages/platform/sdk/src/runtime/catalog/source/snapshot.test.ts +0 −0
  85. packages/platform/sdk/src/runtime/catalog/source/sync.ts +0 −0
  86. packages/platform/sdk/src/runtime/catalog/source/type-declarations.test.ts +0 −0
  87. packages/platform/sdk/src/runtime/catalog/source/type-declarations.ts +0 −0
  88. packages/platform/sdk/src/runtime/control-plane-runtime.test.ts +2356 −0
  89. packages/platform/sdk/src/runtime/effect-errors.ts +0 −0
  90. packages/platform/sdk/src/runtime/execution/http.test.ts +0 −0
  91. packages/platform/sdk/src/runtime/execution/ir-execution.ts +0 −0
  92. packages/platform/sdk/src/runtime/execution/live.test.ts +0 −0
  93. packages/platform/sdk/src/runtime/execution/live.ts +0 −0
  94. packages/platform/sdk/src/runtime/execution/mcp-resume.test.ts +0 −0
  95. packages/platform/sdk/src/runtime/execution/runtime-config.test.ts +0 −0
  96. packages/platform/sdk/src/runtime/execution/runtime.ts +0 −0
  97. packages/platform/sdk/src/runtime/execution/service.ts +2 −2
  98. packages/platform/sdk/src/runtime/execution/state.ts +0 −0
  99. packages/platform/sdk/src/runtime/execution/test-http-client.ts +3 −4
  100. packages/platform/sdk/src/runtime/execution/workspace/authorization.ts +0 −0
  101. packages/platform/sdk/src/runtime/execution/workspace/environment.ts +51 −22
  102. packages/platform/sdk/src/runtime/execution/workspace/local.ts +0 −0
  103. packages/platform/sdk/src/runtime/execution/workspace/source-catalog.ts +0 −0
  104. packages/platform/sdk/src/runtime/execution/workspace/tool-invoker.ts +89 −41
  105. packages/platform/sdk/src/runtime/fixtures/google-sheets-discovery.json +0 −0
  106. packages/platform/sdk/src/runtime/fixtures/linear-introspection.json +0 −0
  107. packages/platform/sdk/src/runtime/fixtures/neon-openapi.json +0 −0
  108. packages/platform/sdk/src/runtime/fixtures/vercel-openapi.json +0 −0
  109. packages/platform/sdk/src/runtime/index.ts +63 −46
  110. packages/platform/sdk/src/runtime/local/capture-release-workspace-fixture.ts +0 −0
  111. packages/platform/sdk/src/runtime/local/config-secrets.ts +0 −0
  112. packages/platform/sdk/src/runtime/local/config.test.ts +0 −0
  113. packages/platform/sdk/src/runtime/local/config.ts +0 −0
  114. packages/platform/sdk/src/runtime/local/control-plane-store.test.ts +0 −0
  115. packages/platform/sdk/src/runtime/local/control-plane-store.ts +0 −0
  116. packages/platform/sdk/src/runtime/local/errors.ts +0 −0
  117. packages/platform/sdk/src/runtime/local/installation.test.ts +0 −0
  118. packages/platform/sdk/src/runtime/local/installation.ts +0 −0
  119. packages/platform/sdk/src/runtime/local/operations.ts +1 −0
  120. packages/platform/sdk/src/runtime/local/release-upgrade-fixtures.ts +0 −0
  121. packages/platform/sdk/src/runtime/local/runtime-context.ts +0 −0
  122. packages/platform/sdk/src/runtime/local/secret-material-providers.ts +0 −0
  123. packages/platform/sdk/src/runtime/local/secrets-operations.ts +1 −0
  124. packages/platform/sdk/src/runtime/local/source-artifacts.test.ts +0 −0
  125. packages/platform/sdk/src/runtime/local/source-artifacts.ts +0 −0
  126. packages/platform/sdk/src/runtime/local/storage.ts +0 −0
  127. packages/platform/sdk/src/runtime/local/tools.ts +0 −0
  128. packages/platform/sdk/src/runtime/local/workspace-state.ts +0 −0
  129. packages/platform/sdk/src/runtime/local/workspace-sync.ts +0 −0
  130. packages/platform/sdk/src/runtime/policy/invocation-policy-engine.test.ts +0 −0
  131. packages/platform/sdk/src/runtime/policy/invocation-policy-engine.ts +0 −0
  132. packages/platform/sdk/src/runtime/policy/operation-errors.ts +1 −1
  133. packages/platform/sdk/src/runtime/policy/operations-shared.ts +1 −1
  134. packages/platform/sdk/src/runtime/policy/policies-operations.ts +1 −0
  135. packages/platform/sdk/src/runtime/sources/catalog-sync-result.ts +0 −0
  136. packages/platform/sdk/src/runtime/sources/executor-tools.ts +0 −0
  137. packages/platform/sdk/src/runtime/sources/graphql-tools.test.ts +0 −0
  138. packages/platform/sdk/src/runtime/sources/graphql-tools.ts +0 −0
  139. packages/platform/sdk/src/runtime/sources/slug.ts +0 −0
  140. packages/platform/sdk/src/runtime/sources/source-adapter-fixture-matrix.test.ts +0 −0
  141. packages/platform/sdk/src/runtime/sources/source-adapters/google-discovery.test.ts +0 −0
  142. packages/platform/sdk/src/runtime/sources/source-adapters/google-discovery.ts +0 −0
  143. packages/platform/sdk/src/runtime/sources/source-adapters/graphql.test.ts +0 −0
  144. packages/platform/sdk/src/runtime/sources/source-adapters/graphql.ts +0 −0
  145. packages/platform/sdk/src/runtime/sources/source-adapters/index.ts +0 −0
  146. packages/platform/sdk/src/runtime/sources/source-adapters/internal.ts +0 −0
  147. packages/platform/sdk/src/runtime/sources/source-adapters/mcp.test.ts +0 −0
  148. packages/platform/sdk/src/runtime/sources/source-adapters/mcp.ts +0 −0
  149. packages/platform/sdk/src/runtime/sources/source-adapters/openapi.ts +0 −0
  150. packages/platform/sdk/src/runtime/sources/source-adapters/shared.ts +0 −0
  151. packages/platform/sdk/src/runtime/sources/source-adapters/types.ts +0 −0
  152. packages/platform/sdk/src/runtime/sources/source-auth-service.ts +0 −0
  153. packages/platform/sdk/src/runtime/sources/source-credential-interactions.ts +0 −0
  154. packages/platform/sdk/src/runtime/sources/source-definitions.test.ts +0 −0
  155. packages/platform/sdk/src/runtime/sources/source-definitions.ts +2 −2
  156. packages/platform/sdk/src/runtime/sources/source-discovery.test.ts +0 −0
  157. packages/platform/sdk/src/runtime/sources/source-discovery.ts +1 −0
  158. packages/platform/sdk/src/runtime/sources/source-inspection.test.ts +0 −0
  159. packages/platform/sdk/src/runtime/sources/source-inspection.ts +1 −0
  160. packages/platform/sdk/src/runtime/sources/source-names.ts +0 −0
  161. packages/platform/sdk/src/runtime/sources/source-store.test.ts +0 −0
  162. packages/platform/sdk/src/runtime/sources/source-store.ts +0 −0
  163. packages/platform/sdk/src/runtime/sources/source-store/auth.ts +0 −0
  164. packages/platform/sdk/src/runtime/sources/source-store/config.ts +0 −0
  165. packages/platform/sdk/src/runtime/sources/source-store/deps.ts +0 −0
  166. packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts +0 −0
  167. packages/platform/sdk/src/runtime/sources/source-store/records.ts +0 −0
  168. packages/platform/sdk/src/runtime/sources/sources-operations.ts +1 −0
  169. packages/platform/sdk/src/runtime/store.ts +0 −0
  170. packages/platform/sdk/src/schema/common.ts +0 −0
  171. packages/platform/sdk/src/schema/enums.ts +0 −0
  172. packages/platform/sdk/src/schema/ids.ts +0 −0
  173. packages/platform/sdk/src/schema/index.ts +0 −0
  174. packages/platform/sdk/src/schema/models/auth-artifact.ts +0 −0
  175. packages/platform/sdk/src/schema/models/auth-lease.ts +0 −0
  176. packages/platform/sdk/src/schema/models/auth/account.ts +0 −0
  177. packages/platform/sdk/src/schema/models/auth/index.ts +0 −0
  178. packages/platform/sdk/src/schema/models/auth/principal.ts +0 −0
  179. packages/platform/sdk/src/schema/models/code-migration.ts +0 −0
  180. packages/platform/sdk/src/schema/models/credential.ts +0 −0
  181. packages/platform/sdk/src/schema/models/execution.ts +0 −0
  182. packages/platform/sdk/src/schema/models/local-config.ts +0 −0
  183. packages/platform/sdk/src/schema/models/local-installation.ts +0 −0
  184. packages/platform/sdk/src/schema/models/policy.ts +0 −0
  185. packages/platform/sdk/src/schema/models/provider-auth-grant.ts +0 −0
  186. packages/platform/sdk/src/schema/models/secret-material.ts +0 −0
  187. packages/platform/sdk/src/schema/models/source-auth-session.ts +0 −0
  188. packages/platform/sdk/src/schema/models/source-catalog.ts +0 −0
  189. packages/platform/sdk/src/schema/models/source-discovery.ts +0 −0
  190. packages/platform/sdk/src/schema/models/source-inspection.ts +0 −0
  191. packages/platform/sdk/src/schema/models/source-oauth-client.ts +0 −0
  192. packages/platform/sdk/src/schema/models/source.ts +0 −0
  193. packages/platform/sdk/src/schema/models/workspace-oauth-client.ts +0 −0
  194. packages/platform/sdk/src/schema/schema.test.ts +0 −0
  195. packages/platform/sdk/src/sources/contracts.ts +170 −0
  196. packages/platform/sdk/src/sources/discovery.ts +3 −3
  197. packages/platform/sdk/src/sources/inspection.ts +88 −41
  198. packages/platform/sdk/src/sources/operations.ts +85 −72
  199. packages/platform/sdk/src/string-schemas.ts +0 −0
  200. packages/platform/sdk/tsconfig.json +14 −0
  201. packages/platform/sdk/vitest.config.ts +8 −0
  202. packages/platform/server/package.json +3 −1
  203. packages/platform/server/src/index.ts +6 −2
  204. packages/sources/openapi/src/real-spec-coverage.test.ts +1 −1
  205. packages/sources/openapi/src/tool-presentation.test.ts +1 −1
.executor/executor.jsonc+5−21
@@ -1,25 +1,9 @@
1{1{
2 "sources": {2 "policies": {
3 "vercel": {3 "demo.*": {
4 "name": "Vercel API",4 "match": "demo.*",
5 "connection": {5 "action": "allow",
6 "endpoint": "https://api.vercel.com/"6 "approval": "auto"
7 },
8 "kind": "openapi",
9 "binding": {
10 "specUrl": "https://openapi.vercel.sh/",
11 "defaultHeaders": null
12 }
13 },
14 "linear": {
15 "kind": "graphql",
16 "name": "Linear GraphQL",
17 "connection": {
18 "endpoint": "https://api.linear.app/graphql"
19 },
20 "binding": {
21 "defaultHeaders": null
22 }
23 }7 }
24 }8 }
25}9}
ARCHITECTURE.md+2−2
@@ -88,7 +88,7 @@ Responsibilities: · 87 unmodified lines
88 88
89This 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.89This 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 core91### `packages/platform/sdk`: product core
92 92
93This is the center of the system.93This is the center of the system.
94 94
@@ -408,4 +408,4 @@ A few practical boundaries are worth calling out: · 313 unmodified lines
408- `README.md` for the product view and usage guidance408- `README.md` for the product view and usage guidance
409- `apps/executor/src/cli/main.ts` for the CLI surface409- `apps/executor/src/cli/main.ts` for the CLI surface
410- `packages/platform/server/src/index.ts` for how the local server is assembled410- `packages/platform/server/src/index.ts` for how the local server is assembled
411- `packages/platform/control-plane/src/runtime/` for the core runtime flows411- `packages/platform/sdk/src/runtime/` for the core runtime flows
README.md+4−2
@@ -329,7 +329,9 @@ If you are exploring the repo, these are the directories that matter most: · 328 unmodified lines
329- `apps/executor`: packaged CLI entrypoint and daemon lifecycle commands329- `apps/executor`: packaged CLI entrypoint and daemon lifecycle commands
330- `apps/web`: local React web UI330- `apps/web`: local React web UI
331- `packages/platform/server`: local HTTP server that serves API, MCP, and UI331- `packages/platform/server`: local HTTP server that serves API, MCP, and UI
332- `packages/platform/control-plane`: source management, secrets, persistence, execution, and inspection332- `packages/platform/sdk`: source management, secrets, persistence, execution, and inspection
333- `packages/platform/api`: thin HTTP adapter over the platform SDK
334- `packages/platform/internal`: thin internal tool adapter over the platform SDK
333- `packages/kernel/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution335- `packages/kernel/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution
334- `packages/kernel/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution336- `packages/kernel/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution
335- `packages/kernel/runtime-ses`: optional SES sandbox runtime for TypeScript execution337- `packages/kernel/runtime-ses`: optional SES sandbox runtime for TypeScript execution
@@ -349,7 +351,7 @@ If you are exploring the repo, these are the directories that matter most: · 13 unmodified lines
349- One-time npm setup: either configure npm trusted publishing for `RhysSullivan/executor` with the workflow file `.github/workflows/publish-executor-package.yml`, or add a GitHub Actions secret named `NPM_TOKEN` that can publish the `executor` package.351- One-time npm setup: either configure npm trusted publishing for `RhysSullivan/executor` with the workflow file `.github/workflows/publish-executor-package.yml`, or add a GitHub Actions secret named `NPM_TOKEN` that can publish the `executor` package.
350- Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`.352- Stable releases use a normal semver like `1.2.3` and publish to npm under `latest`.
351- Beta releases use a prerelease semver like `1.3.0-beta.1` and publish to npm under `beta`.353- Beta releases use a prerelease semver like `1.3.0-beta.1` and publish to npm under `beta`.
352- When a release should become an upgrade test fixture, capture a real workspace snapshot with `bun run fixture:release:capture -- ...` and commit it under [`packages/platform/control-plane/src/runtime/__fixtures__`](./packages/platform/control-plane/src/runtime/__fixtures__/README.md).354- When a release should become an upgrade test fixture, capture a real workspace snapshot with `bun run fixture:release:capture -- ...` and commit it under [`packages/platform/sdk/src/runtime/__fixtures__`](./packages/platform/sdk/src/runtime/__fixtures__/README.md).
353 355
354## Project status356## Project status
355 357
apps/docs/developer/cli.mdx+1−1
@@ -41,7 +41,7 @@ At a high level, the CLI: · 40 unmodified lines
41 41
421. ensures the local daemon is available421. ensures the local daemon is available
432. calls the local HTTP API exposed by `packages/platform/server`432. calls the local HTTP API exposed by `packages/platform/server`
443. hands off real work to `packages/platform/control-plane`443. hands off real work to `packages/platform/sdk` and `packages/platform/api`
45 45
46That makes the CLI a stable shell over a shared local runtime rather than a second product core.46That makes the CLI a stable shell over a shared local runtime rather than a second product core.
47 47
apps/docs/developer/overview.mdx+1−1
@@ -41,7 +41,7 @@ Responsibilities: · 40 unmodified lines
41- mount the local MCP server at `/mcp`41- mount the local MCP server at `/mcp`
42- serve the web UI42- serve the web UI
43 43
44### `packages/platform/control-plane`44### `packages/platform/sdk`
45 45
46The product core.46The product core.
47 47
apps/executor/package.json+3−1
@@ -37,8 +37,10 @@ · 36 unmodified lines
37 "@effect/platform": "catalog:",37 "@effect/platform": "catalog:",
38 "@effect/platform-node": "catalog:",38 "@effect/platform-node": "catalog:",
39 "@executor/codemode-core": "workspace:*",39 "@executor/codemode-core": "workspace:*",
40 "@executor/control-plane": "workspace:*",
41 "@executor/executor-mcp": "workspace:*",40 "@executor/executor-mcp": "workspace:*",
41 "@executor/platform-api": "workspace:*",
42 "@executor/platform-internal": "workspace:*",
43 "@executor/platform-sdk": "workspace:*",
42 "@executor/server": "workspace:*",44 "@executor/server": "workspace:*",
43 "effect": "catalog:"45 "effect": "catalog:"
44 },46 },
apps/executor/src/cli/dev.ts+2−4
@@ -1,7 +1,5 @@
1import {1import type { ControlPlaneClient } from "@executor/platform-api";
2 type ControlPlaneClient,2import { WorkspaceIdSchema } from "@executor/platform-sdk/schema";
3 WorkspaceIdSchema,
4} from "@executor/control-plane";
5import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
6 4
7const readBindingString = (binding: Record<string, unknown>, key: string): string | null =>5const readBindingString = (binding: Record<string, unknown>, key: string): string | null =>
apps/executor/src/cli/main.ts+11−5
@@ -9,17 +9,22 @@ import { · 8 unmodified lines
9 NodePath,9 NodePath,
10 NodeRuntime,10 NodeRuntime,
11} from "@effect/platform-node";11} from "@effect/platform-node";
12import {
13 createControlPlaneClient,
14 type ControlPlaneClient,
15} from "@executor/platform-api";
16import { createExecutorAdminToolMap } from "@executor/platform-internal";
12import {17import {
13 EXECUTOR_SOURCES_ADD_HELP_LINES,18 EXECUTOR_SOURCES_ADD_HELP_LINES,
14 ExecutionIdSchema,
15 RuntimeExecutionResolverService,19 RuntimeExecutionResolverService,
16 createControlPlaneClient,
17 createControlPlaneRuntime,20 createControlPlaneRuntime,
18 type ControlPlaneClient,21 type ControlPlaneRuntime,
22} from "@executor/platform-sdk/runtime";
23import {
24 ExecutionIdSchema,
19 type ExecutionEnvelope,25 type ExecutionEnvelope,
20 type ExecutionInteraction,26 type ExecutionInteraction,
21 type ControlPlaneRuntime,27} from "@executor/platform-sdk/schema";
22} from "@executor/control-plane";
23import type { ToolCatalog } from "@executor/codemode-core";28import type { ToolCatalog } from "@executor/codemode-core";
24 29
25import * as Effect from "effect/Effect";30import * as Effect from "effect/Effect";
@@ -272,6 +277,7 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 246 unmodified lines
272 Effect.acquireUseRelease(277 Effect.acquireUseRelease(
273 createControlPlaneRuntime({278 createControlPlaneRuntime({
274 localDataDir: DEFAULT_LOCAL_DATA_DIR,279 localDataDir: DEFAULT_LOCAL_DATA_DIR,
280 createInternalToolMap: createExecutorAdminToolMap,
275 }).pipe(Effect.mapError(toError)),281 }).pipe(Effect.mapError(toError)),
276 (runtime) =>282 (runtime) =>
277 Effect.gen(function* () {283 Effect.gen(function* () {
apps/executor/src/cli/pending-interaction-output.test.ts+1−1
@@ -1,5 +1,5 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import type { ExecutionInteraction } from "@executor/control-plane";2import type { ExecutionInteraction } from "@executor/platform-sdk/schema";
3 3
4import {4import {
5 buildPausedExecutionOutput,5 buildPausedExecutionOutput,
apps/executor/src/cli/pending-interaction-output.ts+1−1
@@ -1,4 +1,4 @@
1import type { ExecutionInteraction } from "@executor/control-plane";1import type { ExecutionInteraction } from "@executor/platform-sdk/schema";
2 2
3const isRecord = (value: unknown): value is Record<string, unknown> =>3const isRecord = (value: unknown): value is Record<string, unknown> =>
4 typeof value === "object" && value !== null && !Array.isArray(value);4 typeof value === "object" && value !== null && !Array.isArray(value);
apps/executor/src/server/server.real-ingestion.test.ts+2−2
@@ -5,8 +5,8 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
5 5
6import {6import {
7 createControlPlaneClient,7 createControlPlaneClient,
8 type SourceDiscoveryKind,8} from "@executor/platform-api";
9} from "@executor/control-plane";9import { type SourceDiscoveryKind } from "@executor/platform-sdk/schema";
10import { createLocalExecutorServer } from "@executor/server";10import { createLocalExecutorServer } from "@executor/server";
11 11
12const REAL_VERCEL_SPEC_URL = "https://openapi.vercel.sh";12const REAL_VERCEL_SPEC_URL = "https://openapi.vercel.sh";
apps/executor/src/server/server.test.ts+5−3
@@ -20,15 +20,17 @@ import { makeToolInvokerFromTools, toTool } from "@executor/codemode-core"; · 19 unmodified lines
20import {20import {
21 createControlPlaneClient,21 createControlPlaneClient,
22 controlPlaneOpenApiSpec,22 controlPlaneOpenApiSpec,
23} from "@executor/platform-api";
24import {
23 buildLocalSourceArtifact,25 buildLocalSourceArtifact,
24 catalogSyncResultFromMcpManifest,26 catalogSyncResultFromMcpManifest,
25 deriveLocalInstallation,27 deriveLocalInstallation,
26 type ResolveExecutionEnvironment,
27 resolveLocalWorkspaceContext,28 resolveLocalWorkspaceContext,
28 SourceIdSchema,
29 writeLocalSourceArtifact,29 writeLocalSourceArtifact,
30 writeProjectLocalExecutorConfig,30 writeProjectLocalExecutorConfig,
31} from "@executor/control-plane";31 type ResolveExecutionEnvironment,
32} from "@executor/platform-sdk/runtime";
33import { SourceIdSchema } from "@executor/platform-sdk/schema";
32import { makeSesExecutor } from "@executor/runtime-ses";34import { makeSesExecutor } from "@executor/runtime-ses";
33import { Client } from "@modelcontextprotocol/sdk/client/index.js";35import { Client } from "@modelcontextprotocol/sdk/client/index.js";
34import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";36import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
bun.lock+56−17
@@ -19,7 +19,7 @@ · 18 unmodified lines
19 },19 },
20 "apps/executor": {20 "apps/executor": {
21 "name": "executor",21 "name": "executor",
22 "version": "1.2.3",22 "version": "1.2.4-beta.4",
23 "bin": {23 "bin": {
24 "executor": "bin/executor",24 "executor": "bin/executor",
25 },25 },
@@ -28,8 +28,10 @@ · 2 unmodified lines
28 "@effect/platform": "catalog:",28 "@effect/platform": "catalog:",
29 "@effect/platform-node": "catalog:",29 "@effect/platform-node": "catalog:",
30 "@executor/codemode-core": "workspace:*",30 "@executor/codemode-core": "workspace:*",
31 "@executor/control-plane": "workspace:*",
32 "@executor/executor-mcp": "workspace:*",31 "@executor/executor-mcp": "workspace:*",
32 "@executor/platform-api": "workspace:*",
33 "@executor/platform-internal": "workspace:*",
34 "@executor/platform-sdk": "workspace:*",
33 "@executor/server": "workspace:*",35 "@executor/server": "workspace:*",
34 "effect": "catalog:",36 "effect": "catalog:",
35 },37 },
@@ -129,7 +131,8 @@ · 93 unmodified lines
129 "dependencies": {131 "dependencies": {
130 "@effect-atom/atom": "0.5.3",132 "@effect-atom/atom": "0.5.3",
131 "@effect-atom/atom-react": "0.5.0",133 "@effect-atom/atom-react": "0.5.0",
132 "@executor/control-plane": "workspace:*",134 "@executor/platform-api": "workspace:*",
135 "@executor/platform-sdk": "workspace:*",
133 "effect": "catalog:",136 "effect": "catalog:",
134 },137 },
135 "devDependencies": {138 "devDependencies": {
@@ -156,7 +159,7 @@ · 20 unmodified lines
156 "@effect/platform": "catalog:",159 "@effect/platform": "catalog:",
157 "@executor/ai-sdk-adapter": "workspace:*",160 "@executor/ai-sdk-adapter": "workspace:*",
158 "@executor/codemode-core": "workspace:*",161 "@executor/codemode-core": "workspace:*",
159 "@executor/control-plane": "workspace:*",162 "@executor/platform-sdk": "workspace:*",
160 "@executor/runtime-ses": "workspace:*",163 "@executor/runtime-ses": "workspace:*",
161 "@executor/source-openapi": "workspace:*",164 "@executor/source-openapi": "workspace:*",
162 "effect": "catalog:",165 "effect": "catalog:",
@@ -187,7 +190,7 @@ · 24 unmodified lines
187 "name": "@executor/executor-mcp",190 "name": "@executor/executor-mcp",
188 "version": "0.0.0",191 "version": "0.0.0",
189 "dependencies": {192 "dependencies": {
190 "@executor/control-plane": "workspace:*",193 "@executor/platform-sdk": "workspace:*",
191 "@modelcontextprotocol/sdk": "^1.26.0",194 "@modelcontextprotocol/sdk": "^1.26.0",
192 "effect": "catalog:",195 "effect": "catalog:",
193 "zod": "catalog:",196 "zod": "catalog:",
@@ -270,11 +273,41 @@ · 76 unmodified lines
270 "bun-types": "catalog:",273 "bun-types": "catalog:",
271 },274 },
272 },275 },
273 "packages/platform/control-plane": {276 "packages/platform/api": {
274 "name": "@executor/control-plane",277 "name": "@executor/platform-api",
275 "version": "0.0.0",278 "version": "0.0.0",
276 "dependencies": {279 "dependencies": {
277 "@effect/platform": "catalog:",280 "@effect/platform": "catalog:",
281 "@executor/platform-sdk": "workspace:*",
282 "effect": "catalog:",
283 },
284 "devDependencies": {
285 "@effect/vitest": "catalog:",
286 "bun-types": "catalog:",
287 "typescript": "^5.9.3",
288 "vitest": "catalog:",
289 },
290 },
291 "packages/platform/internal": {
292 "name": "@executor/platform-internal",
293 "version": "0.0.0",
294 "dependencies": {
295 "@executor/codemode-core": "workspace:*",
296 "@executor/platform-sdk": "workspace:*",
297 "effect": "catalog:",
298 },
299 "devDependencies": {
300 "@types/node": "catalog:",
301 "bun-types": "catalog:",
302 "typescript": "^5.9.3",
303 },
304 },
305 "packages/platform/sdk": {
306 "name": "@executor/platform-sdk",
307 "version": "0.0.0",
308 "dependencies": {
309 "@effect/platform": "catalog:",
310 "@effect/platform-node": "catalog:",
278 "@executor/auth-mcp-oauth": "workspace:*",311 "@executor/auth-mcp-oauth": "workspace:*",
279 "@executor/auth-oauth2": "workspace:*",312 "@executor/auth-oauth2": "workspace:*",
280 "@executor/codemode-core": "workspace:*",313 "@executor/codemode-core": "workspace:*",
@@ -289,20 +322,20 @@ · 8 unmodified lines
289 "@executor/source-mcp": "workspace:*",322 "@executor/source-mcp": "workspace:*",
290 "@executor/source-openapi": "workspace:*",323 "@executor/source-openapi": "workspace:*",
291 "effect": "catalog:",324 "effect": "catalog:",
292 "graphql": "^16.12.0",
293 "jsonc-parser": "3.3.1",
294 "postgres": "^3.4.8",
295 "prettier": "^3.8.1",
296 "typescript": "^5.9.3",
297 },325 },
298 "devDependencies": {326 "devDependencies": {
299 "@effect/platform-node": "catalog:",
300 "@effect/vitest": "catalog:",327 "@effect/vitest": "catalog:",
328 "@executor/platform-api": "workspace:*",
329 "@executor/platform-internal": "workspace:*",
301 "@modelcontextprotocol/sdk": "^1.26.0",330 "@modelcontextprotocol/sdk": "^1.26.0",
302 "@types/node": "catalog:",331 "@types/node": "catalog:",
303 "bun-types": "catalog:",332 "bun-types": "catalog:",
333 "graphql": "^16.12.0",
334 "jsonc-parser": "3.3.1",
335 "postgres": "^3.4.8",
336 "prettier": "^3.8.1",
337 "typescript": "^5.9.3",
304 "vitest": "catalog:",338 "vitest": "catalog:",
305 "zod": "catalog:",
306 },339 },
307 },340 },
308 "packages/platform/server": {341 "packages/platform/server": {
@@ -312,8 +345,10 @@ · 3 unmodified lines
312 "@effect/opentelemetry": "^0.61.0",345 "@effect/opentelemetry": "^0.61.0",
313 "@effect/platform": "catalog:",346 "@effect/platform": "catalog:",
314 "@effect/platform-node": "catalog:",347 "@effect/platform-node": "catalog:",
315 "@executor/control-plane": "workspace:*",
316 "@executor/executor-mcp": "workspace:*",348 "@executor/executor-mcp": "workspace:*",
349 "@executor/platform-api": "workspace:*",
350 "@executor/platform-internal": "workspace:*",
351 "@executor/platform-sdk": "workspace:*",
317 "@opentelemetry/api": "^1.9.0",352 "@opentelemetry/api": "^1.9.0",
318 "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",353 "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
319 "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",354 "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
@@ -641,8 +676,6 @@ · 321 unmodified lines
641 676
642 "@executor/codemode-core": ["@executor/codemode-core@workspace:packages/kernel/core"],677 "@executor/codemode-core": ["@executor/codemode-core@workspace:packages/kernel/core"],
643 678
644 "@executor/control-plane": ["@executor/control-plane@workspace:packages/platform/control-plane"],
645
646 "@executor/docs": ["@executor/docs@workspace:apps/docs"],679 "@executor/docs": ["@executor/docs@workspace:apps/docs"],
647 680
648 "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/hosts/mcp"],681 "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/hosts/mcp"],
@@ -653,6 +686,12 @@ · 4 unmodified lines
653 686
654 "@executor/mcp-elicitation-demo": ["@executor/mcp-elicitation-demo@workspace:examples/mcp-elicitation-demo"],687 "@executor/mcp-elicitation-demo": ["@executor/mcp-elicitation-demo@workspace:examples/mcp-elicitation-demo"],
655 688
689 "@executor/platform-api": ["@executor/platform-api@workspace:packages/platform/api"],
690
691 "@executor/platform-internal": ["@executor/platform-internal@workspace:packages/platform/internal"],
692
693 "@executor/platform-sdk": ["@executor/platform-sdk@workspace:packages/platform/sdk"],
694
656 "@executor/react": ["@executor/react@workspace:packages/clients/react"],695 "@executor/react": ["@executor/react@workspace:packages/clients/react"],
657 696
658 "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/kernel/runtime-deno-subprocess"],697 "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/kernel/runtime-deno-subprocess"],
package.json+1−1
@@ -35,7 +35,7 @@ · 34 unmodified lines
35 "release:pre:exit": "changeset pre exit",35 "release:pre:exit": "changeset pre exit",
36 "release:beta:start": "changeset pre enter beta",36 "release:beta:start": "changeset pre enter beta",
37 "release:beta:stop": "changeset pre exit",37 "release:beta:stop": "changeset pre exit",
38 "fixture:release:capture": "bun run --cwd packages/platform/control-plane fixture:release:capture",38 "fixture:release:capture": "bun run --cwd packages/platform/sdk fixture:release:capture",
39 "typecheck": "bun run --filter='*' typecheck",39 "typecheck": "bun run --filter='*' typecheck",
40 "knip": "knip"40 "knip": "knip"
41 },41 },
packages/clients/react/package.json+2−1
@@ -13,7 +13,8 @@ · 12 unmodified lines
13 "dependencies": {13 "dependencies": {
14 "@effect-atom/atom": "0.5.3",14 "@effect-atom/atom": "0.5.3",
15 "@effect-atom/atom-react": "0.5.0",15 "@effect-atom/atom-react": "0.5.0",
16 "@executor/control-plane": "workspace:*",16 "@executor/platform-api": "workspace:*",
17 "@executor/platform-sdk": "workspace:*",
17 "effect": "catalog:"18 "effect": "catalog:"
18 },19 },
19 "peerDependencies": {20 "peerDependencies": {
packages/clients/react/src/index.test.tsx+7−3
@@ -5,11 +5,15 @@ import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node"; · 4 unmodified lines
5import { describe, expect, it } from "@effect/vitest";5import { describe, expect, it } from "@effect/vitest";
6import {6import {
7 createControlPlaneApiLayer,7 createControlPlaneApiLayer,
8} from "@executor/platform-api";
9import {
8 createControlPlaneRuntime,10 createControlPlaneRuntime,
9 type LocalInstallation,
10 type Source,
11 type ControlPlaneRuntime,11 type ControlPlaneRuntime,
12} from "@executor/control-plane";12} from "@executor/platform-sdk/runtime";
13import type {
14 LocalInstallation,
15 Source,
16} from "@executor/platform-sdk/schema";
13import * as Context from "effect/Context";17import * as Context from "effect/Context";
14import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
15import * as Exit from "effect/Exit";19import * as Exit from "effect/Exit";
packages/clients/react/src/index.ts+24−22
@@ -1,35 +1,37 @@
1import { Atom, Result } from "@effect-atom/atom";1import { Atom, Result } from "@effect-atom/atom";
2import type * as Registry from "@effect-atom/atom/Registry";2import type * as Registry from "@effect-atom/atom/Registry";
3import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react";3import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react";
4import { createControlPlaneClient } from "@executor/control-plane/client";4import {
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";
5import type {27import type {
6 CompleteSourceOAuthResult,
7 ConnectSourceBatchPayload,
8 ConnectSourceBatchResult,
9 ConnectSourcePayload,
10 ConnectSourceResult,
11 ControlPlaneClient,
12 CreateSecretPayload,
13 CreateSecretResult,
14 CreateSourcePayload,
15 CreateWorkspaceOauthClientPayload,
16 DeleteSecretResult,
17 DiscoverSourcePayload,
18 InstanceConfig,
19 LocalInstallation,
20 SecretListItem,
21 Source,28 Source,
22 SourceDiscoveryResult,29 SourceDiscoveryResult,
23 SourceInspection,30 SourceInspection,
24 SourceInspectionDiscoverResult,31 SourceInspectionDiscoverResult,
25 SourceInspectionToolDetail,32 SourceInspectionToolDetail,
26 StartSourceOAuthPayload,
27 StartSourceOAuthResult,
28 UpdateSecretPayload,
29 UpdateSecretResult,
30 UpdateSourcePayload,
31 WorkspaceOauthClient,33 WorkspaceOauthClient,
32} from "@executor/control-plane";34} from "@executor/platform-sdk/schema";
33import * as Cause from "effect/Cause";35import * as Cause from "effect/Cause";
34import * as Effect from "effect/Effect";36import * as Effect from "effect/Effect";
35import * as Exit from "effect/Exit";37import * as Exit from "effect/Exit";
packages/dev/kitchen-sink/package.json+2−2
@@ -12,9 +12,9 @@ · 11 unmodified lines
12 "@effect/platform": "catalog:",12 "@effect/platform": "catalog:",
13 "@executor/ai-sdk-adapter": "workspace:*",13 "@executor/ai-sdk-adapter": "workspace:*",
14 "@executor/codemode-core": "workspace:*",14 "@executor/codemode-core": "workspace:*",
15 "@executor/source-openapi": "workspace:*",15 "@executor/platform-sdk": "workspace:*",
16 "@executor/control-plane": "workspace:*",
17 "@executor/runtime-ses": "workspace:*",16 "@executor/runtime-ses": "workspace:*",
17 "@executor/source-openapi": "workspace:*",
18 "effect": "catalog:"18 "effect": "catalog:"
19 },19 },
20 "devDependencies": {20 "devDependencies": {
packages/dev/kitchen-sink/src/source-runtime.test.ts+1−1
@@ -17,7 +17,7 @@ import { · 16 unmodified lines
17 type Source,17 type Source,
18 type SourceId,18 type SourceId,
19 type WorkspaceId,19 type WorkspaceId,
20} from "@executor/control-plane";20} from "@executor/platform-sdk/schema";
21import {21import {
22 createToolCatalogDiscovery,22 createToolCatalogDiscovery,
23 createToolCatalogFromTools,23 createToolCatalogFromTools,
packages/hosts/mcp/package.json+1−1
@@ -11,7 +11,7 @@ · 10 unmodified lines
11 "test:watch": "vitest"11 "test:watch": "vitest"
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@executor/control-plane": "workspace:*",14 "@executor/platform-sdk": "workspace:*",
15 "@modelcontextprotocol/sdk": "^1.26.0",15 "@modelcontextprotocol/sdk": "^1.26.0",
16 "effect": "catalog:",16 "effect": "catalog:",
17 "zod": "catalog:"17 "zod": "catalog:"
packages/hosts/mcp/src/index.ts+4−4
@@ -1,15 +1,15 @@
1import type {1import type {
2 ExecutionEnvelope,2 ExecutionEnvelope,
3 ControlPlaneRuntime,3} from "@executor/platform-sdk/schema";
4} from "@executor/control-plane";
5import {4import {
6 EXECUTOR_SOURCES_ADD_HELP_LINES,5 EXECUTOR_SOURCES_ADD_HELP_LINES,
7 ExecutionIdSchema,
8 RuntimeExecutionResolverService,6 RuntimeExecutionResolverService,
9 createExecution,7 createExecution,
10 getExecution,8 getExecution,
11 resumeExecution,9 resumeExecution,
12} from "@executor/control-plane";10 type ControlPlaneRuntime,
11} from "@executor/platform-sdk/runtime";
12import { ExecutionIdSchema } from "@executor/platform-sdk/schema";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as Cause from "effect/Cause";14import * as Cause from "effect/Cause";
15import * as Exit from "effect/Exit";15import * as Exit from "effect/Exit";
packages/hosts/mcp/src/paused-result.test.ts+4−1
@@ -1,5 +1,8 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import type { ExecutionEnvelope, ExecutionInteraction } from "@executor/control-plane";2import type {
3 ExecutionEnvelope,
4 ExecutionInteraction,
5} from "@executor/platform-sdk/schema";
3 6
4import {7import {
5 buildPausedResultText,8 buildPausedResultText,
packages/hosts/mcp/src/paused-result.ts+1−1
@@ -1,7 +1,7 @@
1import type {1import type {
2 ExecutionEnvelope,2 ExecutionEnvelope,
3 ExecutionInteraction,3 ExecutionInteraction,
4} from "@executor/control-plane";4} from "@executor/platform-sdk/schema";
5 5
6export type ParsedInteractionPayload = {6export type ParsedInteractionPayload = {
7 mode: "form" | "url";7 mode: "form" | "url";
packages/platform/api/package.json+40−0
@@ -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}
packages/platform/api/src/api.tsrenamed from packages/platform/control-plane/src/api/api.ts+0−0

File renamed without changes.

packages/platform/api/src/client.tsrenamed from packages/platform/control-plane/src/client.ts+1−1
@@ -4,7 +4,7 @@ import { · 3 unmodified lines
4} from "@effect/platform";4} from "@effect/platform";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6 6
7import { ControlPlaneApi } from "./api/api";7import { ControlPlaneApi } from "./api";
8 8
9export const createControlPlaneClient = (input: {9export const createControlPlaneClient = (input: {
10 baseUrl: string;10 baseUrl: string;
packages/platform/api/src/errors.ts+7−0
@@ -0,0 +1,7 @@
1export {
2 ControlPlaneBadRequestError,
3 ControlPlaneForbiddenError,
4 ControlPlaneNotFoundError,
5 ControlPlaneStorageError,
6 ControlPlaneUnauthorizedError,
7} from "@executor/platform-sdk/errors";
packages/platform/api/src/executions/api.tsrenamed from packages/platform/control-plane/src/api/executions/api.ts+14−17
@@ -1,10 +1,17 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import {
3 CreateExecutionPayloadSchema,
4 ResumeExecutionPayloadSchema,
5} from "@executor/platform-sdk/executions/contracts";
6export type {
7 CreateExecutionPayload,
8 ResumeExecutionPayload,
9} from "@executor/platform-sdk/executions/contracts";
2import {10import {
3 ExecutionIdSchema,11 ExecutionIdSchema,
4 ExecutionEnvelopeSchema,12 ExecutionEnvelopeSchema,
5 WorkspaceIdSchema,13 WorkspaceIdSchema,
6} from "#schema";14} from "@executor/platform-sdk/schema";
7import * as Schema from "effect/Schema";
8 15
9import {16import {
10 ControlPlaneBadRequestError,17 ControlPlaneBadRequestError,
@@ -12,22 +19,12 @@ import { · 1 unmodified line
12 ControlPlaneNotFoundError,19 ControlPlaneNotFoundError,
13 ControlPlaneStorageError,20 ControlPlaneStorageError,
14 ControlPlaneUnauthorizedError,21 ControlPlaneUnauthorizedError,
15} from "../errors";22} from "@executor/platform-sdk/errors";
16import { TrimmedNonEmptyStringSchema } from "../string-schemas";
17
18export const CreateExecutionPayloadSchema = Schema.Struct({
19 code: TrimmedNonEmptyStringSchema,
20 interactionMode: Schema.optional(Schema.Literal("live", "live_form", "detach")),
21});
22
23export type CreateExecutionPayload = typeof CreateExecutionPayloadSchema.Type;
24
25export const ResumeExecutionPayloadSchema = Schema.Struct({
26 responseJson: Schema.optional(Schema.String),
27 interactionMode: Schema.optional(Schema.Literal("live", "live_form", "detach")),
28});
29 23
30export type ResumeExecutionPayload = typeof ResumeExecutionPayloadSchema.Type;24export {
25 CreateExecutionPayloadSchema,
26 ResumeExecutionPayloadSchema,
27};
31 28
32const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);29const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
33const executionIdParam = HttpApiSchema.param("executionId", ExecutionIdSchema);30const executionIdParam = HttpApiSchema.param("executionId", ExecutionIdSchema);
packages/platform/api/src/executions/http.tsrenamed from packages/platform/control-plane/src/api/executions/http.ts+1−1
@@ -4,7 +4,7 @@ import { · 3 unmodified lines
4 createExecution,4 createExecution,
5 getExecution,5 getExecution,
6 resumeExecution,6 resumeExecution,
7} from "../../runtime/execution/service";7} from "@executor/platform-sdk/runtime";
8 8
9import { ControlPlaneApi } from "../api";9import { ControlPlaneApi } from "../api";
10import { resolveRequestedLocalWorkspace } from "../local-context";10import { resolveRequestedLocalWorkspace } from "../local-context";
packages/platform/api/src/http.tsrenamed from packages/platform/control-plane/src/api/http.ts+0−0

File renamed without changes.

packages/platform/api/src/index.tsrenamed from packages/platform/control-plane/src/api/index.ts+19−1
@@ -2,8 +2,12 @@ export { · 1 unmodified line
2 ControlPlaneApi,2 ControlPlaneApi,
3 controlPlaneOpenApiSpec,3 controlPlaneOpenApiSpec,
4} from "./api";4} from "./api";
5export {
6 createControlPlaneClient,
7 type ControlPlaneClient,
8} from "./client";
5 9
6export type { LocalInstallation } from "#schema";10export type { LocalInstallation } from "@executor/platform-sdk/schema";
7 11
8export {12export {
9 ControlPlaneBadRequestError,13 ControlPlaneBadRequestError,
@@ -22,10 +26,12 @@ export { · 12 unmodified lines
22 26
23export {27export {
24 CreateExecutionPayloadSchema,28 CreateExecutionPayloadSchema,
29 ExecutionsApi,
25 ResumeExecutionPayloadSchema,30 ResumeExecutionPayloadSchema,
26 type CreateExecutionPayload,31 type CreateExecutionPayload,
27 type ResumeExecutionPayload,32 type ResumeExecutionPayload,
28} from "./executions/api";33} from "./executions/api";
34export { ControlPlaneExecutionsLive } from "./executions/http";
29 35
30export {36export {
31 LocalApi,37 LocalApi,
@@ -38,16 +44,22 @@ export { · 6 unmodified lines
38 type UpdateSecretResult,44 type UpdateSecretResult,
39 type DeleteSecretResult,45 type DeleteSecretResult,
40} from "./local/api";46} from "./local/api";
47export { ControlPlaneLocalLive } from "./local/http";
41 48
42export {49export {
43 OAuthApi,50 OAuthApi,
44 StartSourceOAuthPayloadSchema,51 StartSourceOAuthPayloadSchema,
45 StartSourceOAuthResultSchema,52 StartSourceOAuthResultSchema,
46 CompleteSourceOAuthResultSchema,53 CompleteSourceOAuthResultSchema,
54 SourceOAuthPopupFailureResultSchema,
55 SourceOAuthPopupResultSchema,
56 SourceOAuthPopupSuccessResultSchema,
47 type StartSourceOAuthPayload,57 type StartSourceOAuthPayload,
48 type StartSourceOAuthResult,58 type StartSourceOAuthResult,
49 type CompleteSourceOAuthResult,59 type CompleteSourceOAuthResult,
60 type SourceOAuthPopupResult,
50} from "./oauth/api";61} from "./oauth/api";
62export { ControlPlaneOAuthLive } from "./oauth/http";
51 63
52export {64export {
53 ConnectSourceBatchPayloadSchema,65 ConnectSourceBatchPayloadSchema,
@@ -57,6 +69,7 @@ export { · 3 unmodified lines
57 CreateWorkspaceOauthClientPayloadSchema,69 CreateWorkspaceOauthClientPayloadSchema,
58 CreateSourcePayloadSchema,70 CreateSourcePayloadSchema,
59 DiscoverSourcePayloadSchema,71 DiscoverSourcePayloadSchema,
72 SourcesApi,
60 UpdateSourcePayloadSchema,73 UpdateSourcePayloadSchema,
61 type ConnectSourceBatchPayload,74 type ConnectSourceBatchPayload,
62 type ConnectSourceBatchResult,75 type ConnectSourceBatchResult,
@@ -67,10 +80,15 @@ export { · 4 unmodified lines
67 type DiscoverSourcePayload,80 type DiscoverSourcePayload,
68 type UpdateSourcePayload,81 type UpdateSourcePayload,
69} from "./sources/api";82} from "./sources/api";
83export { ControlPlaneSourcesLive } from "./sources/http";
70 84
71export {85export {
72 CreatePolicyPayloadSchema,86 CreatePolicyPayloadSchema,
87 PoliciesApi,
73 UpdatePolicyPayloadSchema,88 UpdatePolicyPayloadSchema,
74 type CreatePolicyPayload,89 type CreatePolicyPayload,
75 type UpdatePolicyPayload,90 type UpdatePolicyPayload,
76} from "./policies/api";91} from "./policies/api";
92export { ControlPlanePoliciesLive } from "./policies/http";
93
94export { resolveRequestedLocalWorkspace } from "./local-context";
packages/platform/api/src/local-context.tsrenamed from packages/platform/control-plane/src/api/local-context.ts+3−3
@@ -1,10 +1,10 @@
1import type { WorkspaceId } from "#schema";1import type { WorkspaceId } from "@executor/platform-sdk/schema";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import {4import {
5 RuntimeLocalWorkspaceMismatchError,5 RuntimeLocalWorkspaceMismatchError,
6} from "../runtime/local/errors";6 requireRuntimeLocalWorkspace,
7import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context";7} from "@executor/platform-sdk/runtime";
8import { ControlPlaneForbiddenError } from "./errors";8import { ControlPlaneForbiddenError } from "./errors";
9 9
10export const resolveRequestedLocalWorkspace = (10export const resolveRequestedLocalWorkspace = (
packages/platform/api/src/local/api.ts+69−0
@@ -0,0 +1,69 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import {
3 CreateSecretPayloadSchema,
4 CreateSecretResultSchema,
5 DeleteSecretResultSchema,
6 InstanceConfigSchema,
7 SecretListItemSchema,
8 UpdateSecretPayloadSchema,
9 UpdateSecretResultSchema,
10} from "@executor/platform-sdk/local/contracts";
11import {
12 ControlPlaneBadRequestError,
13 ControlPlaneNotFoundError,
14 ControlPlaneStorageError,
15} from "@executor/platform-sdk/errors";
16import { LocalInstallationSchema } from "@executor/platform-sdk/schema";
17import * as Schema from "effect/Schema";
18
19export 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
31export 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") {}
packages/platform/api/src/local/http.ts+29−0
@@ -0,0 +1,29 @@
1import { HttpApiBuilder } from "@effect/platform";
2import {
3 createLocalSecret,
4 deleteLocalSecret,
5 getLocalInstanceConfig,
6 getLocalInstallation,
7 listLocalSecrets,
8 updateLocalSecret,
9} from "@executor/platform-sdk";
10
11import { ControlPlaneApi } from "../api";
12
13export 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);
packages/platform/api/src/oauth/api.tsrenamed from packages/platform/control-plane/src/api/oauth/api.ts+3−3
@@ -5,7 +5,7 @@ import { · 4 unmodified lines
5 SourceTransportSchema,5 SourceTransportSchema,
6 StringMapSchema,6 StringMapSchema,
7 WorkspaceIdSchema,7 WorkspaceIdSchema,
8} from "#schema";8} from "@executor/platform-sdk/schema";
9import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
10 10
11import {11import {
@@ -13,8 +13,8 @@ import { · 1 unmodified line
13 ControlPlaneForbiddenError,13 ControlPlaneForbiddenError,
14 ControlPlaneStorageError,14 ControlPlaneStorageError,
15 ControlPlaneUnauthorizedError,15 ControlPlaneUnauthorizedError,
16} from "../errors";16} from "@executor/platform-sdk/errors";
17import { TrimmedNonEmptyStringSchema } from "../string-schemas";17import { TrimmedNonEmptyStringSchema } from "@executor/platform-sdk/string-schemas";
18 18
19const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);19const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
20 20
packages/platform/api/src/oauth/http.tsrenamed from packages/platform/control-plane/src/api/oauth/http.ts+2−2
@@ -3,13 +3,13 @@ import { · 2 unmodified lines
3 HttpServerRequest,3 HttpServerRequest,
4 HttpServerResponse,4 HttpServerResponse,
5} from "@effect/platform";5} from "@effect/platform";
6import type { WorkspaceId } from "#schema";6import type { WorkspaceId } from "@executor/platform-sdk/schema";
7import * as Cause from "effect/Cause";7import * as Cause from "effect/Cause";
8import * as Either from "effect/Either";8import * as Either from "effect/Either";
9import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11 11
12import { RuntimeSourceAuthServiceTag } from "../../runtime/sources/source-auth-service";12import { RuntimeSourceAuthServiceTag } from "@executor/platform-sdk/runtime";
13import { ControlPlaneApi } from "../api";13import { ControlPlaneApi } from "../api";
14import {14import {
15 ControlPlaneBadRequestError,15 ControlPlaneBadRequestError,
packages/platform/api/src/payload-schemas.test.tsrenamed from packages/platform/control-plane/src/api/payload-schemas.test.ts+0−0

File renamed without changes.

packages/platform/api/src/policies/api.tsrenamed from packages/platform/control-plane/src/api/policies/api.ts+18−24
@@ -1,37 +1,31 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import {2import {
3 LocalWorkspacePolicyApprovalModeSchema,3 CreatePolicyPayloadSchema,
4 LocalWorkspacePolicyEffectSchema,4 UpdatePolicyPayloadSchema,
5 LocalWorkspacePolicySchema,5} from "@executor/platform-sdk/policies/contracts";
6 PolicyIdSchema,
7 WorkspaceIdSchema,
8} from "#schema";
9import * as Schema from "effect/Schema";
10
11import {6import {
12 ControlPlaneBadRequestError,7 ControlPlaneBadRequestError,
13 ControlPlaneForbiddenError,8 ControlPlaneForbiddenError,
14 ControlPlaneNotFoundError,9 ControlPlaneNotFoundError,
15 ControlPlaneStorageError,10 ControlPlaneStorageError,
16 ControlPlaneUnauthorizedError,11 ControlPlaneUnauthorizedError,
17} from "../errors";12} from "@executor/platform-sdk/errors";
18import { OptionalTrimmedNonEmptyStringSchema } from "../string-schemas";13import {
19 14 LocalWorkspacePolicySchema,
20const LocalWorkspacePolicyPayloadSchema = Schema.Struct({15 PolicyIdSchema,
21 resourcePattern: OptionalTrimmedNonEmptyStringSchema,16 WorkspaceIdSchema,
22 effect: Schema.optional(LocalWorkspacePolicyEffectSchema),17} from "@executor/platform-sdk/schema";
23 approvalMode: Schema.optional(LocalWorkspacePolicyApprovalModeSchema),18import * as Schema from "effect/Schema";
24 priority: Schema.optional(Schema.Number),
25 enabled: Schema.optional(Schema.Boolean),
26});
27
28export const CreatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema;
29
30export type CreatePolicyPayload = typeof CreatePolicyPayloadSchema.Type;
31 19
32export const UpdatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema;20export type {
21 CreatePolicyPayload,
22 UpdatePolicyPayload,
23} from "@executor/platform-sdk/policies/contracts";
33 24
34export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type;25export {
26 CreatePolicyPayloadSchema,
27 UpdatePolicyPayloadSchema,
28};
35 29
36const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);30const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
37const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema);31const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema);
packages/platform/api/src/policies/http.tsrenamed from packages/platform/control-plane/src/api/policies/http.ts+14−7
@@ -6,7 +6,7 @@ import { · 5 unmodified lines
6 listPolicies,6 listPolicies,
7 removePolicy,7 removePolicy,
8 updatePolicy,8 updatePolicy,
9} from "../../runtime/policy/policies-operations";9} from "@executor/platform-sdk";
10 10
11import { ControlPlaneApi } from "../api";11import { ControlPlaneApi } from "../api";
12import { resolveRequestedLocalWorkspace } from "../local-context";12import { resolveRequestedLocalWorkspace } from "../local-context";
@@ -18,13 +18,14 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 5 unmodified lines
18 handlers18 handlers
19 .handle("list", ({ path }) =>19 .handle("list", ({ path }) =>
20 resolveRequestedLocalWorkspace("policies.list", path.workspaceId).pipe(20 resolveRequestedLocalWorkspace("policies.list", path.workspaceId).pipe(
21 Effect.zipRight(21 Effect.zipRight(listPolicies(path.workspaceId)),
22 listPolicies(path.workspaceId),
23 ),
24 ),22 ),
25 )23 )
26 .handle("create", ({ path, payload }) =>24 .handle("create", ({ path, payload }) =>
27 resolveRequestedLocalWorkspace("policies.create", path.workspaceId).pipe(25 resolveRequestedLocalWorkspace(
26 "policies.create",
27 path.workspaceId,
28 ).pipe(
28 Effect.zipRight(29 Effect.zipRight(
29 createPolicy({ workspaceId: path.workspaceId, payload }),30 createPolicy({ workspaceId: path.workspaceId, payload }),
30 ),31 ),
@@ -41,7 +42,10 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 10 unmodified lines
41 ),42 ),
42 )43 )
43 .handle("update", ({ path, payload }) =>44 .handle("update", ({ path, payload }) =>
44 resolveRequestedLocalWorkspace("policies.update", path.workspaceId).pipe(45 resolveRequestedLocalWorkspace(
46 "policies.update",
47 path.workspaceId,
48 ).pipe(
45 Effect.zipRight(49 Effect.zipRight(
46 updatePolicy({50 updatePolicy({
47 workspaceId: path.workspaceId,51 workspaceId: path.workspaceId,
@@ -52,7 +56,10 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 4 unmodified lines
52 ),56 ),
53 )57 )
54 .handle("remove", ({ path }) =>58 .handle("remove", ({ path }) =>
55 resolveRequestedLocalWorkspace("policies.remove", path.workspaceId).pipe(59 resolveRequestedLocalWorkspace(
60 "policies.remove",
61 path.workspaceId,
62 ).pipe(
56 Effect.zipRight(63 Effect.zipRight(
57 removePolicy({64 removePolicy({
58 workspaceId: path.workspaceId,65 workspaceId: path.workspaceId,
packages/platform/api/src/sources/api.tsrenamed from packages/platform/control-plane/src/api/sources/api.ts+43−162
@@ -1,189 +1,70 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import {2import {
3 SourceOauthClientInputSchema,3 ConnectSourceBatchPayloadSchema,
4 ExecutionInteractionIdSchema,4 ConnectSourceBatchResultSchema,
5 JsonObjectSchema,5 ConnectSourcePayloadSchema,
6 ConnectSourceResultSchema,
7 CreateSourcePayloadSchema,
8 CreateWorkspaceOauthClientPayloadSchema,
9 CredentialOauthCompleteUrlParamsSchema,
10 CredentialPageUrlParamsSchema,
11 CredentialSubmitPayloadSchema,
12 DiscoverSourcePayloadSchema,
13 UpdateSourcePayloadSchema,
14 WorkspaceOauthClientQuerySchema,
15} from "@executor/platform-sdk/sources/contracts";
16import {
17 ControlPlaneBadRequestError,
18 ControlPlaneForbiddenError,
19 ControlPlaneNotFoundError,
20 ControlPlaneStorageError,
21 ControlPlaneUnauthorizedError,
22} from "@executor/platform-sdk/errors";
23import {
6 ProviderAuthGrantIdSchema,24 ProviderAuthGrantIdSchema,
7 SourceAuthSchema,
8 SourceAuthSessionIdSchema,
9 SourceDiscoveryResultSchema,25 SourceDiscoveryResultSchema,
10 SourceIdSchema,26 SourceIdSchema,
11 SourceInspectionDiscoverPayloadSchema,27 SourceInspectionDiscoverPayloadSchema,
12 SourceInspectionDiscoverResultSchema,28 SourceInspectionDiscoverResultSchema,
13 SourceInspectionSchema,29 SourceInspectionSchema,
14 SourceInspectionToolDetailSchema,30 SourceInspectionToolDetailSchema,
15 SourceImportAuthPolicySchema,
16 SourceKindSchema,
17 SourceProbeAuthSchema,
18 SourceSchema,31 SourceSchema,
19 SourceStatusSchema,
20 WorkspaceOauthClientIdSchema,32 WorkspaceOauthClientIdSchema,
21 WorkspaceOauthClientSchema,33 WorkspaceOauthClientSchema,
22 WorkspaceIdSchema,34 WorkspaceIdSchema,
23} from "#schema";35} from "@executor/platform-sdk/schema";
24import * as Schema from "effect/Schema";36import * as Schema from "effect/Schema";
25 37
26import {38export type {
27 ControlPlaneBadRequestError,39 ConnectSourceBatchPayload,
28 ControlPlaneForbiddenError,40 ConnectSourceBatchResult,
29 ControlPlaneNotFoundError,41 ConnectSourcePayload,
30 ControlPlaneStorageError,42 ConnectSourceResult,
31 ControlPlaneUnauthorizedError,43 CreateSourcePayload,
32} from "../errors";44 CreateWorkspaceOauthClientPayload,
33import {45 DiscoverSourcePayload,
34 OptionalTrimmedNonEmptyStringSchema,46 UpdateSourcePayload,
35 TrimmedNonEmptyStringSchema,47} from "@executor/platform-sdk/sources/contracts";
36} from "../string-schemas";
37import {
38 ConnectSourcePayloadSchema,
39 type ConnectSourcePayload,
40} from "../../runtime/sources/source-adapters";
41
42const createSourcePayloadRequiredSchema = Schema.Struct({
43 name: TrimmedNonEmptyStringSchema,
44 kind: SourceKindSchema,
45 endpoint: TrimmedNonEmptyStringSchema,
46});
47
48const 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
60export const CreateSourcePayloadSchema = Schema.extend(
61 createSourcePayloadRequiredSchema,
62 createSourcePayloadOptionalSchema,
63);
64
65export type CreateSourcePayload = typeof CreateSourcePayloadSchema.Type;
66export type CreateWorkspaceOauthClientPayload =
67 typeof CreateWorkspaceOauthClientPayloadSchema.Type;
68
69export 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
83export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type;
84
85const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
86const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
87const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);
88
89const CredentialPageUrlParamsSchema = Schema.Struct({
90 interactionId: ExecutionInteractionIdSchema,
91});
92
93const CredentialSubmitPayloadSchema = Schema.Struct({
94 action: Schema.optional(Schema.Literal("submit", "continue", "cancel")),
95 token: Schema.optional(Schema.String),
96});
97
98const 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
105const WorkspaceOauthClientQuerySchema = Schema.Struct({
106 providerKey: Schema.String,
107});
108
109const CreateWorkspaceOauthClientPayloadSchema = Schema.Struct({
110 providerKey: Schema.String,
111 label: Schema.optional(Schema.NullOr(Schema.String)),
112 oauthClient: SourceOauthClientInputSchema,
113});
114
115const oauthClientIdParam = HttpApiSchema.param("oauthClientId", WorkspaceOauthClientIdSchema);
116const grantIdParam = HttpApiSchema.param("grantId", ProviderAuthGrantIdSchema);
117
118const 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
127const ConnectSourceBatchPayloadSchema = Schema.Struct({
128 workspaceOauthClientId: WorkspaceOauthClientIdSchema,
129 sources: Schema.Array(ConnectGoogleDiscoveryBatchSourceSchema),
130});
131
132const 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
148const DiscoverSourcePayloadSchema = Schema.Struct({
149 url: TrimmedNonEmptyStringSchema,
150 probeAuth: Schema.optional(SourceProbeAuthSchema),
151});
152
153export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type;
154
155const 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
173export type ConnectSourceResult = typeof ConnectSourceResultSchema.Type;
174export type ConnectSourceBatchPayload = typeof ConnectSourceBatchPayloadSchema.Type;
175export type ConnectSourceBatchResult = typeof ConnectSourceBatchResultSchema.Type;
176 48
177export {49export {
178 ConnectSourcePayloadSchema,
179 ConnectSourceBatchPayloadSchema,50 ConnectSourceBatchPayloadSchema,
180 ConnectSourceBatchResultSchema,51 ConnectSourceBatchResultSchema,
52 ConnectSourcePayloadSchema,
181 ConnectSourceResultSchema,53 ConnectSourceResultSchema,
54 CreateSourcePayloadSchema,
182 CreateWorkspaceOauthClientPayloadSchema,55 CreateWorkspaceOauthClientPayloadSchema,
183 DiscoverSourcePayloadSchema,56 DiscoverSourcePayloadSchema,
57 UpdateSourcePayloadSchema,
184};58};
185 59
186export type { ConnectSourcePayload };60const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
61const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
62const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);
63const oauthClientIdParam = HttpApiSchema.param(
64 "oauthClientId",
65 WorkspaceOauthClientIdSchema,
66);
67const grantIdParam = HttpApiSchema.param("grantId", ProviderAuthGrantIdSchema);
187 68
188const HtmlSchema = HttpApiSchema.Text({69const HtmlSchema = HttpApiSchema.Text({
189 contentType: "text/html",70 contentType: "text/html",
packages/platform/api/src/sources/http.tsrenamed from packages/platform/control-plane/src/api/sources/http.ts+170−96
@@ -3,32 +3,32 @@ import { · 2 unmodified lines
3 HttpServerRequest,3 HttpServerRequest,
4 HttpServerResponse,4 HttpServerResponse,
5} from "@effect/platform";5} from "@effect/platform";
6import type { ExecutionInteraction, Source, WorkspaceId } from "#schema";6import type {
7 ExecutionInteraction,
8 Source,
9 WorkspaceId,
10} from "@executor/platform-sdk/schema";
7import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
8 12
9import {13import {
14 completeSourceCredentialSetup,
10 createSource,15 createSource,
11 getSource,16 discoverSource,
12 listSources,
13 removeSource,
14 updateSource,
15} from "../../runtime/sources/sources-operations";
16import {
17 discoverSourceInspectionTools,17 discoverSourceInspectionTools,
18 getSource,
19 getSourceCredentialInteraction,
18 getSourceInspection,20 getSourceInspection,
19 getSourceInspectionToolDetail,21 getSourceInspectionToolDetail,
20} from "../../runtime/sources/source-inspection";22 listSources,
21import {23 removeSource,
22 completeSourceCredentialSetup,
23 getSourceCredentialInteraction,
24 submitSourceCredentialInteraction,24 submitSourceCredentialInteraction,
25} from "../../runtime/local/operations";25 updateSource,
26import { discoverSource } from "../../runtime/sources/source-discovery";26} from "@executor/platform-sdk";
27import { sourceAdapterRequiresInteractiveConnect } from "../../runtime/sources/source-adapters";
28import {27import {
28 sourceAdapterRequiresInteractiveConnect,
29 RuntimeSourceAuthServiceTag,29 RuntimeSourceAuthServiceTag,
30 type ExecutorAddSourceInput,30 type ExecutorAddSourceInput,
31} from "../../runtime/sources/source-auth-service";31} from "@executor/platform-sdk/runtime";
32 32
33import {33import {
34 ControlPlaneBadRequestError,34 ControlPlaneBadRequestError,
@@ -58,7 +58,10 @@ const readHeader = (headers: unknown, name: string): string | null => { · 23 unmodified lines
58 return null;58 return null;
59};59};
60 60
61const resolveRequestOrigin = (request: { url: string; headers: unknown }): string | null => {61const resolveRequestOrigin = (request: {
62 url: string;
63 headers: unknown;
64}): string | null => {
62 try {65 try {
63 return new URL(request.url).origin;66 return new URL(request.url).origin;
64 } catch {67 } catch {
@@ -69,7 +72,8 @@ const resolveRequestOrigin = (request: { url: string; headers: unknown }): strin · 4 unmodified lines
69 }72 }
70 73
71 const forwardedProto = readHeader(request.headers, "x-forwarded-proto");74 const forwardedProto = readHeader(request.headers, "x-forwarded-proto");
72 const protocol = forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http";75 const protocol =
76 forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http";
73 return `${protocol}://${host}`;77 return `${protocol}://${host}`;
74 }78 }
75};79};
@@ -86,7 +90,8 @@ const toBadRequestError = (operation: string, cause: unknown) => { · 10 unmodified lines
86const isInteractiveConnectPayload = (90const isInteractiveConnectPayload = (
87 payload: ConnectSourcePayload,91 payload: ConnectSourcePayload,
88): payload is Extract<ConnectSourcePayload, { kind?: "mcp" }> =>92): payload is Extract<ConnectSourcePayload, { kind?: "mcp" }> =>
89 payload.kind === undefined || sourceAdapterRequiresInteractiveConnect(payload.kind);93 payload.kind === undefined ||
94 sourceAdapterRequiresInteractiveConnect(payload.kind);
90 95
91const escapeHtml = (value: string): string =>96const escapeHtml = (value: string): string =>
92 value97 value
@@ -415,11 +420,7 @@ const credentialPageDocument = (input: { · 322 unmodified lines
415 420
416 <hr />421 <hr />
417 422
418 ${423 ${input.error ? `<p class="alert">${escapeHtml(input.error)}</p>` : ""}
419 input.error
420 ? `<p class="alert">${escapeHtml(input.error)}</p>`
421 : ""
422 }
423 ${424 ${
424 input.state === "stored"425 input.state === "stored"
425 ? `<div class="result-card success">426 ? `<div class="result-card success">
@@ -431,17 +432,17 @@ const credentialPageDocument = (input: { · 5 unmodified lines
431 <p class="result-heading">Continuing without auth</p>432 <p class="result-heading">Continuing without auth</p>
432 <p>Executor resumed the source connection without stored credentials. You can close this window.</p>433 <p>Executor resumed the source connection without stored credentials. You can close this window.</p>
433 </div>`434 </div>`
434 : input.state === "cancelled"435 : input.state === "cancelled"
435 ? `<div class="result-card neutral">436 ? `<div class="result-card neutral">
436 <p class="result-heading">Request cancelled</p>437 <p class="result-heading">Request cancelled</p>
437 <p>Executor was notified. You can close this window.</p>438 <p>Executor was notified. You can close this window.</p>
438 </div>`439 </div>`
439 : input.state === "inactive"440 : input.state === "inactive"
440 ? `<div class="result-card neutral">441 ? `<div class="result-card neutral">
441 <p class="result-heading">No longer active</p>442 <p class="result-heading">No longer active</p>
442 <p>This credential request has expired. Start a new source connection flow if needed.</p>443 <p>This credential request has expired. Start a new source connection flow if needed.</p>
443 </div>`444 </div>`
444 : `<form method="post">445 : `<form method="post">
445 <div class="field">446 <div class="field">
446 <label for="token">Personal access token</label>447 <label for="token">Personal access token</label>
447 <input448 <input
@@ -474,8 +475,8 @@ const sourceOAuthPopupResultDocument = (input: { · 26 unmodified lines
474 sessionId?: string | null;475 sessionId?: string | null;
475 payload:476 payload:
476 | {477 | {
477 type: "executor:source-oauth-result";478 type: "executor:source-oauth-result";
478 ok: true;479 ok: true;
479 sourceId: Source["id"];480 sourceId: Source["id"];
480 }481 }
481 | {482 | {
@@ -595,9 +596,7 @@ const sourceOAuthPopupResultDocument = (input: { · 113 unmodified lines
595</html>`;596</html>`;
596 597
597const htmlResponse = (html: string, status = 200) =>598const htmlResponse = (html: string, status = 200) =>
598 HttpServerResponse.html(html).pipe(599 HttpServerResponse.html(html).pipe(HttpServerResponse.setStatus(status));
599 HttpServerResponse.setStatus(status),
600 );
601 600
602const credentialErrorResponse = (input: {601const credentialErrorResponse = (input: {
603 title: string;602 title: string;
@@ -619,7 +618,10 @@ const credentialErrorResponse = (input: { · 15 unmodified lines
619 );618 );
620 619
621const credentialErrorStatus = (620const credentialErrorStatus = (
622 error: ControlPlaneBadRequestError | ControlPlaneNotFoundError | ControlPlaneStorageError,621 error:
622 | ControlPlaneBadRequestError
623 | ControlPlaneNotFoundError
624 | ControlPlaneStorageError,
623) =>625) =>
624 error instanceof ControlPlaneNotFoundError626 error instanceof ControlPlaneNotFoundError
625 ? 404627 ? 404
@@ -679,15 +681,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 53 unmodified lines
679 ),681 ),
680 )682 )
681 .handle("connect", ({ path, payload }) =>683 .handle("connect", ({ path, payload }) =>
682 resolveRequestedLocalWorkspace("sources.connect", path.workspaceId).pipe(684 resolveRequestedLocalWorkspace(
685 "sources.connect",
686 path.workspaceId,
687 ).pipe(
683 Effect.flatMap((runtimeLocalWorkspace) =>688 Effect.flatMap((runtimeLocalWorkspace) =>
684 Effect.gen(function* () {689 Effect.gen(function* () {
685 const request = yield* HttpServerRequest.HttpServerRequest;690 const request = yield* HttpServerRequest.HttpServerRequest;
686 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;691 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
687 const baseUrl = resolveRequestOrigin(request);692 const baseUrl = resolveRequestOrigin(request);
688 693
689 if (isInteractiveConnectPayload(payload)) {694 if (isInteractiveConnectPayload(payload)) {
690 return yield* sourceAuthService.connectMcpSource({695 return yield* sourceAuthService.connectMcpSource({
691 workspaceId: path.workspaceId,696 workspaceId: path.workspaceId,
692 actorAccountId: runtimeLocalWorkspace.installation.accountId,697 actorAccountId: runtimeLocalWorkspace.installation.accountId,
693 endpoint: payload.endpoint,698 endpoint: payload.endpoint,
@@ -704,26 +709,29 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 10 unmodified lines
704 });709 });
705 }710 }
706 711
707 return yield* sourceAuthService.addExecutorSource(712 return yield* sourceAuthService.addExecutorSource(
708 ({713 {
709 workspaceId: path.workspaceId,714 workspaceId: path.workspaceId,
710 actorAccountId: runtimeLocalWorkspace.installation.accountId,715 actorAccountId: runtimeLocalWorkspace.installation.accountId,
711 executionId: null,716 executionId: null,
712 interactionId: null,717 interactionId: null,
713 ...(payload as Record<string, unknown>),718 ...(payload as Record<string, unknown>),
714 } as ExecutorAddSourceInput),719 } as ExecutorAddSourceInput,
715 { baseUrl },720 { baseUrl },
716 );721 );
717 }).pipe(722 }).pipe(
718 Effect.catchAll((cause) =>723 Effect.catchAll((cause) =>
719 Effect.fail(toBadRequestError("sources.connect", cause)),724 Effect.fail(toBadRequestError("sources.connect", cause)),
720 ),725 ),
721 )726 ),
722 ),727 ),
723 ),728 ),
724 )729 )
725 .handle("connectBatch", ({ path, payload }) =>730 .handle("connectBatch", ({ path, payload }) =>
726 resolveRequestedLocalWorkspace("sources.connectBatch", path.workspaceId).pipe(731 resolveRequestedLocalWorkspace(
732 "sources.connectBatch",
733 path.workspaceId,
734 ).pipe(
727 Effect.flatMap((runtimeLocalWorkspace) =>735 Effect.flatMap((runtimeLocalWorkspace) =>
728 Effect.gen(function* () {736 Effect.gen(function* () {
729 const request = yield* HttpServerRequest.HttpServerRequest;737 const request = yield* HttpServerRequest.HttpServerRequest;
@@ -748,7 +756,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 18 unmodified lines
748 ),756 ),
749 )757 )
750 .handle("listWorkspaceOauthClients", ({ path, urlParams }) =>758 .handle("listWorkspaceOauthClients", ({ path, urlParams }) =>
751 resolveRequestedLocalWorkspace("sources.listWorkspaceOauthClients", path.workspaceId).pipe(759 resolveRequestedLocalWorkspace(
760 "sources.listWorkspaceOauthClients",
761 path.workspaceId,
762 ).pipe(
752 Effect.flatMap(() =>763 Effect.flatMap(() =>
753 Effect.gen(function* () {764 Effect.gen(function* () {
754 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;765 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
@@ -758,14 +769,19 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines
758 });769 });
759 }).pipe(770 }).pipe(
760 Effect.catchAll((cause) =>771 Effect.catchAll((cause) =>
761 Effect.fail(toBadRequestError("sources.listWorkspaceOauthClients", cause)),772 Effect.fail(
773 toBadRequestError("sources.listWorkspaceOauthClients", cause),
774 ),
762 ),775 ),
763 ),776 ),
764 ),777 ),
765 ),778 ),
766 )779 )
767 .handle("createWorkspaceOauthClient", ({ path, payload }) =>780 .handle("createWorkspaceOauthClient", ({ path, payload }) =>
768 resolveRequestedLocalWorkspace("sources.createWorkspaceOauthClient", path.workspaceId).pipe(781 resolveRequestedLocalWorkspace(
782 "sources.createWorkspaceOauthClient",
783 path.workspaceId,
784 ).pipe(
769 Effect.flatMap(() =>785 Effect.flatMap(() =>
770 Effect.gen(function* () {786 Effect.gen(function* () {
771 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;787 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
@@ -777,44 +793,68 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines
777 });793 });
778 }).pipe(794 }).pipe(
779 Effect.catchAll((cause) =>795 Effect.catchAll((cause) =>
780 Effect.fail(toBadRequestError("sources.createWorkspaceOauthClient", cause)),796 Effect.fail(
797 toBadRequestError(
798 "sources.createWorkspaceOauthClient",
799 cause,
800 ),
801 ),
781 ),802 ),
782 ),803 ),
783 ),804 ),
784 ),805 ),
785 )806 )
786 .handle("removeWorkspaceOauthClient", ({ path }) =>807 .handle("removeWorkspaceOauthClient", ({ path }) =>
787 resolveRequestedLocalWorkspace("sources.removeWorkspaceOauthClient", path.workspaceId).pipe(808 resolveRequestedLocalWorkspace(
809 "sources.removeWorkspaceOauthClient",
810 path.workspaceId,
811 ).pipe(
788 Effect.flatMap(() =>812 Effect.flatMap(() =>
789 Effect.gen(function* () {813 Effect.gen(function* () {
790 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;814 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
791 const removed = yield* sourceAuthService.removeWorkspaceOauthClient({815 const removed =
792 workspaceId: path.workspaceId,816 yield* sourceAuthService.removeWorkspaceOauthClient({
793 oauthClientId: path.oauthClientId,817 workspaceId: path.workspaceId,
794 });818 oauthClientId: path.oauthClientId,
819 });
795 820
796 return { removed };821 return { removed };
797 }).pipe(822 }).pipe(
798 Effect.catchAll((cause) =>823 Effect.catchAll((cause) =>
799 Effect.fail(toBadRequestError("sources.removeWorkspaceOauthClient", cause)),824 Effect.fail(
825 toBadRequestError(
826 "sources.removeWorkspaceOauthClient",
827 cause,
828 ),
829 ),
800 ),830 ),
801 ),831 ),
802 ),832 ),
803 ),833 ),
804 )834 )
805 .handle("removeProviderAuthGrant", ({ path }) =>835 .handle("removeProviderAuthGrant", ({ path }) =>
806 resolveRequestedLocalWorkspace("sources.removeProviderAuthGrant", path.workspaceId).pipe(836 resolveRequestedLocalWorkspace(
837 "sources.removeProviderAuthGrant",
838 path.workspaceId,
839 ).pipe(
807 Effect.flatMap(() =>840 Effect.flatMap(() =>
808 Effect.gen(function* () {841 Effect.gen(function* () {
809 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;842 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
810 const removed = yield* sourceAuthService.removeProviderAuthGrant({843 const removed = yield* sourceAuthService
811 workspaceId: path.workspaceId,844 .removeProviderAuthGrant({
812 grantId: path.grantId,845 workspaceId: path.workspaceId,
813 }).pipe(846 grantId: path.grantId,
814 Effect.catchAll((cause) =>847 })
815 Effect.fail(toBadRequestError("sources.removeProviderAuthGrant", cause)),848 .pipe(
816 ),849 Effect.catchAll((cause) =>
817 );850 Effect.fail(
851 toBadRequestError(
852 "sources.removeProviderAuthGrant",
853 cause,
854 ),
855 ),
856 ),
857 );
818 858
819 return { removed };859 return { removed };
820 }),860 }),
@@ -827,7 +867,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 6 unmodified lines
827 listSources({867 listSources({
828 workspaceId: path.workspaceId,868 workspaceId: path.workspaceId,
829 accountId: runtimeLocalWorkspace.installation.accountId,869 accountId: runtimeLocalWorkspace.installation.accountId,
830 })870 }),
831 ),871 ),
832 ),872 ),
833 )873 )
@@ -838,7 +878,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines
838 workspaceId: path.workspaceId,878 workspaceId: path.workspaceId,
839 accountId: runtimeLocalWorkspace.installation.accountId,879 accountId: runtimeLocalWorkspace.installation.accountId,
840 payload,880 payload,
841 })881 }),
842 ),882 ),
843 ),883 ),
844 )884 )
@@ -849,12 +889,15 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines
849 workspaceId: path.workspaceId,889 workspaceId: path.workspaceId,
850 sourceId: path.sourceId,890 sourceId: path.sourceId,
851 accountId: runtimeLocalWorkspace.installation.accountId,891 accountId: runtimeLocalWorkspace.installation.accountId,
852 })892 }),
853 ),893 ),
854 ),894 ),
855 )895 )
856 .handle("inspection", ({ path }) =>896 .handle("inspection", ({ path }) =>
857 resolveRequestedLocalWorkspace("sources.inspection", path.workspaceId).pipe(897 resolveRequestedLocalWorkspace(
898 "sources.inspection",
899 path.workspaceId,
900 ).pipe(
858 Effect.zipRight(901 Effect.zipRight(
859 getSourceInspection({902 getSourceInspection({
860 workspaceId: path.workspaceId,903 workspaceId: path.workspaceId,
@@ -864,7 +907,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines
864 ),907 ),
865 )908 )
866 .handle("inspectionTool", ({ path }) =>909 .handle("inspectionTool", ({ path }) =>
867 resolveRequestedLocalWorkspace("sources.inspection_tool", path.workspaceId).pipe(910 resolveRequestedLocalWorkspace(
911 "sources.inspection_tool",
912 path.workspaceId,
913 ).pipe(
868 Effect.zipRight(914 Effect.zipRight(
869 getSourceInspectionToolDetail({915 getSourceInspectionToolDetail({
870 workspaceId: path.workspaceId,916 workspaceId: path.workspaceId,
@@ -875,7 +921,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines
875 ),921 ),
876 )922 )
877 .handle("inspectionDiscover", ({ path, payload }) =>923 .handle("inspectionDiscover", ({ path, payload }) =>
878 resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe(924 resolveRequestedLocalWorkspace(
925 "sources.inspection_discover",
926 path.workspaceId,
927 ).pipe(
879 Effect.zipRight(928 Effect.zipRight(
880 discoverSourceInspectionTools({929 discoverSourceInspectionTools({
881 workspaceId: path.workspaceId,930 workspaceId: path.workspaceId,
@@ -893,7 +942,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 11 unmodified lines
893 sourceId: path.sourceId,942 sourceId: path.sourceId,
894 accountId: runtimeLocalWorkspace.installation.accountId,943 accountId: runtimeLocalWorkspace.installation.accountId,
895 payload,944 payload,
896 })945 }),
897 ),946 ),
898 ),947 ),
899 )948 )
@@ -922,9 +971,10 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 22 unmodified lines
922 sourceLabel: interaction.sourceLabel,971 sourceLabel: interaction.sourceLabel,
923 endpoint: interaction.endpoint,972 endpoint: interaction.endpoint,
924 interactionId: interaction.interactionId,973 interactionId: interaction.interactionId,
925 state: interaction.status === "pending" ? "pending" : "inactive",974 state:
975 interaction.status === "pending" ? "pending" : "inactive",
926 }),976 }),
927 )977 ),
928 ),978 ),
929 Effect.catchTags({979 Effect.catchTags({
930 ControlPlaneNotFoundError: (error) =>980 ControlPlaneNotFoundError: (error) =>
@@ -967,14 +1017,14 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 36 unmodified lines
967 ? "Source Credential Stored"1017 ? "Source Credential Stored"
968 : result.kind === "continued"1018 : result.kind === "continued"
969 ? "Source Access Updated"1019 ? "Source Access Updated"
970 : "Source Credential Request Cancelled",1020 : "Source Credential Request Cancelled",
971 eyebrow: "Executor",1021 eyebrow: "Executor",
972 message:1022 message:
973 result.kind === "stored"1023 result.kind === "stored"
974 ? "Executor stored the credential and resumed the source connection."1024 ? "Executor stored the credential and resumed the source connection."
975 : result.kind === "continued"1025 : result.kind === "continued"
976 ? "Executor resumed the source connection without stored credentials."1026 ? "Executor resumed the source connection without stored credentials."
977 : "Executor cancelled the source credential request.",1027 : "Executor cancelled the source credential request.",
978 sourceLabel: result.sourceLabel,1028 sourceLabel: result.sourceLabel,
979 endpoint: result.endpoint,1029 endpoint: result.endpoint,
980 interactionId: urlParams.interactionId,1030 interactionId: urlParams.interactionId,
@@ -985,7 +1035,7 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 4 unmodified lines
985 ? "continued"1035 ? "continued"
986 : "cancelled",1036 : "cancelled",
987 }),1037 }),
988 )1038 ),
989 ),1039 ),
990 Effect.catchTags({1040 Effect.catchTags({
991 ControlPlaneBadRequestError: (error) =>1041 ControlPlaneBadRequestError: (error) =>
@@ -1044,12 +1094,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 52 unmodified lines
1044 htmlResponse(1094 htmlResponse(
1045 sourceOAuthPopupResultDocument({1095 sourceOAuthPopupResultDocument({
1046 title: "OAuth failed",1096 title: "OAuth failed",
1047 message: error instanceof Error ? error.message : "Failed completing OAuth",1097 message:
1098 error instanceof Error
1099 ? error.message
1100 : "Failed completing OAuth",
1048 state: "cancelled",1101 state: "cancelled",
1049 payload: {1102 payload: {
1050 type: "executor:source-oauth-result",1103 type: "executor:source-oauth-result",
1051 ok: false,1104 ok: false,
1052 error: error instanceof Error ? error.message : "Failed completing OAuth",1105 error:
1106 error instanceof Error
1107 ? error.message
1108 : "Failed completing OAuth",
1053 },1109 },
1054 }),1110 }),
1055 500,1111 500,
@@ -1059,18 +1115,22 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines
1059 ),1115 ),
1060 )1116 )
1061 .handle("providerOauthComplete", ({ path, urlParams }) =>1117 .handle("providerOauthComplete", ({ path, urlParams }) =>
1062 resolveRequestedLocalWorkspace("sources.providerOauthComplete", path.workspaceId).pipe(1118 resolveRequestedLocalWorkspace(
1119 "sources.providerOauthComplete",
1120 path.workspaceId,
1121 ).pipe(
1063 Effect.flatMap((runtimeLocalWorkspace) =>1122 Effect.flatMap((runtimeLocalWorkspace) =>
1064 Effect.gen(function* () {1123 Effect.gen(function* () {
1065 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;1124 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
1066 const completed = yield* sourceAuthService.completeProviderOauthCallback({1125 const completed =
1067 workspaceId: path.workspaceId,1126 yield* sourceAuthService.completeProviderOauthCallback({
1068 actorAccountId: runtimeLocalWorkspace.installation.accountId,1127 workspaceId: path.workspaceId,
1069 state: urlParams.state,1128 actorAccountId: runtimeLocalWorkspace.installation.accountId,
1070 code: urlParams.code,1129 state: urlParams.state,
1071 error: urlParams.error,1130 code: urlParams.code,
1072 errorDescription: urlParams.error_description,1131 error: urlParams.error,
1073 });1132 errorDescription: urlParams.error_description,
1133 });
1074 const primarySource = completed.sources[0] ?? null;1134 const primarySource = completed.sources[0] ?? null;
1075 1135
1076 return htmlResponse(1136 return htmlResponse(
@@ -1082,7 +1142,9 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 5 unmodified lines
1082 payload: {1142 payload: {
1083 type: "executor:source-oauth-result",1143 type: "executor:source-oauth-result",
1084 ok: true,1144 ok: true,
1085 sourceId: primarySource?.id ?? ("src_oauth_complete" as Source["id"]),1145 sourceId:
1146 primarySource?.id ??
1147 ("src_oauth_complete" as Source["id"]),
1086 },1148 },
1087 }),1149 }),
1088 );1150 );
@@ -1092,12 +1154,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 3 unmodified lines
1092 htmlResponse(1154 htmlResponse(
1093 sourceOAuthPopupResultDocument({1155 sourceOAuthPopupResultDocument({
1094 title: "OAuth failed",1156 title: "OAuth failed",
1095 message: error instanceof Error ? error.message : "Failed completing OAuth",1157 message:
1158 error instanceof Error
1159 ? error.message
1160 : "Failed completing OAuth",
1096 state: "cancelled",1161 state: "cancelled",
1097 payload: {1162 payload: {
1098 type: "executor:source-oauth-result",1163 type: "executor:source-oauth-result",
1099 ok: false,1164 ok: false,
1100 error: error instanceof Error ? error.message : "Failed completing OAuth",1165 error:
1166 error instanceof Error
1167 ? error.message
1168 : "Failed completing OAuth",
1101 },1169 },
1102 }),1170 }),
1103 500,1171 500,
@@ -1111,12 +1179,18 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 7 unmodified lines
1111 htmlResponse(1179 htmlResponse(
1112 sourceOAuthPopupResultDocument({1180 sourceOAuthPopupResultDocument({
1113 title: "OAuth failed",1181 title: "OAuth failed",
1114 message: error instanceof Error ? error.message : "Failed completing OAuth",1182 message:
1183 error instanceof Error
1184 ? error.message
1185 : "Failed completing OAuth",
1115 state: "cancelled",1186 state: "cancelled",
1116 payload: {1187 payload: {
1117 type: "executor:source-oauth-result",1188 type: "executor:source-oauth-result",
1118 ok: false,1189 ok: false,
1119 error: error instanceof Error ? error.message : "Failed completing OAuth",1190 error:
1191 error instanceof Error
1192 ? error.message
1193 : "Failed completing OAuth",
1120 },1194 },
1121 }),1195 }),
1122 500,1196 500,
packages/platform/api/src/string-schemas.ts+4−0
@@ -0,0 +1,4 @@
1export {
2 OptionalTrimmedNonEmptyStringSchema,
3 TrimmedNonEmptyStringSchema,
4} from "@executor/platform-sdk/string-schemas";
packages/platform/api/tsconfig.jsonrenamed from packages/platform/control-plane/tsconfig.json+1−8
@@ -9,12 +9,5 @@ · 8 unmodified lines
9 "lib": ["ES2022"],9 "lib": ["ES2022"],
10 "types": ["bun-types", "node"]10 "types": ["bun-types", "node"]
11 },11 },
12 "include": [12 "include": ["src/**/*.ts"]
13 "src/**/*.ts",
14 "src/**/*.d.ts"
15 ],
16 "exclude": [
17 "src/**/*.test.ts",
18 "src/persistence/**/*.ts"
19 ]
20}13}
packages/platform/api/vitest.config.tsrenamed from packages/platform/control-plane/vitest.config.ts+0−0

File renamed without changes.

packages/platform/control-plane/CHANGELOG.md+0−13
@@ -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
packages/platform/control-plane/src/api/local/http.ts+0−255
@@ -1,255 +0,0 @@
1import {
2 HttpApiBuilder,
3} from "@effect/platform";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
6
7import { SecretMaterialIdSchema } from "#schema";
8import {
9 getLocalInstallation,
10} from "../../runtime/local/operations";
11import { requireRuntimeLocalWorkspace } from "../../runtime/local/runtime-context";
12import {
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";
22import { RuntimeSourceStoreService } from "../../runtime/sources/source-store";
23import { ControlPlaneStore } from "../../runtime/store";
24import type {
25 CreateSecretResult,
26 InstanceConfig,
27 SecretProvider,
28 UpdateSecretResult,
29} from "./api";
30
31import { ControlPlaneApi } from "../api";
32import {
33 ControlPlaneBadRequestError,
34 ControlPlaneNotFoundError,
35 ControlPlaneStorageError,
36} from "../errors";
37
38const SECRET_STORE_PROVIDER_ENV = "EXECUTOR_SECRET_STORE_PROVIDER";
39
40const 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
78const storageError = (message: string) =>
79 new ControlPlaneStorageError({
80 operation: "secrets",
81 message,
82 details: message,
83 });
84
85export 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);
packages/platform/control-plane/src/index.ts+0−4
@@ -1,4 +0,0 @@
1export * from "./schema/index";
2export * from "./api/index";
3export * from "./runtime/index";
4export * from "./client";
packages/platform/control-plane/src/runtime/control-plane-runtime.test.ts+0−1991
@@ -1,1991 +0,0 @@
1import { createServer } from "node:http";
2import { join } from "node:path";
3
4import { FileSystem } from "@effect/platform";
5import { NodeFileSystem } from "@effect/platform-node";
6import { describe, expect, it } from "@effect/vitest";
7import { assertTrue } from "@effect/vitest/utils";
8import * as Either from "effect/Either";
9import * as Effect from "effect/Effect";
10import * as Fiber from "effect/Fiber";
11import * as Option from "effect/Option";
12
13import {
14 ExecutionIdSchema,
15 ExecutionInteractionIdSchema,
16 type ExecutionInteraction,
17 ProviderAuthGrantIdSchema,
18 SecretMaterialIdSchema,
19 SourceAuthSessionIdSchema,
20 SourceIdSchema,
21 WorkspaceOauthClientIdSchema,
22} from "#schema";
23import type { ToolPath } from "@executor/codemode-core";
24import { createCatalogImportMetadata } from "@executor/source-core";
25import { createGraphqlCatalogFragment } from "@executor/source-graphql";
26
27import {
28 type ControlPlaneRuntime,
29 createControlPlaneRuntime,
30 LiveExecutionManagerService,
31 provideControlPlaneRuntime,
32} from "./index";
33import { createSourceFromPayload } from "./sources/source-definitions";
34import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions";
35import { persistSource } from "./sources/source-store";
36import { withControlPlaneClient } from "./execution/test-http-client";
37import { runtimeEffectError } from "./effect-errors";
38import {
39 resolveLocalWorkspaceContext,
40 writeProjectLocalExecutorConfig,
41} from "./local/config";
42import { writeLocalControlPlaneState } from "./local/control-plane-store";
43import { deriveLocalInstallation } from "./local/installation";
44import {
45 buildLocalSourceArtifact,
46 readLocalSourceArtifact,
47 writeLocalSourceArtifact,
48} from "./local/source-artifacts";
49import { writeLocalWorkspaceState } from "./local/workspace-state";
50
51const 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
69type OpenApiSpecServer = {
70 baseUrl: string;
71 specUrl: string;
72 close: () => Promise<void>;
73};
74
75type 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
94const 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
162const 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
316const 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
333const 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
382const 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
391describe("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});
packages/platform/internal/package.json+22−0
@@ -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}
packages/platform/internal/src/index.ts+1−0
@@ -0,0 +1 @@
1export * from "./tools/executor-admin";
packages/platform/internal/src/tools/executor-admin.ts+692−0
@@ -0,0 +1,692 @@
1import { toTool, type ToolMap } from "@executor/codemode-core";
2import {
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";
16import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";
18import * as Schema from "effect/Schema";
19
20import {
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";
38import {
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";
49import {
50 createLocalSecret,
51 deleteLocalSecret,
52 getLocalInstanceConfig,
53 listLocalSecrets,
54 updateLocalSecret,
55} from "@executor/platform-sdk/local/secrets";
56import {
57 CreatePolicyPayloadSchema,
58 type CreatePolicyPayload,
59 type UpdatePolicyPayload,
60 UpdatePolicyPayloadSchema,
61} from "@executor/platform-sdk/policies/contracts";
62import {
63 createPolicy,
64 getPolicy,
65 listPolicies,
66 removePolicy,
67 updatePolicy,
68} from "@executor/platform-sdk/policies/operations";
69import {
70 CreateWorkspaceOauthClientPayloadSchema,
71 DiscoverSourcePayloadSchema,
72 type CreateWorkspaceOauthClientPayload,
73 UpdateSourcePayloadSchema,
74} from "@executor/platform-sdk/sources/contracts";
75import {
76 discoverSource,
77} from "@executor/platform-sdk/sources/discovery";
78import {
79 discoverSourceInspectionTools,
80 getSourceInspection,
81 getSourceInspectionToolDetail,
82} from "@executor/platform-sdk/sources/inspection";
83import {
84 getSource,
85 listSources,
86 removeSource,
87 updateSource,
88} from "@executor/platform-sdk/sources/operations";
89
90const emptyInputSchema = Schema.standardSchemaV1(Schema.Struct({}));
91const localInstallationOutputSchema = Schema.standardSchemaV1(
92 LocalInstallationSchema,
93);
94const instanceConfigOutputSchema =
95 Schema.standardSchemaV1(InstanceConfigSchema);
96const secretListOutputSchema = Schema.standardSchemaV1(
97 Schema.Array(SecretListItemSchema),
98);
99const createSecretInputSchema = Schema.standardSchemaV1(
100 CreateSecretPayloadSchema,
101);
102const updateSecretInputSchema = Schema.standardSchemaV1(
103 Schema.Struct({
104 secretId: Schema.String,
105 payload: UpdateSecretPayloadSchema,
106 }),
107);
108const removeSecretInputSchema = Schema.standardSchemaV1(
109 Schema.Struct({
110 secretId: Schema.String,
111 }),
112);
113const removeResultSchema = Schema.standardSchemaV1(
114 Schema.Struct({
115 removed: Schema.Boolean,
116 }),
117);
118const listSourcesOutputSchema = Schema.standardSchemaV1(
119 Schema.Array(SourceSchema),
120);
121const getSourceInputSchema = Schema.standardSchemaV1(
122 Schema.Struct({
123 sourceId: SourceIdSchema,
124 }),
125);
126const updateSourceInputSchema = Schema.standardSchemaV1(
127 Schema.Struct({
128 sourceId: SourceIdSchema,
129 payload: UpdateSourcePayloadSchema,
130 }),
131);
132const discoverSourceInputSchema = Schema.standardSchemaV1(
133 DiscoverSourcePayloadSchema,
134);
135const inspectToolInputSchema = Schema.standardSchemaV1(
136 Schema.Struct({
137 sourceId: SourceIdSchema,
138 toolPath: Schema.String,
139 }),
140);
141const inspectDiscoverInputSchema = Schema.standardSchemaV1(
142 Schema.Struct({
143 sourceId: SourceIdSchema,
144 payload: SourceInspectionDiscoverPayloadSchema,
145 }),
146);
147const listPoliciesOutputSchema = Schema.standardSchemaV1(
148 Schema.Array(LocalWorkspacePolicySchema),
149);
150const policyIdInputSchema = Schema.standardSchemaV1(
151 Schema.Struct({
152 policyId: Schema.String,
153 }),
154);
155const createPolicyInputSchema = Schema.standardSchemaV1(
156 CreatePolicyPayloadSchema,
157);
158const updatePolicyInputSchema = Schema.standardSchemaV1(
159 Schema.Struct({
160 policyId: Schema.String,
161 payload: UpdatePolicyPayloadSchema,
162 }),
163);
164const workspaceOauthClientListInputSchema = Schema.standardSchemaV1(
165 Schema.Struct({
166 providerKey: Schema.String,
167 }),
168);
169const workspaceOauthClientListOutputSchema = Schema.standardSchemaV1(
170 Schema.Array(WorkspaceOauthClientSchema),
171);
172const createWorkspaceOauthClientInputSchema = Schema.standardSchemaV1(
173 CreateWorkspaceOauthClientPayloadSchema,
174);
175const removeWorkspaceOauthClientInputSchema = Schema.standardSchemaV1(
176 Schema.Struct({
177 oauthClientId: Schema.String,
178 }),
179);
180const removeProviderGrantInputSchema = Schema.standardSchemaV1(
181 Schema.Struct({
182 grantId: Schema.String,
183 }),
184);
185const sourceOutputSchema = Schema.standardSchemaV1(SourceSchema);
186const sourceInspectionOutputSchema = Schema.standardSchemaV1(
187 SourceInspectionSchema,
188);
189const sourceInspectionToolOutputSchema = Schema.standardSchemaV1(
190 SourceInspectionToolDetailSchema,
191);
192const sourceInspectionDiscoverOutputSchema = Schema.standardSchemaV1(
193 SourceInspectionDiscoverResultSchema,
194);
195const sourceDiscoveryOutputSchema = Schema.standardSchemaV1(
196 SourceDiscoveryResultSchema,
197);
198const localWorkspacePolicyOutputSchema = Schema.standardSchemaV1(
199 LocalWorkspacePolicySchema,
200);
201
202const 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
226const 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
247const 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
263export 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};
packages/platform/internal/tsconfig.json+13−0
@@ -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}
packages/platform/sdk/package.jsonrenamed from packages/platform/control-plane/package.json+36−25
@@ -1,54 +1,65 @@
1{1{
2 "name": "@executor/control-plane",2 "name": "@executor/platform-sdk",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 ".": "./src/index.ts",6 ".": "./src/index.ts",
7 "./client": "./src/client.ts",7 "./errors": "./src/errors.ts",
8 "./schema": "./src/schema/index.ts"8 "./string-schemas": "./src/string-schemas.ts",
9 "./executions/contracts": "./src/executions/contracts.ts",
10 "./schema": "./src/schema/index.ts",
11 "./runtime": "./src/runtime/index.ts",
12 "./local/contracts": "./src/local/contracts.ts",
13 "./local/operations": "./src/local/operations.ts",
14 "./local/secrets": "./src/local/secrets.ts",
15 "./policies/contracts": "./src/policies/contracts.ts",
16 "./policies/operations": "./src/policies/operations.ts",
17 "./sources/contracts": "./src/sources/contracts.ts",
18 "./sources/discovery": "./src/sources/discovery.ts",
19 "./sources/inspection": "./src/sources/inspection.ts",
20 "./sources/operations": "./src/sources/operations.ts"
9 },21 },
10 "imports": {22 "imports": {
11 "#schema": "./src/schema/index.ts",23 "#runtime": "./src/runtime/index.ts",
12 "#domain": "./src/domain/index.ts",24 "#schema": "./src/schema/index.ts"
13 "#api": "./src/api/index.ts",
14 "#runtime": "./src/runtime/index.ts"
15 },25 },
16 "scripts": {26 "scripts": {
17 "fixture:release:capture": "bun run ./src/runtime/local/capture-release-workspace-fixture.ts",27 "fixture:release:capture": "bun run ./src/runtime/local/capture-release-workspace-fixture.ts",
18 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
19 "test": "vitest run",28 "test": "vitest run",
20 "test:watch": "vitest"29 "test:watch": "vitest",
30 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
21 },31 },
22 "dependencies": {32 "dependencies": {
23 "@effect/platform": "catalog:",33 "@effect/platform": "catalog:",
24 "@executor/source-core": "workspace:*",34 "@effect/platform-node": "catalog:",
25 "@executor/source-builtins": "workspace:*",
26 "@executor/auth-mcp-oauth": "workspace:*",35 "@executor/auth-mcp-oauth": "workspace:*",
27 "@executor/auth-oauth2": "workspace:*",36 "@executor/auth-oauth2": "workspace:*",
28 "@executor/codemode-core": "workspace:*",37 "@executor/codemode-core": "workspace:*",
29 "@executor/source-google-discovery": "workspace:*",
30 "@executor/source-graphql": "workspace:*",
31 "@executor/ir": "workspace:*",38 "@executor/ir": "workspace:*",
32 "@executor/source-mcp": "workspace:*",
33 "@executor/source-openapi": "workspace:*",
34 "@executor/runtime-deno-subprocess": "workspace:*",39 "@executor/runtime-deno-subprocess": "workspace:*",
35 "@executor/runtime-quickjs": "workspace:*",40 "@executor/runtime-quickjs": "workspace:*",
36 "@executor/runtime-ses": "workspace:*",41 "@executor/runtime-ses": "workspace:*",
37 "effect": "catalog:",42 "@executor/source-core": "workspace:*",
38 "graphql": "^16.12.0",43 "@executor/source-builtins": "workspace:*",
39 "jsonc-parser": "3.3.1",44 "@executor/source-google-discovery": "workspace:*",
40 "postgres": "^3.4.8",45 "@executor/source-graphql": "workspace:*",
41 "prettier": "^3.8.1",46 "@executor/source-mcp": "workspace:*",
42 "typescript": "^5.9.3"47 "@executor/source-openapi": "workspace:*",
48 "effect": "catalog:"
43 },49 },
44 "devDependencies": {50 "devDependencies": {
45 "@effect/vitest": "catalog:",51 "@effect/vitest": "catalog:",
46 "@types/node": "catalog:",52 "@executor/platform-api": "workspace:*",
47 "@effect/platform-node": "catalog:",53 "@executor/platform-internal": "workspace:*",
48 "@modelcontextprotocol/sdk": "^1.26.0",54 "@modelcontextprotocol/sdk": "^1.26.0",
55 "@types/node": "catalog:",
49 "bun-types": "catalog:",56 "bun-types": "catalog:",
50 "vitest": "catalog:",57 "graphql": "^16.12.0",
51 "zod": "catalog:"58 "jsonc-parser": "3.3.1",
59 "postgres": "^3.4.8",
60 "prettier": "^3.8.1",
61 "typescript": "^5.9.3",
62 "vitest": "catalog:"
52 },63 },
53 "version": "0.0.0"64 "version": "0.0.0"
54}65}
packages/platform/sdk/src/errors.tsrenamed from packages/platform/control-plane/src/api/errors.ts+0−0

File renamed without changes.

packages/platform/sdk/src/executions/contracts.ts+21−0
@@ -0,0 +1,21 @@
1import * as Schema from "effect/Schema";
2
3import { TrimmedNonEmptyStringSchema } from "../string-schemas";
4
5export const CreateExecutionPayloadSchema = Schema.Struct({
6 code: TrimmedNonEmptyStringSchema,
7 interactionMode: Schema.optional(
8 Schema.Literal("live", "live_form", "detach"),
9 ),
10});
11
12export type CreateExecutionPayload = typeof CreateExecutionPayloadSchema.Type;
13
14export const ResumeExecutionPayloadSchema = Schema.Struct({
15 responseJson: Schema.optional(Schema.String),
16 interactionMode: Schema.optional(
17 Schema.Literal("live", "live_form", "detach"),
18 ),
19});
20
21export type ResumeExecutionPayload = typeof ResumeExecutionPayloadSchema.Type;
packages/platform/sdk/src/index.ts+12−0
@@ -0,0 +1,12 @@
1export * from "./errors";
2export * from "./string-schemas";
3export * from "./executions/contracts";
4export * from "./local/contracts";
5export * from "./local/operations";
6export * from "./local/secrets";
7export * from "./policies/contracts";
8export * from "./policies/operations";
9export * from "./sources/contracts";
10export * from "./sources/discovery";
11export * from "./sources/inspection";
12export * from "./sources/operations";
packages/platform/sdk/src/local/contracts.tsrenamed from packages/platform/control-plane/src/api/local/api.ts+1−52
@@ -1,13 +1,6 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";1import { SecretMaterialPurposeSchema } from "../schema";
2import { LocalInstallationSchema, SecretMaterialPurposeSchema } from "#schema";
3import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
4 3
5import {
6 ControlPlaneBadRequestError,
7 ControlPlaneNotFoundError,
8 ControlPlaneStorageError,
9} from "../errors";
10
11export const SecretProviderSchema = Schema.Struct({4export const SecretProviderSchema = Schema.Struct({
12 id: Schema.String,5 id: Schema.String,
13 name: Schema.String,6 name: Schema.String,
@@ -23,8 +16,6 @@ export const InstanceConfigSchema = Schema.Struct({ · 9 unmodified lines
23export type SecretProvider = typeof SecretProviderSchema.Type;16export type SecretProvider = typeof SecretProviderSchema.Type;
24export type InstanceConfig = typeof InstanceConfigSchema.Type;17export type InstanceConfig = typeof InstanceConfigSchema.Type;
25 18
26// -- Secrets CRUD schemas ---------------------------------------------------
27
28export const SecretLinkedSourceSchema = Schema.Struct({19export const SecretLinkedSourceSchema = Schema.Struct({
29 sourceId: Schema.String,20 sourceId: Schema.String,
30 sourceName: Schema.String,21 sourceName: Schema.String,
@@ -87,45 +78,3 @@ export const DeleteSecretResultSchema = Schema.Struct({ · 56 unmodified lines
87});78});
88 79
89export type DeleteSecretResult = typeof DeleteSecretResultSchema.Type;80export type DeleteSecretResult = typeof DeleteSecretResultSchema.Type;
90
91// -- API group --------------------------------------------------------------
92
93export 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") {}
packages/platform/sdk/src/local/operations.tsrenamed from packages/platform/control-plane/src/runtime/local/operations.ts+112−94
@@ -3,28 +3,29 @@ import type { · 2 unmodified lines
3 ExecutionInteraction,3 ExecutionInteraction,
4 Source,4 Source,
5 WorkspaceId,5 WorkspaceId,
6} from "#schema";6} from "../schema";
7import {7import {
8 SourceIdSchema,8 SourceIdSchema,
9 WorkspaceIdSchema,9 WorkspaceIdSchema,
10} from "#schema";10} from "../schema";
11import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
12import * as Option from "effect/Option";12import * as Option from "effect/Option";
13 13
14import { submitExecutionInteractionResponse } from "../execution/service";14import { submitExecutionInteractionResponse } from "../runtime/execution/service";
15import { LiveExecutionManagerService } from "../execution/live";15import { LiveExecutionManagerService } from "../runtime/execution/live";
16import { operationErrors } from "../runtime/policy/operation-errors";
17import { resolveLocalWorkspaceContext } from "../runtime/local/config";
16import {18import {
17 operationErrors,19 InstallationStore,
18} from "../policy/operation-errors";20 LocalInstallationStore,
19import { resolveLocalWorkspaceContext } from "./config";21} from "../runtime/local/storage";
20import { InstallationStore, LocalInstallationStore } from "./storage";22import { getRuntimeLocalWorkspaceOption } from "../runtime/local/runtime-context";
21import { getRuntimeLocalWorkspaceOption } from "./runtime-context";23import { RuntimeSourceAuthServiceTag } from "../runtime/sources/source-auth-service";
22import { RuntimeSourceAuthServiceTag } from "../sources/source-auth-service";
23import {24import {
24 createSourceCredentialSelectionBearerContent,25 createSourceCredentialSelectionBearerContent,
25 createSourceCredentialSelectionNoneContent,26 createSourceCredentialSelectionNoneContent,
26} from "../sources/source-credential-interactions";27} from "../runtime/sources/source-credential-interactions";
27import { ControlPlaneStore } from "../store";28import { ControlPlaneStore } from "../runtime/store";
28 29
29const localOps = {30const localOps = {
30 installation: operationErrors("local.installation.get"),31 installation: operationErrors("local.installation.get"),
@@ -59,13 +60,15 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 28 unmodified lines
59 return trimmed.length > 0 ? trimmed : null;60 return trimmed.length > 0 ? trimmed : null;
60};61};
61 62
62const decodeSourceCredentialInteraction = (63export const decodeSourceCredentialInteraction = (
63 interaction: ExecutionInteraction,64 interaction: ExecutionInteraction,
64): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => {65): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => {
65 try {66 try {
66 if (interaction.purpose !== "source_connect_oauth2"67 if (
67 && interaction.purpose !== "source_connect_secret"68 interaction.purpose !== "source_connect_oauth2" &&
68 && interaction.purpose !== "elicitation") {69 interaction.purpose !== "source_connect_secret" &&
70 interaction.purpose !== "elicitation"
71 ) {
69 return null;72 return null;
70 }73 }
71 74
@@ -80,18 +83,25 @@ const decodeSourceCredentialInteraction = ( · 8 unmodified lines
80 return null;83 return null;
81 }84 }
82 85
83 if (payload.path !== "executor.sources.add" || args.kind !== "openapi") {86 if (payload.path !== "executor.sources.add") {
84 return null;87 return null;
85 }88 }
86 const effectivePurpose = interaction.purpose === "elicitation"89 const effectivePurpose =
87 ? elicitation.mode === "url"90 interaction.purpose === "elicitation"
88 ? "source_connect_oauth2"91 ? elicitation.mode === "url"
89 : "source_connect_secret"92 ? "source_connect_oauth2"
90 : interaction.purpose;93 : "source_connect_secret"
91 if (effectivePurpose === "source_connect_oauth2" && elicitation.mode !== "url") {94 : interaction.purpose;
95 if (
96 effectivePurpose === "source_connect_oauth2" &&
97 elicitation.mode !== "url"
98 ) {
92 return null;99 return null;
93 }100 }
94 if (effectivePurpose === "source_connect_secret" && elicitation.mode !== "form") {101 if (
102 effectivePurpose === "source_connect_secret" &&
103 elicitation.mode !== "form"
104 ) {
95 return null;105 return null;
96 }106 }
97 107
@@ -127,45 +137,49 @@ const loadSourceCredentialInteraction = (input: { · 29 unmodified lines
127 const store = yield* ControlPlaneStore;137 const store = yield* ControlPlaneStore;
128 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;138 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
129 139
130 const stored = yield* store.executionInteractions.getById(input.interactionId).pipe(140 const stored = yield* store.executionInteractions
131 Effect.mapError((error) =>141 .getById(input.interactionId)
132 input.operation.unknownStorage(142 .pipe(
133 error,143 Effect.mapError((error) =>
134 `Failed loading execution interaction ${input.interactionId}`,144 input.operation.unknownStorage(
145 error,
146 `Failed loading execution interaction ${input.interactionId}`,
147 ),
135 ),148 ),
136 ),149 );
137 );
138 150
139 if (Option.isNone(stored)) {151 if (Option.isNone(stored)) {
140 return yield* input.operation.notFound(152 return yield* input.operation.notFound(
141 "Source credential request not found",153 "Source credential request not found",
142 `interactionId=${input.interactionId}`,154 `interactionId=${input.interactionId}`,
143 );155 );
144 }156 }
145 157
146 const decoded = decodeSourceCredentialInteraction(stored.value);158 const decoded = decodeSourceCredentialInteraction(stored.value);
147 if (159 if (
148 decoded === null160 decoded === null ||
149 || decoded.workspaceId !== input.workspaceId161 decoded.workspaceId !== input.workspaceId ||
150 || decoded.sourceId !== input.sourceId162 decoded.sourceId !== input.sourceId
151 ) {163 ) {
152 return yield* input.operation.notFound(164 return yield* input.operation.notFound(
153 "Source credential request not found",165 "Source credential request not found",
154 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`,166 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`,
155 );167 );
156 }168 }
157 169
158 const source = yield* sourceAuthService.getSourceById({170 const source = yield* sourceAuthService
159 workspaceId: input.workspaceId,171 .getSourceById({
160 sourceId: input.sourceId,172 workspaceId: input.workspaceId,
161 }).pipe(173 sourceId: input.sourceId,
162 Effect.mapError((error) =>174 })
163 input.operation.unknownStorage(175 .pipe(
164 error,176 Effect.mapError((error) =>
165 `Failed loading source ${input.sourceId}`,177 input.operation.unknownStorage(
178 error,
179 `Failed loading source ${input.sourceId}`,
180 ),
166 ),181 ),
167 ),182 );
168 );
169 183
170 return {184 return {
171 ...decoded,185 ...decoded,
@@ -177,23 +191,23 @@ const loadSourceCredentialInteraction = (input: { · 5 unmodified lines
177export const getLocalInstallation = () =>191export const getLocalInstallation = () =>
178 Effect.gen(function* () {192 Effect.gen(function* () {
179 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();193 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
180 const context = runtimeLocalWorkspace?.context194 const context =
181 ?? (yield* resolveLocalWorkspaceContext().pipe(195 runtimeLocalWorkspace?.context ??
196 (yield* resolveLocalWorkspaceContext().pipe(
182 Effect.mapError((error) =>197 Effect.mapError((error) =>
183 localOps.installation.unknownStorage(198 localOps.installation.unknownStorage(
184 error,199 error,
185 "Failed resolving local workspace context",200 "Failed resolving local workspace context",
186 ),201 ),
187 ),202 ),
188 ));203 ));
189 204
190 const installationStore = runtimeLocalWorkspace !== null205 const installationStore =
191 ? yield* InstallationStore206 runtimeLocalWorkspace !== null
192 : LocalInstallationStore;207 ? yield* InstallationStore
208 : LocalInstallationStore;
193 209
194 return yield* installationStore.load(210 return yield* installationStore.load(context);
195 context,
196 );
197 });211 });
198 212
199export const getSourceCredentialInteraction = (input: {213export const getSourceCredentialInteraction = (input: {
@@ -223,9 +237,9 @@ export const submitSourceCredentialInteraction = (input: { · 23 unmodified lines
223 237
224 if (interaction.status !== "pending") {238 if (interaction.status !== "pending") {
225 return yield* localOps.sourceCredentialSubmit.badRequest(239 return yield* localOps.sourceCredentialSubmit.badRequest(
226 "Source credential request is no longer active",240 "Source credential request is no longer active",
227 `interactionId=${interaction.interactionId} status=${interaction.status}`,241 `interactionId=${interaction.interactionId} status=${interaction.status}`,
228 );242 );
229 }243 }
230 244
231 const liveExecutionManager = yield* LiveExecutionManagerService;245 const liveExecutionManager = yield* LiveExecutionManagerService;
@@ -249,15 +263,15 @@ export const submitSourceCredentialInteraction = (input: { · 17 unmodified lines
249 localOps.sourceCredentialSubmit.unknownStorage(263 localOps.sourceCredentialSubmit.unknownStorage(
250 error,264 error,
251 `Failed resuming execution for interaction ${interaction.interactionId}`,265 `Failed resuming execution for interaction ${interaction.interactionId}`,
252 )266 ),
253 ),267 ),
254 );268 );
255 269
256 if (!persisted) {270 if (!persisted) {
257 return yield* localOps.sourceCredentialSubmit.badRequest(271 return yield* localOps.sourceCredentialSubmit.badRequest(
258 "Source credential request is no longer resumable",272 "Source credential request is no longer resumable",
259 `interactionId=${interaction.interactionId}`,273 `interactionId=${interaction.interactionId}`,
260 );274 );
261 }275 }
262 }276 }
263 277
@@ -289,15 +303,15 @@ export const submitSourceCredentialInteraction = (input: { · 25 unmodified lines
289 localOps.sourceCredentialSubmit.unknownStorage(303 localOps.sourceCredentialSubmit.unknownStorage(
290 error,304 error,
291 `Failed resuming execution for interaction ${interaction.interactionId}`,305 `Failed resuming execution for interaction ${interaction.interactionId}`,
292 )306 ),
293 ),307 ),
294 );308 );
295 309
296 if (!persisted) {310 if (!persisted) {
297 return yield* localOps.sourceCredentialSubmit.badRequest(311 return yield* localOps.sourceCredentialSubmit.badRequest(
298 "Source credential request is no longer resumable",312 "Source credential request is no longer resumable",
299 `interactionId=${interaction.interactionId}`,313 `interactionId=${interaction.interactionId}`,
300 );314 );
301 }315 }
302 }316 }
303 317
@@ -311,23 +325,25 @@ export const submitSourceCredentialInteraction = (input: { · 7 unmodified lines
311 const token = trimOrNull(input.token);325 const token = trimOrNull(input.token);
312 if (token === null) {326 if (token === null) {
313 return yield* localOps.sourceCredentialSubmit.badRequest(327 return yield* localOps.sourceCredentialSubmit.badRequest(
314 "Missing token",328 "Missing token",
315 `interactionId=${interaction.interactionId}`,329 `interactionId=${interaction.interactionId}`,
316 );330 );
317 }331 }
318 332
319 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;333 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
320 const tokenRef = yield* sourceAuthService.storeSecretMaterial({334 const tokenRef = yield* sourceAuthService
321 purpose: "auth_material",335 .storeSecretMaterial({
322 value: token,336 purpose: "auth_material",
323 }).pipe(337 value: token,
324 Effect.mapError((error) =>338 })
325 localOps.sourceCredentialSubmit.unknownStorage(339 .pipe(
326 error,340 Effect.mapError((error) =>
327 `Failed storing credential material for interaction ${interaction.interactionId}`,341 localOps.sourceCredentialSubmit.unknownStorage(
342 error,
343 `Failed storing credential material for interaction ${interaction.interactionId}`,
344 ),
328 ),345 ),
329 ),346 );
330 );
331 const resumed = yield* liveExecutionManager.resolveInteraction({347 const resumed = yield* liveExecutionManager.resolveInteraction({
332 executionId: interaction.executionId,348 executionId: interaction.executionId,
333 response: {349 response: {
@@ -348,15 +364,15 @@ export const submitSourceCredentialInteraction = (input: { · 14 unmodified lines
348 localOps.sourceCredentialSubmit.unknownStorage(364 localOps.sourceCredentialSubmit.unknownStorage(
349 error,365 error,
350 `Failed resuming execution for interaction ${interaction.interactionId}`,366 `Failed resuming execution for interaction ${interaction.interactionId}`,
351 )367 ),
352 ),368 ),
353 );369 );
354 370
355 if (!persisted) {371 if (!persisted) {
356 return yield* localOps.sourceCredentialSubmit.badRequest(372 return yield* localOps.sourceCredentialSubmit.badRequest(
357 "Source credential request is no longer resumable",373 "Source credential request is no longer resumable",
358 `interactionId=${interaction.interactionId}`,374 `interactionId=${interaction.interactionId}`,
359 );375 );
360 }376 }
361 }377 }
362 378
@@ -378,12 +394,14 @@ export const completeSourceCredentialSetup = (input: { · 15 unmodified lines
378 Effect.gen(function* () {394 Effect.gen(function* () {
379 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;395 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
380 396
381 return yield* sourceAuthService.completeSourceCredentialSetup(input).pipe(397 return yield* sourceAuthService
382 Effect.mapError((error) =>398 .completeSourceCredentialSetup(input)
383 localOps.sourceCredentialComplete.unknownStorage(399 .pipe(
384 error,400 Effect.mapError((error) =>
385 "Failed completing source credential setup",401 localOps.sourceCredentialComplete.unknownStorage(
402 error,
403 "Failed completing source credential setup",
404 ),
386 ),405 ),
387 ),406 );
388 );
389 });407 });
packages/platform/sdk/src/local/secrets.ts+283−0
@@ -0,0 +1,283 @@
1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3
4import { SecretMaterialIdSchema } from "../schema";
5import {
6 type CreateSecretPayload,
7 type CreateSecretResult,
8 type DeleteSecretResult,
9 type InstanceConfig,
10 type SecretListItem,
11 type UpdateSecretPayload,
12 type UpdateSecretResult,
13} from "./contracts";
14import {
15 ControlPlaneBadRequestError,
16 ControlPlaneNotFoundError,
17 ControlPlaneStorageError,
18} from "../errors";
19import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context";
20import {
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";
30import { RuntimeSourceStoreService } from "../runtime/sources/source-store";
31import { ControlPlaneStore } from "../runtime/store";
32
33const SECRET_STORE_PROVIDER_ENV = "EXECUTOR_SECRET_STORE_PROVIDER";
34
35const secretStorageError = (operation: string, message: string) =>
36 new ControlPlaneStorageError({
37 operation,
38 message,
39 details: message,
40 });
41
42export 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
82export 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
117export 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
188export 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
242export 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 });
packages/platform/sdk/src/policies/contracts.ts+23−0
@@ -0,0 +1,23 @@
1import {
2 LocalWorkspacePolicyApprovalModeSchema,
3 LocalWorkspacePolicyEffectSchema,
4} from "../schema";
5import * as Schema from "effect/Schema";
6
7import { OptionalTrimmedNonEmptyStringSchema } from "../string-schemas";
8
9const 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
17export const CreatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema;
18
19export type CreatePolicyPayload = typeof CreatePolicyPayloadSchema.Type;
20
21export const UpdatePolicyPayloadSchema = LocalWorkspacePolicyPayloadSchema;
22
23export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type;
packages/platform/sdk/src/policies/operations.tsrenamed from packages/platform/control-plane/src/runtime/policy/policies-operations.ts+126−72
@@ -3,38 +3,32 @@ import { createHash } from "node:crypto"; · 2 unmodified lines
3import type {3import type {
4 CreatePolicyPayload,4 CreatePolicyPayload,
5 UpdatePolicyPayload,5 UpdatePolicyPayload,
6} from "../../api/policies/api";6} from "./contracts";
7import {7import {
8 PolicyIdSchema,8 PolicyIdSchema,
9 type LocalExecutorConfig,9 type LocalExecutorConfig,
10 type LocalWorkspacePolicy,10 type LocalWorkspacePolicy,
11 type PolicyId,11 type PolicyId,
12 type WorkspaceId,12 type WorkspaceId,
13} from "#schema";13} from "../schema";
14import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
15 15
16import {16import { requireRuntimeLocalWorkspace } from "../runtime/local/runtime-context";
17 requireRuntimeLocalWorkspace,
18} from "../local/runtime-context";
19import type {17import type {
20 WorkspaceConfigStoreShape,18 WorkspaceConfigStoreShape,
21 WorkspaceStateStoreShape,19 WorkspaceStateStoreShape,
22} from "../local/storage";20} from "../runtime/local/storage";
23import {21import {
24 WorkspaceConfigStore,22 WorkspaceConfigStore,
25 WorkspaceStateStore,23 WorkspaceStateStore,
26} from "../local/storage";24} from "../runtime/local/storage";
27import type { ResolvedLocalWorkspaceContext } from "../local/config";25import type { ResolvedLocalWorkspaceContext } from "../runtime/local/config";
28import {26import { type LocalWorkspaceState } from "../runtime/local/workspace-state";
29 type LocalWorkspaceState,27import { derivePolicyConfigKey } from "../runtime/local/workspace-sync";
30} from "../local/workspace-state";
31import {
32 derivePolicyConfigKey,
33} from "../local/workspace-sync";
34import {28import {
35 type OperationErrors,29 type OperationErrors,
36 operationErrors,30 operationErrors,
37} from "./operation-errors";31} from "../runtime/policy/operation-errors";
38 32
39const policyOps = {33const policyOps = {
40 list: operationErrors("policies.list"),34 list: operationErrors("policies.list"),
@@ -44,8 +38,7 @@ const policyOps = { · 3 unmodified lines
44 remove: operationErrors("policies.remove"),38 remove: operationErrors("policies.remove"),
45} as const;39} as const;
46 40
47const cloneJson = <T>(value: T): T =>41const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
48 JSON.parse(JSON.stringify(value)) as T;
49 42
50const localPolicyIdForKey = (input: {43const localPolicyIdForKey = (input: {
51 workspaceRoot: string;44 workspaceRoot: string;
@@ -62,10 +55,12 @@ const toLocalWorkspacePolicy = (input: { · 10 unmodified lines
62 policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string];55 policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string];
63 state: LocalWorkspaceState["policies"][string] | undefined;56 state: LocalWorkspaceState["policies"][string] | undefined;
64}): LocalWorkspacePolicy => ({57}): LocalWorkspacePolicy => ({
65 id: input.state?.id ?? localPolicyIdForKey({58 id:
66 workspaceRoot: input.workspaceRoot,59 input.state?.id ??
67 key: input.key,60 localPolicyIdForKey({
68 }),61 workspaceRoot: input.workspaceRoot,
62 key: input.key,
63 }),
69 key: input.key,64 key: input.key,
70 workspaceId: input.workspaceId,65 workspaceId: input.workspaceId,
71 resourcePattern: input.policyConfig.match.trim(),66 resourcePattern: input.policyConfig.match.trim(),
@@ -79,7 +74,8 @@ const toLocalWorkspacePolicy = (input: { · 7 unmodified lines
79 74
80export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) =>75export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) =>
81 Effect.gen(function* () {76 Effect.gen(function* () {
82 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);77 const runtimeLocalWorkspace =
78 yield* requireRuntimeLocalWorkspace(workspaceId);
83 const workspaceConfigStore = yield* WorkspaceConfigStore;79 const workspaceConfigStore = yield* WorkspaceConfigStore;
84 const workspaceStateStore = yield* WorkspaceStateStore;80 const workspaceStateStore = yield* WorkspaceStateStore;
85 const loadedConfig = yield* workspaceConfigStore.load(81 const loadedConfig = yield* workspaceConfigStore.load(
@@ -89,14 +85,16 @@ export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => · 3 unmodified lines
89 runtimeLocalWorkspace.context,85 runtimeLocalWorkspace.context,
90 );86 );
91 87
92 const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(([key, policyConfig]) =>88 const policies = Object.entries(loadedConfig.config?.policies ?? {}).map(
93 toLocalWorkspacePolicy({89 ([key, policyConfig]) =>
94 workspaceId,90 toLocalWorkspacePolicy({
95 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,91 workspaceId,
96 key,92 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
97 policyConfig,93 key,
98 state: workspaceState.policies[key],94 policyConfig,
99 }));95 state: workspaceState.policies[key],
96 }),
97 );
100 98
101 return {99 return {
102 runtimeLocalWorkspace,100 runtimeLocalWorkspace,
@@ -114,16 +112,19 @@ const writeLocalPolicyFiles = (input: { · 11 unmodified lines
114 projectConfig: LocalExecutorConfig;112 projectConfig: LocalExecutorConfig;
115 workspaceState: LocalWorkspaceState;113 workspaceState: LocalWorkspaceState;
116}) =>114}) =>
117 Effect.all([115 Effect.all(
118 input.workspaceConfigStore.writeProject({116 [
119 context: input.context,117 input.workspaceConfigStore.writeProject({
120 config: input.projectConfig,118 context: input.context,
121 }),119 config: input.projectConfig,
122 input.workspaceStateStore.write({120 }),
123 context: input.context,121 input.workspaceStateStore.write({
124 state: input.workspaceState,122 context: input.context,
125 }),123 state: input.workspaceState,
126 ], { discard: true }).pipe(124 }),
125 ],
126 { discard: true },
127 ).pipe(
127 Effect.mapError((cause) =>128 Effect.mapError((cause) =>
128 input.operation.unknownStorage(129 input.operation.unknownStorage(
129 cause,130 cause,
@@ -148,7 +149,9 @@ const loadWorkspacePolicyContext = ( · 18 unmodified lines
148export const listPolicies = (workspaceId: WorkspaceId) =>149export const listPolicies = (workspaceId: WorkspaceId) =>
149 Effect.gen(function* () {150 Effect.gen(function* () {
150 yield* loadWorkspacePolicyContext(policyOps.list, workspaceId);151 yield* loadWorkspacePolicyContext(policyOps.list, workspaceId);
151 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(workspaceId).pipe(152 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(
153 workspaceId,
154 ).pipe(
152 Effect.mapError((cause) =>155 Effect.mapError((cause) =>
153 policyOps.list.unknownStorage(156 policyOps.list.unknownStorage(
154 cause,157 cause,
@@ -164,10 +167,15 @@ export const createPolicy = (input: { · 9 unmodified lines
164 payload: CreatePolicyPayload;167 payload: CreatePolicyPayload;
165}) =>168}) =>
166 Effect.gen(function* () {169 Effect.gen(function* () {
167 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId);170 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(
171 policyOps.create,
172 input.workspaceId,
173 );
168 const workspaceConfigStore = yield* WorkspaceConfigStore;174 const workspaceConfigStore = yield* WorkspaceConfigStore;
169 const workspaceStateStore = yield* WorkspaceStateStore;175 const workspaceStateStore = yield* WorkspaceStateStore;
170 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(176 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(
177 input.workspaceId,
178 ).pipe(
171 Effect.mapError((cause) =>179 Effect.mapError((cause) =>
172 policyOps.create.unknownStorage(180 policyOps.create.unknownStorage(
173 cause,181 cause,
@@ -176,7 +184,9 @@ export const createPolicy = (input: { · 2 unmodified lines
176 ),184 ),
177 );185 );
178 const now = Date.now();186 const now = Date.now();
179 const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});187 const projectConfig = cloneJson(
188 localWorkspace.loadedConfig.projectConfig ?? {},
189 );
180 const policies = { ...projectConfig.policies };190 const policies = { ...projectConfig.policies };
181 const key = derivePolicyConfigKey(191 const key = derivePolicyConfigKey(
182 {192 {
@@ -190,9 +200,14 @@ export const createPolicy = (input: { · 7 unmodified lines
190 policies[key] = {200 policies[key] = {
191 match: input.payload.resourcePattern ?? "*",201 match: input.payload.resourcePattern ?? "*",
192 action: input.payload.effect ?? "allow",202 action: input.payload.effect ?? "allow",
193 approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto",203 approval:
204 (input.payload.approvalMode ?? "auto") === "required"
205 ? "manual"
206 : "auto",
194 ...(input.payload.enabled === false ? { enabled: false } : {}),207 ...(input.payload.enabled === false ? { enabled: false } : {}),
195 ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}),208 ...((input.payload.priority ?? 0) !== 0
209 ? { priority: input.payload.priority ?? 0 }
210 : {}),
196 };211 };
197 212
198 const existingState = localWorkspace.workspaceState.policies[key];213 const existingState = localWorkspace.workspaceState.policies[key];
@@ -201,10 +216,12 @@ export const createPolicy = (input: { · 2 unmodified lines
201 policies: {216 policies: {
202 ...localWorkspace.workspaceState.policies,217 ...localWorkspace.workspaceState.policies,
203 [key]: {218 [key]: {
204 id: existingState?.id ?? localPolicyIdForKey({219 id:
205 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,220 existingState?.id ??
206 key,221 localPolicyIdForKey({
207 }),222 workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot,
223 key,
224 }),
208 createdAt: existingState?.createdAt ?? now,225 createdAt: existingState?.createdAt ?? now,
209 updatedAt: now,226 updatedAt: now,
210 },227 },
@@ -238,7 +255,9 @@ export const getPolicy = (input: { · 27 unmodified lines
238}) =>255}) =>
239 Effect.gen(function* () {256 Effect.gen(function* () {
240 yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId);257 yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId);
241 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(258 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(
259 input.workspaceId,
260 ).pipe(
242 Effect.mapError((cause) =>261 Effect.mapError((cause) =>
243 policyOps.get.unknownStorage(262 policyOps.get.unknownStorage(
244 cause,263 cause,
@@ -246,12 +265,15 @@ export const getPolicy = (input: { · 1 unmodified line
246 ),265 ),
247 ),266 ),
248 );267 );
249 const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;268 const policy =
269 localWorkspace.policies.find(
270 (candidate) => candidate.id === input.policyId,
271 ) ?? null;
250 if (policy === null) {272 if (policy === null) {
251 return yield* policyOps.get.notFound(273 return yield* policyOps.get.notFound(
252 "Policy not found",274 "Policy not found",
253 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,275 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,
254 );276 );
255 }277 }
256 return policy;278 return policy;
257 });279 });
@@ -262,10 +284,15 @@ export const updatePolicy = (input: { · 4 unmodified lines
262 payload: UpdatePolicyPayload;284 payload: UpdatePolicyPayload;
263}) =>285}) =>
264 Effect.gen(function* () {286 Effect.gen(function* () {
265 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId);287 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(
288 policyOps.update,
289 input.workspaceId,
290 );
266 const workspaceConfigStore = yield* WorkspaceConfigStore;291 const workspaceConfigStore = yield* WorkspaceConfigStore;
267 const workspaceStateStore = yield* WorkspaceStateStore;292 const workspaceStateStore = yield* WorkspaceStateStore;
268 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(293 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(
294 input.workspaceId,
295 ).pipe(
269 Effect.mapError((cause) =>296 Effect.mapError((cause) =>
270 policyOps.update.unknownStorage(297 policyOps.update.unknownStorage(
271 cause,298 cause,
@@ -273,27 +300,43 @@ export const updatePolicy = (input: { · 1 unmodified line
273 ),300 ),
274 ),301 ),
275 );302 );
276 const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;303 const existing =
304 localWorkspace.policies.find(
305 (candidate) => candidate.id === input.policyId,
306 ) ?? null;
277 if (existing === null) {307 if (existing === null) {
278 return yield* policyOps.update.notFound(308 return yield* policyOps.update.notFound(
279 "Policy not found",309 "Policy not found",
280 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,310 `workspaceId=${input.workspaceId} policyId=${input.policyId}`,
281 );311 );
282 }312 }
283 313
284 const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});314 const projectConfig = cloneJson(
315 localWorkspace.loadedConfig.projectConfig ?? {},
316 );
285 const policies = { ...projectConfig.policies };317 const policies = { ...projectConfig.policies };
286 const existingConfig = policies[existing.key]!;318 const existingConfig = policies[existing.key]!;
287 319
288 policies[existing.key] = {320 policies[existing.key] = {
289 ...existingConfig,321 ...existingConfig,
290 ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}),322 ...(input.payload.resourcePattern !== undefined
291 ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}),323 ? { match: input.payload.resourcePattern }
324 : {}),
325 ...(input.payload.effect !== undefined
326 ? { action: input.payload.effect }
327 : {}),
292 ...(input.payload.approvalMode !== undefined328 ...(input.payload.approvalMode !== undefined
293 ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" }329 ? {
330 approval:
331 input.payload.approvalMode === "required" ? "manual" : "auto",
332 }
333 : {}),
334 ...(input.payload.enabled !== undefined
335 ? { enabled: input.payload.enabled }
336 : {}),
337 ...(input.payload.priority !== undefined
338 ? { priority: input.payload.priority }
294 : {}),339 : {}),
295 ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}),
296 ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}),
297 };340 };
298 341
299 const updatedAt = Date.now();342 const updatedAt = Date.now();
@@ -336,10 +379,15 @@ export const removePolicy = (input: { · 36 unmodified lines
336 policyId: PolicyId;379 policyId: PolicyId;
337}) =>380}) =>
338 Effect.gen(function* () {381 Effect.gen(function* () {
339 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId);382 const runtimeLocalWorkspace = yield* loadWorkspacePolicyContext(
383 policyOps.remove,
384 input.workspaceId,
385 );
340 const workspaceConfigStore = yield* WorkspaceConfigStore;386 const workspaceConfigStore = yield* WorkspaceConfigStore;
341 const workspaceStateStore = yield* WorkspaceStateStore;387 const workspaceStateStore = yield* WorkspaceStateStore;
342 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe(388 const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(
389 input.workspaceId,
390 ).pipe(
343 Effect.mapError((cause) =>391 Effect.mapError((cause) =>
344 policyOps.remove.unknownStorage(392 policyOps.remove.unknownStorage(
345 cause,393 cause,
@@ -347,16 +395,22 @@ export const removePolicy = (input: { · 1 unmodified line
347 ),395 ),
348 ),396 ),
349 );397 );
350 const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null;398 const existing =
399 localWorkspace.policies.find(
400 (candidate) => candidate.id === input.policyId,
401 ) ?? null;
351 if (existing === null) {402 if (existing === null) {
352 return { removed: false };403 return { removed: false };
353 }404 }
354 405
355 const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});406 const projectConfig = cloneJson(
407 localWorkspace.loadedConfig.projectConfig ?? {},
408 );
356 const policies = { ...projectConfig.policies };409 const policies = { ...projectConfig.policies };
357 delete policies[existing.key];410 delete policies[existing.key];
358 411
359 const { [existing.key]: _removedPolicy, ...remainingPolicies } = localWorkspace.workspaceState.policies;412 const { [existing.key]: _removedPolicy, ...remainingPolicies } =
413 localWorkspace.workspaceState.policies;
360 yield* writeLocalPolicyFiles({414 yield* writeLocalPolicyFiles({
361 operation: policyOps.remove,415 operation: policyOps.remove,
362 context: runtimeLocalWorkspace.context,416 context: runtimeLocalWorkspace.context,
packages/platform/sdk/src/runtime/__fixtures__/README.mdrenamed from packages/platform/control-plane/src/runtime/__fixtures__/README.md+1−1
@@ -35,4 +35,4 @@ On releases where you want upgrade coverage: · 34 unmodified lines
351. Start from a real workspace that has a fully synced source on the released version.351. Start from a real workspace that has a fully synced source on the released version.
362. Run `bun run fixture:release:capture -- ...` for that source.362. Run `bun run fixture:release:capture -- ...` for that source.
373. Review the fixture for secrets before committing it.373. Review the fixture for secrets before committing it.
384. Run `bun run --cwd packages/platform/control-plane test -- src/runtime/local/source-artifacts.test.ts`.384. Run `bun run --cwd packages/platform/sdk test -- src/runtime/local/source-artifacts.test.ts`.
packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar.jsonrenamed from packages/platform/control-plane/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar/documents/doc_3c24a6731267de91.txtrenamed from packages/platform/control-plane/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/artifacts/sources/google-calendar/documents/doc_3c24a6731267de91.txt+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/executor.jsoncrenamed from packages/platform/control-plane/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/executor.jsonc+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/state/workspace-state.jsonrenamed from packages/platform/control-plane/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/.executor/state/workspace-state.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/fixture.jsonrenamed from packages/platform/control-plane/src/runtime/__fixtures__/v1.2.3-google-calendar-workspace/fixture.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/auth-artifacts.tsrenamed from packages/platform/control-plane/src/runtime/auth/auth-artifacts.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/auth-leases.tsrenamed from packages/platform/control-plane/src/runtime/auth/auth-leases.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/mcp-auth-provider.tsrenamed from packages/platform/control-plane/src/runtime/auth/mcp-auth-provider.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/mcp-oauth.tsrenamed from packages/platform/control-plane/src/runtime/auth/mcp-oauth.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/oauth-loopback.test.tsrenamed from packages/platform/control-plane/src/runtime/auth/oauth-loopback.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/oauth-loopback.tsrenamed from packages/platform/control-plane/src/runtime/auth/oauth-loopback.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/oauth2-pkce.tsrenamed from packages/platform/control-plane/src/runtime/auth/oauth2-pkce.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/provider-grant-lifecycle.tsrenamed from packages/platform/control-plane/src/runtime/auth/provider-grant-lifecycle.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/source-auth-material.test.tsrenamed from packages/platform/control-plane/src/runtime/auth/source-auth-material.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/auth/source-auth-material.tsrenamed from packages/platform/control-plane/src/runtime/auth/source-auth-material.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/catalog-typescript.tsrenamed from packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/prettier-format.tsrenamed from packages/platform/control-plane/src/runtime/catalog/prettier-format.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/schema-type-signature.tsrenamed from packages/platform/control-plane/src/runtime/catalog/schema-type-signature.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/reconcile.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/reconcile.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/runtime.test.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/runtime.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/runtime.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/snapshot.test.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/snapshot.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/sync.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/sync.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/type-declarations.test.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/type-declarations.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/catalog/source/type-declarations.tsrenamed from packages/platform/control-plane/src/runtime/catalog/source/type-declarations.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/control-plane-runtime.test.ts+2356−0
@@ -0,0 +1,2356 @@
1import { createServer } from "node:http";
2import { join } from "node:path";
3
4import { FileSystem } from "@effect/platform";
5import { NodeFileSystem } from "@effect/platform-node";
6import { describe, expect, it } from "@effect/vitest";
7import { assertTrue } from "@effect/vitest/utils";
8import * as Either from "effect/Either";
9import * as Effect from "effect/Effect";
10import * as Fiber from "effect/Fiber";
11import * as Option from "effect/Option";
12
13import {
14 ExecutionIdSchema,
15 ExecutionInteractionIdSchema,
16 type ExecutionInteraction,
17 ProviderAuthGrantIdSchema,
18 SecretMaterialIdSchema,
19 SourceAuthSessionIdSchema,
20 SourceIdSchema,
21 WorkspaceOauthClientIdSchema,
22} from "#schema";
23import type { ToolPath } from "@executor/codemode-core";
24import { createCatalogImportMetadata } from "@executor/source-core";
25import { createGraphqlCatalogFragment } from "@executor/source-graphql";
26import { createExecutorAdminToolMap } from "@executor/platform-internal";
27
28import {
29 type ControlPlaneRuntime,
30 createControlPlaneRuntime,
31 LiveExecutionManagerService,
32 provideControlPlaneRuntime,
33 RuntimeExecutionResolverService,
34} from "./index";
35import { createSourceFromPayload } from "./sources/source-definitions";
36import { decodeSourceCredentialSelectionContent } from "./sources/source-credential-interactions";
37import { persistSource } from "./sources/source-store";
38import { withControlPlaneClient } from "./execution/test-http-client";
39import { runtimeEffectError } from "./effect-errors";
40import {
41 resolveLocalWorkspaceContext,
42 writeProjectLocalExecutorConfig,
43} from "./local/config";
44import { writeLocalControlPlaneState } from "./local/control-plane-store";
45import { deriveLocalInstallation } from "./local/installation";
46import {
47 decodeSourceCredentialInteraction,
48 submitSourceCredentialInteraction,
49} from "./local/operations";
50import {
51 buildLocalSourceArtifact,
52 readLocalSourceArtifact,
53 writeLocalSourceArtifact,
54} from "./local/source-artifacts";
55import { writeLocalWorkspaceState } from "./local/workspace-state";
56
57const 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
76type OpenApiSpecServer = {
77 baseUrl: string;
78 specUrl: string;
79 close: () => Promise<void>;
80};
81
82type 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
101const 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
169const 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
337const 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
357const 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
408const 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
422const 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
445describe("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});
packages/platform/sdk/src/runtime/effect-errors.tsrenamed from packages/platform/control-plane/src/runtime/effect-errors.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/http.test.tsrenamed from packages/platform/control-plane/src/runtime/execution/http.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/ir-execution.tsrenamed from packages/platform/control-plane/src/runtime/execution/ir-execution.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/live.test.tsrenamed from packages/platform/control-plane/src/runtime/execution/live.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/live.tsrenamed from packages/platform/control-plane/src/runtime/execution/live.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/mcp-resume.test.tsrenamed from packages/platform/control-plane/src/runtime/execution/mcp-resume.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/runtime-config.test.tsrenamed from packages/platform/control-plane/src/runtime/execution/runtime-config.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/runtime.tsrenamed from packages/platform/control-plane/src/runtime/execution/runtime.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/service.tsrenamed from packages/platform/control-plane/src/runtime/execution/service.ts+2−2
@@ -1,7 +1,7 @@
1import {1import {
2 ControlPlaneNotFoundError,2 ControlPlaneNotFoundError,
3 ControlPlaneStorageError,3 ControlPlaneStorageError,
4} from "../../api/errors";4} from "../../errors";
5import type {5import type {
6 ElicitationResponse,6 ElicitationResponse,
7 OnElicitation,7 OnElicitation,
@@ -11,7 +11,7 @@ import type { · 3 unmodified lines
11import type {11import type {
12 CreateExecutionPayload,12 CreateExecutionPayload,
13 ResumeExecutionPayload,13 ResumeExecutionPayload,
14} from "../../api/executions/api";14} from "../../executions/contracts";
15import {15import {
16 ExecutionIdSchema,16 ExecutionIdSchema,
17 ExecutionInteractionIdSchema,17 ExecutionInteractionIdSchema,
packages/platform/sdk/src/runtime/execution/state.tsrenamed from packages/platform/control-plane/src/runtime/execution/state.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/test-http-client.tsrenamed from packages/platform/control-plane/src/runtime/execution/test-http-client.ts+3−4
@@ -3,13 +3,12 @@ import { · 2 unmodified lines
3 HttpApiClient,3 HttpApiClient,
4} from "@effect/platform";4} from "@effect/platform";
5import { NodeHttpServer } from "@effect/platform-node";5import { NodeHttpServer } from "@effect/platform-node";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8
9import {6import {
10 ControlPlaneApi,7 ControlPlaneApi,
11 createControlPlaneApiLayer,8 createControlPlaneApiLayer,
12} from "#api";9} from "@executor/platform-api";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
13 12
14import {13import {
15 type ControlPlaneRuntime,14 type ControlPlaneRuntime,
packages/platform/sdk/src/runtime/execution/workspace/authorization.tsrenamed from packages/platform/control-plane/src/runtime/execution/workspace/authorization.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/workspace/environment.tsrenamed from packages/platform/control-plane/src/runtime/execution/workspace/environment.ts+51−22
@@ -15,16 +15,11 @@ import { · 14 unmodified lines
15 resolveConfiguredExecutionRuntime,15 resolveConfiguredExecutionRuntime,
16} from "../runtime";16} from "../runtime";
17import { createWorkspaceToolInvoker } from "./tool-invoker";17import { createWorkspaceToolInvoker } from "./tool-invoker";
18import {18import { RuntimeSourceAuthServiceTag } from "../../sources/source-auth-service";
19 RuntimeSourceAuthServiceTag,19import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime";
20} from "../../sources/source-auth-service";
21import {
22 RuntimeSourceCatalogStoreService,
23} from "../../catalog/source/runtime";
24import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material";20import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material";
25import {21import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync";
26 getRuntimeLocalWorkspaceOption,22import { getRuntimeLocalWorkspaceOption } from "../../local/runtime-context";
27} from "../../local/runtime-context";
28import {23import {
29 LocalToolRuntimeLoaderService,24 LocalToolRuntimeLoaderService,
30 type LocalToolRuntimeLoaderShape,25 type LocalToolRuntimeLoaderShape,
@@ -38,6 +33,9 @@ import { · 7 unmodified lines
38 WorkspaceStateStore,33 WorkspaceStateStore,
39 type WorkspaceStateStoreShape,34 type WorkspaceStateStoreShape,
40} from "../../local/storage";35} from "../../local/storage";
36import { ControlPlaneStore } from "../../store";
37import { RuntimeSourceStoreService } from "../../sources/source-store";
38import type { CreateWorkspaceInternalToolMap } from "./tool-invoker";
41export {39export {
42 createCodeExecutorForRuntime,40 createCodeExecutorForRuntime,
43 resolveConfiguredExecutionRuntime,41 resolveConfiguredExecutionRuntime,
@@ -50,29 +48,49 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 6 unmodified lines
50 toolPaths: new Set<string>(),48 toolPaths: new Set<string>(),
51});49});
52 50
53export const createWorkspaceExecutionEnvironmentResolver = (input: {51export const createWorkspaceExecutionEnvironmentResolver =
54 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;52 (input: {
55 sourceAuthService: Effect.Effect.Success<typeof RuntimeSourceAuthServiceTag>;53 controlPlaneStore: Effect.Effect.Success<typeof ControlPlaneStore>;
56 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;54 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
57 localToolRuntimeLoader: LocalToolRuntimeLoaderShape;55 sourceCatalogSyncService: Effect.Effect.Success<
58 workspaceConfigStore: WorkspaceConfigStoreShape;56 typeof RuntimeSourceCatalogSyncService
59 workspaceStateStore: WorkspaceStateStoreShape;57 >;
60 sourceArtifactStore: SourceArtifactStoreShape;58 sourceAuthMaterialService: Effect.Effect.Success<
61}): ResolveExecutionEnvironment =>59 typeof RuntimeSourceAuthMaterialService
60 >;
61 sourceAuthService: Effect.Effect.Success<
62 typeof RuntimeSourceAuthServiceTag
63 >;
64 sourceCatalogStore: Effect.Effect.Success<
65 typeof RuntimeSourceCatalogStoreService
66 >;
67 localToolRuntimeLoader: LocalToolRuntimeLoaderShape;
68 workspaceConfigStore: WorkspaceConfigStoreShape;
69 workspaceStateStore: WorkspaceStateStoreShape;
70 sourceArtifactStore: SourceArtifactStoreShape;
71 createInternalToolMap?: CreateWorkspaceInternalToolMap;
72 }): ResolveExecutionEnvironment =>
62 ({ workspaceId, accountId, onElicitation }) =>73 ({ workspaceId, accountId, onElicitation }) =>
63 Effect.gen(function* () {74 Effect.gen(function* () {
64 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();75 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
65 const loadedConfig =76 const loadedConfig =
66 runtimeLocalWorkspace === null77 runtimeLocalWorkspace === null
67 ? null78 ? null
68 : yield* input.workspaceConfigStore.load(runtimeLocalWorkspace.context);79 : yield* input.workspaceConfigStore.load(
80 runtimeLocalWorkspace.context,
81 );
69 const localToolRuntime =82 const localToolRuntime =
70 runtimeLocalWorkspace === null83 runtimeLocalWorkspace === null
71 ? createEmptyLocalToolRuntime()84 ? createEmptyLocalToolRuntime()
72 : yield* input.localToolRuntimeLoader.load(runtimeLocalWorkspace.context);85 : yield* input.localToolRuntimeLoader.load(
86 runtimeLocalWorkspace.context,
87 );
73 const { catalog, toolInvoker } = createWorkspaceToolInvoker({88 const { catalog, toolInvoker } = createWorkspaceToolInvoker({
74 workspaceId,89 workspaceId,
75 accountId,90 accountId,
91 controlPlaneStore: input.controlPlaneStore,
92 sourceStore: input.sourceStore,
93 sourceCatalogSyncService: input.sourceCatalogSyncService,
76 sourceCatalogStore: input.sourceCatalogStore,94 sourceCatalogStore: input.sourceCatalogStore,
77 workspaceConfigStore: input.workspaceConfigStore,95 workspaceConfigStore: input.workspaceConfigStore,
78 workspaceStateStore: input.workspaceStateStore,96 workspaceStateStore: input.workspaceStateStore,
@@ -81,6 +99,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 2 unmodified lines
81 sourceAuthService: input.sourceAuthService,99 sourceAuthService: input.sourceAuthService,
82 runtimeLocalWorkspace,100 runtimeLocalWorkspace,
83 localToolRuntime,101 localToolRuntime,
102 createInternalToolMap: input.createInternalToolMap,
84 onElicitation,103 onElicitation,
85 });104 });
86 105
@@ -105,14 +124,20 @@ export class RuntimeExecutionResolverService extends Context.Tag( · 18 unmodified lines
105export const RuntimeExecutionResolverLive = (124export const RuntimeExecutionResolverLive = (
106 input: {125 input: {
107 executionResolver?: ResolveExecutionEnvironment;126 executionResolver?: ResolveExecutionEnvironment;
127 createInternalToolMap?: CreateWorkspaceInternalToolMap;
108 } = {},128 } = {},
109) =>129) =>
110 input.executionResolver130 input.executionResolver
111 ? Layer.succeed(RuntimeExecutionResolverService, input.executionResolver)131 ? Layer.succeed(RuntimeExecutionResolverService, input.executionResolver)
112 : Layer.effect(132 : Layer.effect(
113 RuntimeExecutionResolverService,133 RuntimeExecutionResolverService,
114 Effect.gen(function* () {134 Effect.gen(function* () {
115 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;135 const controlPlaneStore = yield* ControlPlaneStore;
136 const sourceStore = yield* RuntimeSourceStoreService;
137 const sourceCatalogSyncService =
138 yield* RuntimeSourceCatalogSyncService;
139 const sourceAuthMaterialService =
140 yield* RuntimeSourceAuthMaterialService;
116 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;141 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
117 const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;142 const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
118 const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;143 const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;
@@ -121,6 +146,9 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines
121 const sourceArtifactStore = yield* SourceArtifactStore;146 const sourceArtifactStore = yield* SourceArtifactStore;
122 147
123 return createWorkspaceExecutionEnvironmentResolver({148 return createWorkspaceExecutionEnvironmentResolver({
149 controlPlaneStore,
150 sourceStore,
151 sourceCatalogSyncService,
124 sourceAuthService,152 sourceAuthService,
125 sourceAuthMaterialService,153 sourceAuthMaterialService,
126 sourceCatalogStore,154 sourceCatalogStore,
@@ -128,6 +156,7 @@ export const RuntimeExecutionResolverLive = ( · 1 unmodified line
128 workspaceConfigStore,156 workspaceConfigStore,
129 workspaceStateStore,157 workspaceStateStore,
130 sourceArtifactStore,158 sourceArtifactStore,
159 createInternalToolMap: input.createInternalToolMap,
131 });160 });
132 }),161 }),
133 );162 );
packages/platform/sdk/src/runtime/execution/workspace/local.tsrenamed from packages/platform/control-plane/src/runtime/execution/workspace/local.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/workspace/source-catalog.tsrenamed from packages/platform/control-plane/src/runtime/execution/workspace/source-catalog.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/execution/workspace/tool-invoker.tsrenamed from packages/platform/control-plane/src/runtime/execution/workspace/tool-invoker.ts+89−41
@@ -5,6 +5,7 @@ import { · 4 unmodified lines
5 mergeToolCatalogs,5 mergeToolCatalogs,
6 mergeToolMaps,6 mergeToolMaps,
7 type ToolCatalog,7 type ToolCatalog,
8 type ToolMap,
8 type ToolInvoker,9 type ToolInvoker,
9} from "@executor/codemode-core";10} from "@executor/codemode-core";
10import type { AccountId, Source } from "#schema";11import type { AccountId, Source } from "#schema";
@@ -13,19 +14,18 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines
13import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material";14import { RuntimeSourceAuthMaterialService } from "../../auth/source-auth-material";
14import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime";15import { RuntimeSourceCatalogStoreService } from "../../catalog/source/runtime";
15import type { RuntimeLocalWorkspaceState } from "../../local/runtime-context";16import type { RuntimeLocalWorkspaceState } from "../../local/runtime-context";
16import {17import { type LocalToolRuntime } from "../../local/tools";
17 type LocalToolRuntime,
18} from "../../local/tools";
19import {18import {
20 makeWorkspaceStorageLayer,19 makeWorkspaceStorageLayer,
21 type SourceArtifactStoreShape,20 type SourceArtifactStoreShape,
22 type WorkspaceConfigStoreShape,21 type WorkspaceConfigStoreShape,
23 type WorkspaceStateStoreShape,22 type WorkspaceStateStoreShape,
24} from "../../local/storage";23} from "../../local/storage";
25import {24import type { ControlPlaneStoreShape } from "../../store";
26 RuntimeSourceAuthService,25import { RuntimeSourceAuthService } from "../../sources/source-auth-service";
27} from "../../sources/source-auth-service";26import { type RuntimeSourceStore } from "../../sources/source-store";
28import { createExecutorToolMap } from "../../sources/executor-tools";27import { createExecutorToolMap } from "../../sources/executor-tools";
28import { RuntimeSourceCatalogSyncService } from "../../catalog/source/sync";
29import { invokeIrTool } from "../ir-execution";29import { invokeIrTool } from "../ir-execution";
30import {30import {
31 authorizePersistedToolInvocation,31 authorizePersistedToolInvocation,
@@ -38,17 +38,46 @@ import { · 6 unmodified lines
38} from "./source-catalog";38} from "./source-catalog";
39import { runtimeEffectError } from "../../effect-errors";39import { runtimeEffectError } from "../../effect-errors";
40 40
41export 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
56export type CreateWorkspaceInternalToolMap = (
57 input: WorkspaceInternalToolContext,
58) => ToolMap;
59
41export const createWorkspaceToolInvoker = (input: {60export const createWorkspaceToolInvoker = (input: {
42 workspaceId: Source["workspaceId"];61 workspaceId: Source["workspaceId"];
43 accountId: AccountId;62 accountId: AccountId;
44 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;63 controlPlaneStore: ControlPlaneStoreShape;
64 sourceStore: RuntimeSourceStore;
65 sourceCatalogSyncService: Effect.Effect.Success<
66 typeof RuntimeSourceCatalogSyncService
67 >;
68 sourceCatalogStore: Effect.Effect.Success<
69 typeof RuntimeSourceCatalogStoreService
70 >;
45 workspaceConfigStore: WorkspaceConfigStoreShape;71 workspaceConfigStore: WorkspaceConfigStoreShape;
46 workspaceStateStore: WorkspaceStateStoreShape;72 workspaceStateStore: WorkspaceStateStoreShape;
47 sourceArtifactStore: SourceArtifactStoreShape;73 sourceArtifactStore: SourceArtifactStoreShape;
48 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;74 sourceAuthMaterialService: Effect.Effect.Success<
75 typeof RuntimeSourceAuthMaterialService
76 >;
49 sourceAuthService: RuntimeSourceAuthService;77 sourceAuthService: RuntimeSourceAuthService;
50 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null;78 runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null;
51 localToolRuntime: LocalToolRuntime;79 localToolRuntime: LocalToolRuntime;
80 createInternalToolMap?: CreateWorkspaceInternalToolMap;
52 onElicitation?: Parameters<81 onElicitation?: Parameters<
53 typeof makeToolInvokerFromTools82 typeof makeToolInvokerFromTools
54 >[0]["onElicitation"];83 >[0]["onElicitation"];
@@ -70,6 +99,19 @@ export const createWorkspaceToolInvoker = (input: { · 15 unmodified lines
70 sourceAuthService: input.sourceAuthService,99 sourceAuthService: input.sourceAuthService,
71 runtimeLocalWorkspace: input.runtimeLocalWorkspace,100 runtimeLocalWorkspace: input.runtimeLocalWorkspace,
72 });101 });
102 const internalTools =
103 input.createInternalToolMap?.({
104 workspaceId: input.workspaceId,
105 accountId: input.accountId,
106 controlPlaneStore: input.controlPlaneStore,
107 sourceStore: input.sourceStore,
108 sourceCatalogSyncService: input.sourceCatalogSyncService,
109 sourceAuthService: input.sourceAuthService,
110 workspaceConfigStore: input.workspaceConfigStore,
111 workspaceStateStore: input.workspaceStateStore,
112 sourceArtifactStore: input.sourceArtifactStore,
113 runtimeLocalWorkspace: input.runtimeLocalWorkspace,
114 }) ?? {};
73 const sourceCatalog = createWorkspaceSourceCatalog({115 const sourceCatalog = createWorkspaceSourceCatalog({
74 workspaceId: input.workspaceId,116 workspaceId: input.workspaceId,
75 accountId: input.accountId,117 accountId: input.accountId,
@@ -92,6 +134,7 @@ export const createWorkspaceToolInvoker = (input: { · 16 unmodified lines
92 const authoredTools = mergeToolMaps([134 const authoredTools = mergeToolMaps([
93 systemTools,135 systemTools,
94 executorTools,136 executorTools,
137 internalTools,
95 input.localToolRuntime.tools,138 input.localToolRuntime.tools,
96 ]);139 ]);
97 const authoredCatalog = createToolCatalogFromTools({140 const authoredCatalog = createToolCatalogFromTools({
@@ -112,41 +155,46 @@ export const createWorkspaceToolInvoker = (input: { · 14 unmodified lines
112 context?: Record<string, unknown>;155 context?: Record<string, unknown>;
113 }) =>156 }) =>
114 provideRuntimeLocalWorkspace(157 provideRuntimeLocalWorkspace(
115 provideWorkspaceStorage(Effect.gen(function* () {158 provideWorkspaceStorage(
116 const catalogTool = yield* loadWorkspaceCatalogToolByPath({159 Effect.gen(function* () {
117 workspaceId: input.workspaceId,160 const catalogTool = yield* loadWorkspaceCatalogToolByPath({
118 accountId: input.accountId,161 workspaceId: input.workspaceId,
119 sourceCatalogStore: input.sourceCatalogStore,162 accountId: input.accountId,
120 path: invocation.path,163 sourceCatalogStore: input.sourceCatalogStore,
121 includeSchemas: false,164 path: invocation.path,
122 });165 includeSchemas: false,
123 if (!catalogTool) {166 });
124 return yield* runtimeEffectError("execution/workspace/tool-invoker", `Unknown tool path: ${invocation.path}`);167 if (!catalogTool) {
125 }168 return yield* runtimeEffectError(
169 "execution/workspace/tool-invoker",
170 `Unknown tool path: ${invocation.path}`,
171 );
172 }
126 173
127 yield* authorizePersistedToolInvocation({174 yield* authorizePersistedToolInvocation({
128 workspaceId: input.workspaceId,175 workspaceId: input.workspaceId,
129 tool: catalogTool,176 tool: catalogTool,
130 args: invocation.args,177 args: invocation.args,
131 context: invocation.context,178 context: invocation.context,
132 onElicitation: input.onElicitation,179 onElicitation: input.onElicitation,
133 });180 });
134 181
135 const auth = yield* input.sourceAuthMaterialService.resolve({182 const auth = yield* input.sourceAuthMaterialService.resolve({
136 source: catalogTool.source,183 source: catalogTool.source,
137 actorAccountId: input.accountId,184 actorAccountId: input.accountId,
138 context: toSecretResolutionContext(invocation.context),185 context: toSecretResolutionContext(invocation.context),
139 });186 });
140 return yield* invokeIrTool({187 return yield* invokeIrTool({
141 workspaceId: input.workspaceId,188 workspaceId: input.workspaceId,
142 accountId: input.accountId,189 accountId: input.accountId,
143 tool: catalogTool,190 tool: catalogTool,
144 auth,191 auth,
145 args: invocation.args,192 args: invocation.args,
146 onElicitation: input.onElicitation,193 onElicitation: input.onElicitation,
147 context: invocation.context,194 context: invocation.context,
148 });195 });
149 })),196 }),
197 ),
150 input.runtimeLocalWorkspace,198 input.runtimeLocalWorkspace,
151 );199 );
152 200
packages/platform/sdk/src/runtime/fixtures/google-sheets-discovery.jsonrenamed from packages/platform/control-plane/src/runtime/fixtures/google-sheets-discovery.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/fixtures/linear-introspection.jsonrenamed from packages/platform/control-plane/src/runtime/fixtures/linear-introspection.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/fixtures/neon-openapi.jsonrenamed from packages/platform/control-plane/src/runtime/fixtures/neon-openapi.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/fixtures/vercel-openapi.jsonrenamed from packages/platform/control-plane/src/runtime/fixtures/vercel-openapi.json+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/index.tsrenamed from packages/platform/control-plane/src/runtime/index.ts+63−46
@@ -6,8 +6,6 @@ import * as ManagedRuntime from "effect/ManagedRuntime"; · 5 unmodified lines
6import * as Scope from "effect/Scope";6import * as Scope from "effect/Scope";
7import { NodeFileSystem } from "@effect/platform-node";7import { NodeFileSystem } from "@effect/platform-node";
8import { clearAllMcpConnectionPools } from "@executor/source-mcp";8import { clearAllMcpConnectionPools } from "@executor/source-mcp";
9
10import { type ControlPlaneApiRuntimeContext } from "#api";
11import type { LocalInstallation } from "#schema";9import type { LocalInstallation } from "#schema";
12 10
13import { type ResolveExecutionEnvironment } from "./execution/state";11import { type ResolveExecutionEnvironment } from "./execution/state";
@@ -20,9 +18,7 @@ import { · 6 unmodified lines
20 type LocalControlPlanePersistence,18 type LocalControlPlanePersistence,
21} from "./local/control-plane-store";19} from "./local/control-plane-store";
22 20
23import {21import { resolveLocalWorkspaceContext } from "./local/config";
24 resolveLocalWorkspaceContext,
25} from "./local/config";
26import {22import {
27 LocalStorageLive,23 LocalStorageLive,
28 LocalInstallationStore,24 LocalInstallationStore,
@@ -40,36 +36,57 @@ import { RuntimeSourceCatalogStoreLive } from "./catalog/source/runtime"; · 11 unmodified lines
40import { reconcileMissingSourceCatalogArtifacts } from "./catalog/source/reconcile";36import { reconcileMissingSourceCatalogArtifacts } from "./catalog/source/reconcile";
41import { RuntimeSourceAuthMaterialLive } from "./auth/source-auth-material";37import { RuntimeSourceAuthMaterialLive } from "./auth/source-auth-material";
42import { RuntimeSourceCatalogSyncLive } from "./catalog/source/sync";38import { RuntimeSourceCatalogSyncLive } from "./catalog/source/sync";
43import {39import { RuntimeSourceAuthServiceLive } from "./sources/source-auth-service";
44 RuntimeSourceAuthServiceLive,
45} from "./sources/source-auth-service";
46import type { ResolveSecretMaterial } from "./local/secret-material-providers";40import type { ResolveSecretMaterial } from "./local/secret-material-providers";
47import { SecretMaterialLive } from "./local/secret-material-providers";41import { SecretMaterialLive } from "./local/secret-material-providers";
48import {42import { RuntimeExecutionResolverLive } from "./execution/workspace/environment";
49 RuntimeExecutionResolverLive,43import type { CreateWorkspaceInternalToolMap } from "./execution/workspace/tool-invoker";
50} from "./execution/workspace/environment";
51 44
52export * from "./execution/state";45export * from "./execution/state";
53export * from "./sources/executor-tools";46export * from "./sources/executor-tools";
54export * from "./execution/live";47export * from "./execution/live";
55export * from "./local/config";48export * from "./local/config";
49export * from "./local/errors";
56export * from "./local/installation";50export * from "./local/installation";
51export * from "./local/runtime-context";
57export * from "./local/storage";52export * from "./local/storage";
58export * from "./local/source-artifacts";53export * from "./local/source-artifacts";
59export * from "./local/tools";54export * from "./local/tools";
60export * from "./catalog/schema-type-signature";55export * from "./catalog/schema-type-signature";
56export * from "./catalog/source/runtime";
57export * from "./catalog/source/sync";
61export * from "./sources/source-auth-service";58export * from "./sources/source-auth-service";
62export * from "./local/secret-material-providers";59export * from "./local/secret-material-providers";
63export * from "./sources/source-credential-interactions";60export * from "./sources/source-credential-interactions";
64export * from "./sources/source-adapters/mcp";61export * from "./sources/source-adapters/mcp";
62export * from "./sources/source-store";
65export * from "./store";63export * from "./store";
66export * from "./execution/workspace/environment";64export * from "./execution/workspace/environment";
67export * from "./sources/source-inspection";65export * from "../sources/inspection";
68export * from "./sources/source-discovery";66export * from "../sources/discovery";
69export * from "./execution/service";67export * from "./execution/service";
68export type {
69 CreateWorkspaceInternalToolMap,
70 WorkspaceInternalToolContext,
71} from "./execution/workspace/tool-invoker";
72export {
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
71export type RuntimeControlPlaneOptions = {87export type RuntimeControlPlaneOptions = {
72 executionResolver?: ResolveExecutionEnvironment;88 executionResolver?: ResolveExecutionEnvironment;
89 createInternalToolMap?: CreateWorkspaceInternalToolMap;
73 resolveSecretMaterial?: ResolveSecretMaterial;90 resolveSecretMaterial?: ResolveSecretMaterial;
74 getLocalServerBaseUrl?: () => string | undefined;91 getLocalServerBaseUrl?: () => string | undefined;
75 localDataDir?: string;92 localDataDir?: string;
@@ -81,9 +98,7 @@ export type RuntimeControlPlaneOptions = { · 5 unmodified lines
81const detailsFromCause = (cause: unknown): string =>98const detailsFromCause = (cause: unknown): string =>
82 cause instanceof Error ? cause.message : String(cause);99 cause instanceof Error ? cause.message : String(cause);
83 100
84const toLocalRuntimeBootstrapError = (101const toLocalRuntimeBootstrapError = (cause: unknown): Error => {
85 cause: unknown,
86): Error => {
87 const details = detailsFromCause(cause);102 const details = detailsFromCause(cause);
88 return new Error(`Failed initializing local runtime: ${details}`);103 return new Error(`Failed initializing local runtime: ${details}`);
89};104};
@@ -91,11 +106,7 @@ const toLocalRuntimeBootstrapError = ( · 1 unmodified line
91const closeScope = (scope: Scope.CloseableScope) =>106const closeScope = (scope: Scope.CloseableScope) =>
92 Scope.close(scope, Exit.void).pipe(Effect.orDie);107 Scope.close(scope, Exit.void).pipe(Effect.orDie);
93 108
94export type RuntimeControlPlaneLayer = Layer.Layer<109export type RuntimeControlPlaneLayer = Layer.Layer<any, never, never>;
95 ControlPlaneApiRuntimeContext,
96 never,
97 never
98>;
99 110
100export const createRuntimeControlPlaneLayer = (111export const createRuntimeControlPlaneLayer = (
101 input: RuntimeControlPlaneOptions & {112 input: RuntimeControlPlaneOptions & {
@@ -105,9 +116,7 @@ export const createRuntimeControlPlaneLayer = ( · 3 unmodified lines
105 },116 },
106) => {117) => {
107 const platformLayer = NodeFileSystem.layer;118 const platformLayer = NodeFileSystem.layer;
108 const storageLayer = LocalStorageLive.pipe(119 const storageLayer = LocalStorageLive.pipe(Layer.provide(platformLayer));
109 Layer.provide(platformLayer),
110 );
111 const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe(120 const localToolRuntimeLayer = LocalToolRuntimeLoaderLive.pipe(
112 Layer.provide(platformLayer),121 Layer.provide(platformLayer),
113 );122 );
@@ -157,11 +166,13 @@ export const createRuntimeControlPlaneLayer = ( · 43 unmodified lines
157 166
158 const executionResolverLayer = RuntimeExecutionResolverLive({167 const executionResolverLayer = RuntimeExecutionResolverLive({
159 executionResolver: input.executionResolver,168 executionResolver: input.executionResolver,
169 createInternalToolMap: input.createInternalToolMap,
160 }).pipe(170 }).pipe(
161 Layer.provide(171 Layer.provide(
162 Layer.mergeAll(172 Layer.mergeAll(
163 baseLayer,173 baseLayer,
164 secretMaterialLayer,174 secretMaterialLayer,
175 sourceStoreLayer,
165 sourceAuthMaterialLayer,176 sourceAuthMaterialLayer,
166 sourceCatalogSyncLayer,177 sourceCatalogSyncLayer,
167 sourceAuthLayer,178 sourceAuthLayer,
@@ -187,7 +198,7 @@ export const createRuntimeControlPlaneLayer = ( · 19 unmodified lines
187export type ControlPlaneRuntime = {198export type ControlPlaneRuntime = {
188 persistence: LocalControlPlanePersistence;199 persistence: LocalControlPlanePersistence;
189 localInstallation: LocalInstallation;200 localInstallation: LocalInstallation;
190 managedRuntime: ManagedRuntime.ManagedRuntime<ControlPlaneApiRuntimeContext, never>;201 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;
191 runtimeLayer: RuntimeControlPlaneLayer;202 runtimeLayer: RuntimeControlPlaneLayer;
192 close: () => Promise<void>;203 close: () => Promise<void>;
193};204};
@@ -197,13 +208,13 @@ export type CreateControlPlaneRuntimeOptions = RuntimeControlPlaneOptions; · 3 unmodified lines
197export const provideControlPlaneRuntime = <A, E, R>(208export const provideControlPlaneRuntime = <A, E, R>(
198 effect: Effect.Effect<A, E, R>,209 effect: Effect.Effect<A, E, R>,
199 runtime: ControlPlaneRuntime,210 runtime: ControlPlaneRuntime,
200): Effect.Effect<A, E | never, Exclude<R, ControlPlaneApiRuntimeContext>> =>211): Effect.Effect<A, E | never, any> =>
201 effect.pipe(Effect.provide(runtime.managedRuntime));212 effect.pipe(Effect.provide(runtime.managedRuntime));
202 213
203export const createControlPlaneRuntime = (214export const createControlPlaneRuntime = (
204 options: CreateControlPlaneRuntimeOptions,215 options: CreateControlPlaneRuntimeOptions,
205): Effect.Effect<ControlPlaneRuntime, Error> =>216): Effect.Effect<ControlPlaneRuntime, Error> =>
206 Effect.gen(function* () {217 (Effect.gen(function* () {
207 const scope = yield* Scope.make();218 const scope = yield* Scope.make();
208 219
209 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({220 const localWorkspaceContext = yield* resolveLocalWorkspaceContext({
@@ -217,19 +228,20 @@ export const createControlPlaneRuntime = ( · 7 unmodified lines
217 ),228 ),
218 );229 );
219 230
220
221 const installationStore = LocalInstallationStore;231 const installationStore = LocalInstallationStore;
222 const workspaceConfigStore = LocalWorkspaceConfigStore;232 const workspaceConfigStore = LocalWorkspaceConfigStore;
223 const fileSystem = yield* FileSystem.FileSystem;233 const fileSystem = yield* FileSystem.FileSystem;
224 234
225 const localInstallation = yield* installationStore.getOrProvision({235 const localInstallation = yield* installationStore
226 context: localWorkspaceContext,236 .getOrProvision({
227 }).pipe(237 context: localWorkspaceContext,
228 Effect.mapError(toLocalRuntimeBootstrapError),238 })
229 Effect.catchAll((error) =>239 .pipe(
230 closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))),240 Effect.mapError(toLocalRuntimeBootstrapError),
231 ),241 Effect.catchAll((error) =>
232 );242 closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))),
243 ),
244 );
233 245
234 const persistence = createLocalControlPlanePersistence(246 const persistence = createLocalControlPlanePersistence(
235 localWorkspaceContext,247 localWorkspaceContext,
@@ -237,14 +249,14 @@ export const createControlPlaneRuntime = ( · 1 unmodified line
237 );249 );
238 const rows = persistence.rows;250 const rows = persistence.rows;
239 251
240 const loadedLocalConfig = yield* workspaceConfigStore.load(252 const loadedLocalConfig = yield* workspaceConfigStore
241 localWorkspaceContext,253 .load(localWorkspaceContext)
242 ).pipe(254 .pipe(
243 Effect.mapError(toLocalRuntimeBootstrapError),255 Effect.mapError(toLocalRuntimeBootstrapError),
244 Effect.catchAll((error) =>256 Effect.catchAll((error) =>
245 closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))),257 closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))),
246 ),258 ),
247 );259 );
248 260
249 const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({261 const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({
250 context: localWorkspaceContext,262 context: localWorkspaceContext,
@@ -291,8 +303,13 @@ export const createControlPlaneRuntime = ( · 40 unmodified lines
291 managedRuntime,303 managedRuntime,
292 runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer,304 runtimeLayer: concreteRuntimeLayer as RuntimeControlPlaneLayer,
293 close: async () => {305 close: async () => {
294 await Effect.runPromise(clearAllMcpConnectionPools()).catch(() => undefined);306 await Effect.runPromise(clearAllMcpConnectionPools()).catch(
307 () => undefined,
308 );
295 await managedRuntime.dispose().catch(() => undefined);309 await managedRuntime.dispose().catch(() => undefined);
296 },310 },
297 };311 };
298 }).pipe(Effect.provide(NodeFileSystem.layer));312 }).pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect<
313 ControlPlaneRuntime,
314 Error
315 >);
packages/platform/sdk/src/runtime/local/capture-release-workspace-fixture.tsrenamed from packages/platform/control-plane/src/runtime/local/capture-release-workspace-fixture.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/config-secrets.tsrenamed from packages/platform/control-plane/src/runtime/local/config-secrets.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/config.test.tsrenamed from packages/platform/control-plane/src/runtime/local/config.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/config.tsrenamed from packages/platform/control-plane/src/runtime/local/config.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/control-plane-store.test.tsrenamed from packages/platform/control-plane/src/runtime/local/control-plane-store.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/control-plane-store.tsrenamed from packages/platform/control-plane/src/runtime/local/control-plane-store.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/errors.tsrenamed from packages/platform/control-plane/src/runtime/local/errors.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/installation.test.tsrenamed from packages/platform/control-plane/src/runtime/local/installation.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/installation.tsrenamed from packages/platform/control-plane/src/runtime/local/installation.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/operations.ts+1−0
@@ -0,0 +1 @@
1export * from "../../local/operations";
packages/platform/sdk/src/runtime/local/release-upgrade-fixtures.tsrenamed from packages/platform/control-plane/src/runtime/local/release-upgrade-fixtures.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/runtime-context.tsrenamed from packages/platform/control-plane/src/runtime/local/runtime-context.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/secret-material-providers.tsrenamed from packages/platform/control-plane/src/runtime/local/secret-material-providers.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/secrets-operations.ts+1−0
@@ -0,0 +1 @@
1export * from "../../local/secrets";
packages/platform/sdk/src/runtime/local/source-artifacts.test.tsrenamed from packages/platform/control-plane/src/runtime/local/source-artifacts.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/source-artifacts.tsrenamed from packages/platform/control-plane/src/runtime/local/source-artifacts.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/storage.tsrenamed from packages/platform/control-plane/src/runtime/local/storage.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/tools.tsrenamed from packages/platform/control-plane/src/runtime/local/tools.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/workspace-state.tsrenamed from packages/platform/control-plane/src/runtime/local/workspace-state.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/local/workspace-sync.tsrenamed from packages/platform/control-plane/src/runtime/local/workspace-sync.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/policy/invocation-policy-engine.test.tsrenamed from packages/platform/control-plane/src/runtime/policy/invocation-policy-engine.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/policy/invocation-policy-engine.tsrenamed from packages/platform/control-plane/src/runtime/policy/invocation-policy-engine.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/policy/operation-errors.tsrenamed from packages/platform/control-plane/src/runtime/policy/operation-errors.ts+1−1
@@ -2,7 +2,7 @@ import { · 1 unmodified line
2 ControlPlaneBadRequestError,2 ControlPlaneBadRequestError,
3 ControlPlaneNotFoundError,3 ControlPlaneNotFoundError,
4 ControlPlaneStorageError,4 ControlPlaneStorageError,
5} from "../../api/errors";5} from "../../errors";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7 7
8export type OperationErrors<TOperation extends string = string> = {8export type OperationErrors<TOperation extends string = string> = {
packages/platform/sdk/src/runtime/policy/operations-shared.tsrenamed from packages/platform/control-plane/src/runtime/policy/operations-shared.ts+1−1
@@ -1,7 +1,7 @@
1import {1import {
2 ControlPlaneBadRequestError,2 ControlPlaneBadRequestError,
3 ControlPlaneStorageError,3 ControlPlaneStorageError,
4} from "../../api/errors";4} from "../../errors";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6 6
7import {7import {
packages/platform/sdk/src/runtime/policy/policies-operations.ts+1−0
@@ -0,0 +1 @@
1export * from "../../policies/operations";
packages/platform/sdk/src/runtime/sources/catalog-sync-result.tsrenamed from packages/platform/control-plane/src/runtime/sources/catalog-sync-result.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/executor-tools.tsrenamed from packages/platform/control-plane/src/runtime/sources/executor-tools.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/graphql-tools.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/graphql-tools.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/graphql-tools.tsrenamed from packages/platform/control-plane/src/runtime/sources/graphql-tools.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/slug.tsrenamed from packages/platform/control-plane/src/runtime/sources/slug.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapter-fixture-matrix.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapter-fixture-matrix.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/google-discovery.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/google-discovery.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/google-discovery.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/google-discovery.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/graphql.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/graphql.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/graphql.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/graphql.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/index.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/index.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/internal.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/internal.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/mcp.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/mcp.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/mcp.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/mcp.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/openapi.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/openapi.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/shared.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/shared.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-adapters/types.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-adapters/types.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-auth-service.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-auth-service.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-credential-interactions.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-credential-interactions.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-definitions.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-definitions.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-definitions.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-definitions.ts+2−2
@@ -3,7 +3,7 @@ import { sha256Hex } from "@executor/codemode-core"; · 2 unmodified lines
3import type {3import type {
4 CreateSourcePayload,4 CreateSourcePayload,
5 UpdateSourcePayload,5 UpdateSourcePayload,
6} from "#api";6} from "../../sources/contracts";
7import type {7import type {
8 AccountId,8 AccountId,
9 AuthArtifact,9 AuthArtifact,
@@ -26,7 +26,7 @@ import { · 16 unmodified lines
26 SourceCatalogRevisionIdSchema,26 SourceCatalogRevisionIdSchema,
27} from "#schema";27} from "#schema";
28import * as Effect from "effect/Effect";28import * as Effect from "effect/Effect";
29 29
30import { getSourceAdapter, getSourceAdapterForSource } from "./source-adapters";30import { getSourceAdapter, getSourceAdapterForSource } from "./source-adapters";
31import {31import {
32 authArtifactFromSourceAuth,32 authArtifactFromSourceAuth,
packages/platform/sdk/src/runtime/sources/source-discovery.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-discovery.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-discovery.ts+1−0
@@ -0,0 +1 @@
1export * from "../../sources/discovery";
packages/platform/sdk/src/runtime/sources/source-inspection.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-inspection.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-inspection.ts+1−0
@@ -0,0 +1 @@
1export * from "../../sources/inspection";
packages/platform/sdk/src/runtime/sources/source-names.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-names.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store.test.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store/auth.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store/auth.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store/config.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store/config.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store/deps.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store/deps.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store/lifecycle.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store/lifecycle.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/source-store/records.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-store/records.ts+0−0

File renamed without changes.

packages/platform/sdk/src/runtime/sources/sources-operations.ts+1−0
@@ -0,0 +1 @@
1export * from "../../sources/operations";
packages/platform/sdk/src/runtime/store.tsrenamed from packages/platform/control-plane/src/runtime/store.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/common.tsrenamed from packages/platform/control-plane/src/schema/common.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/enums.tsrenamed from packages/platform/control-plane/src/schema/enums.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/ids.tsrenamed from packages/platform/control-plane/src/schema/ids.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/index.tsrenamed from packages/platform/control-plane/src/schema/index.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/auth-artifact.tsrenamed from packages/platform/control-plane/src/schema/models/auth-artifact.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/auth-lease.tsrenamed from packages/platform/control-plane/src/schema/models/auth-lease.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/auth/account.tsrenamed from packages/platform/control-plane/src/schema/models/auth/account.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/auth/index.tsrenamed from packages/platform/control-plane/src/schema/models/auth/index.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/auth/principal.tsrenamed from packages/platform/control-plane/src/schema/models/auth/principal.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/code-migration.tsrenamed from packages/platform/control-plane/src/schema/models/code-migration.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/credential.tsrenamed from packages/platform/control-plane/src/schema/models/credential.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/execution.tsrenamed from packages/platform/control-plane/src/schema/models/execution.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/local-config.tsrenamed from packages/platform/control-plane/src/schema/models/local-config.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/local-installation.tsrenamed from packages/platform/control-plane/src/schema/models/local-installation.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/policy.tsrenamed from packages/platform/control-plane/src/schema/models/policy.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/provider-auth-grant.tsrenamed from packages/platform/control-plane/src/schema/models/provider-auth-grant.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/secret-material.tsrenamed from packages/platform/control-plane/src/schema/models/secret-material.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source-auth-session.tsrenamed from packages/platform/control-plane/src/schema/models/source-auth-session.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source-catalog.tsrenamed from packages/platform/control-plane/src/schema/models/source-catalog.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source-discovery.tsrenamed from packages/platform/control-plane/src/schema/models/source-discovery.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source-inspection.tsrenamed from packages/platform/control-plane/src/schema/models/source-inspection.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source-oauth-client.tsrenamed from packages/platform/control-plane/src/schema/models/source-oauth-client.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/source.tsrenamed from packages/platform/control-plane/src/schema/models/source.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/models/workspace-oauth-client.tsrenamed from packages/platform/control-plane/src/schema/models/workspace-oauth-client.ts+0−0

File renamed without changes.

packages/platform/sdk/src/schema/schema.test.tsrenamed from packages/platform/control-plane/src/schema/schema.test.ts+0−0

File renamed without changes.

packages/platform/sdk/src/sources/contracts.ts+170−0
@@ -0,0 +1,170 @@
1import {
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";
19import {
20 ConnectSourcePayloadSchema,
21 type ConnectSourcePayload,
22} from "../runtime/sources/source-adapters";
23import * as Schema from "effect/Schema";
24
25import {
26 OptionalTrimmedNonEmptyStringSchema,
27 TrimmedNonEmptyStringSchema,
28} from "../string-schemas";
29
30const createSourcePayloadRequiredSchema = Schema.Struct({
31 name: TrimmedNonEmptyStringSchema,
32 kind: SourceKindSchema,
33 endpoint: TrimmedNonEmptyStringSchema,
34});
35
36const 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
48export const CreateSourcePayloadSchema = Schema.extend(
49 createSourcePayloadRequiredSchema,
50 createSourcePayloadOptionalSchema,
51);
52
53export type CreateSourcePayload = typeof CreateSourcePayloadSchema.Type;
54
55export 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
69export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type;
70
71export const CredentialPageUrlParamsSchema = Schema.Struct({
72 interactionId: ExecutionInteractionIdSchema,
73});
74
75export const CredentialSubmitPayloadSchema = Schema.Struct({
76 action: Schema.optional(Schema.Literal("submit", "continue", "cancel")),
77 token: Schema.optional(Schema.String),
78});
79
80export 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
87export const WorkspaceOauthClientQuerySchema = Schema.Struct({
88 providerKey: Schema.String,
89});
90
91export const CreateWorkspaceOauthClientPayloadSchema = Schema.Struct({
92 providerKey: Schema.String,
93 label: Schema.optional(Schema.NullOr(Schema.String)),
94 oauthClient: SourceOauthClientInputSchema,
95});
96
97export type CreateWorkspaceOauthClientPayload =
98 typeof CreateWorkspaceOauthClientPayloadSchema.Type;
99
100export const oauthClientIdParam = WorkspaceOauthClientIdSchema;
101export const grantIdParam = ProviderAuthGrantIdSchema;
102
103const 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
112export const ConnectSourceBatchPayloadSchema = Schema.Struct({
113 workspaceOauthClientId: WorkspaceOauthClientIdSchema,
114 sources: Schema.Array(ConnectGoogleDiscoveryBatchSourceSchema),
115});
116
117export type ConnectSourceBatchPayload = typeof ConnectSourceBatchPayloadSchema.Type;
118
119export 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
135export type ConnectSourceBatchResult = typeof ConnectSourceBatchResultSchema.Type;
136
137export const DiscoverSourcePayloadSchema = Schema.Struct({
138 url: TrimmedNonEmptyStringSchema,
139 probeAuth: Schema.optional(SourceProbeAuthSchema),
140});
141
142export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type;
143
144export 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
162export type ConnectSourceResult = typeof ConnectSourceResultSchema.Type;
163
164export {
165 ConnectSourcePayloadSchema,
166 type ConnectSourcePayload,
167 SourceDiscoveryResultSchema,
168 WorkspaceIdSchema,
169 WorkspaceOauthClientSchema,
170};
packages/platform/sdk/src/sources/discovery.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-discovery.ts+3−3
@@ -9,7 +9,7 @@ import { · 8 unmodified lines
9 type SourceDiscoveryResult,9 type SourceDiscoveryResult,
10} from "@executor/source-core";10} from "@executor/source-core";
11 11
12import { builtInSourceAdapters } from "./source-adapters";12import { builtInSourceAdapters } from "../runtime/sources/source-adapters";
13 13
14type DiscoverableSourceAdapter = SourceAdapter & {14type DiscoverableSourceAdapter = SourceAdapter & {
15 detectSource: NonNullable<SourceAdapter["detectSource"]>;15 detectSource: NonNullable<SourceAdapter["detectSource"]>;
@@ -36,8 +36,8 @@ export const discoverSource = (input: { · 20 unmodified lines
36 36
37 const adaptersByPriority = [...discoverableSourceAdapters].sort(37 const adaptersByPriority = [...discoverableSourceAdapters].sort(
38 (left, right) =>38 (left, right) =>
39 (right.discoveryPriority?.({ normalizedUrl }) ?? 0)39 (right.discoveryPriority?.({ normalizedUrl }) ?? 0) -
40 - (left.discoveryPriority?.({ normalizedUrl }) ?? 0),40 (left.discoveryPriority?.({ normalizedUrl }) ?? 0),
41 );41 );
42 42
43 for (const adapter of adaptersByPriority) {43 for (const adapter of adaptersByPriority) {
packages/platform/sdk/src/sources/inspection.tsrenamed from packages/platform/control-plane/src/runtime/sources/source-inspection.ts+88−41
@@ -9,23 +9,23 @@ import type { · 8 unmodified lines
9 SourceInspectionToolListItem,9 SourceInspectionToolListItem,
10 SourceInspectionToolSummary,10 SourceInspectionToolSummary,
11 WorkspaceId,11 WorkspaceId,
12} from "#schema";12} from "../schema";
13import {13import {
14 ControlPlaneNotFoundError,14 ControlPlaneNotFoundError,
15 ControlPlaneStorageError,15 ControlPlaneStorageError,
16} from "../../api/errors";16} from "../errors";
17import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
18 18
19import { LocalSourceArtifactMissingError } from "../local/errors";19import { LocalSourceArtifactMissingError } from "../runtime/local/errors";
20import { operationErrors } from "../policy/operation-errors";20import { operationErrors } from "../runtime/policy/operation-errors";
21import {21import {
22 buildLoadedSourceCatalogToolContract,22 buildLoadedSourceCatalogToolContract,
23 expandCatalogToolByPath,23 expandCatalogToolByPath,
24 expandCatalogTools,24 expandCatalogTools,
25 loadSourceWithCatalog,25 loadSourceWithCatalog,
26 type LoadedSourceCatalogTool,26 type LoadedSourceCatalogTool,
27} from "../catalog/source/runtime";27} from "../runtime/catalog/source/runtime";
28import { RuntimeSourceStoreService } from "./source-store";28import { RuntimeSourceStoreService } from "../runtime/sources/source-store";
29 29
30const sourceInspectOps = {30const sourceInspectOps = {
31 bundle: operationErrors("sources.inspect.bundle"),31 bundle: operationErrors("sources.inspect.bundle"),
@@ -41,9 +41,9 @@ const tokenize = (value: string): Array<string> => · 9 unmodified lines
41 .filter(Boolean);41 .filter(Boolean);
42 42
43const canInspectSourceWithoutCatalog = (source: Source): boolean =>43const canInspectSourceWithoutCatalog = (source: Source): boolean =>
44 source.status === "draft"44 source.status === "draft" ||
45 || source.status === "probing"45 source.status === "probing" ||
46 || source.status === "auth_required";46 source.status === "auth_required";
47 47
48const loadSourceForMissingCatalog = (input: {48const loadSourceForMissingCatalog = (input: {
49 workspaceId: WorkspaceId;49 workspaceId: WorkspaceId;
@@ -88,7 +88,8 @@ const loadSourceCatalogOrEmpty = (input: { · 38 unmodified lines
88 kind: "empty" as const,88 kind: "empty" as const,
89 source,89 source,
90 };90 };
91 })),91 }),
92 ),
92 );93 );
93 94
94const executableDetails = (tool: LoadedSourceCatalogTool) => {95const executableDetails = (tool: LoadedSourceCatalogTool) => {
@@ -105,7 +106,9 @@ const executableDetails = (tool: LoadedSourceCatalogTool) => { · 10 unmodified lines
105 };106 };
106};107};
107 108
108const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({109const inspectionToolListItemFromTool = (
110 tool: LoadedSourceCatalogTool,
111): SourceInspectionToolListItem => ({
109 path: tool.path,112 path: tool.path,
110 method: executableDetails(tool).method,113 method: executableDetails(tool).method,
111 ...(tool.descriptor.contract?.inputTypePreview114 ...(tool.descriptor.contract?.inputTypePreview
@@ -116,15 +119,23 @@ const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceIn · 4 unmodified lines
116 : {}),119 : {}),
117});120});
118 121
119const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {122const persistedToolSummaryFromTool = (
123 tool: LoadedSourceCatalogTool,
124): SourceInspectionToolSummary => {
120 const details = executableDetails(tool);125 const details = executableDetails(tool);
121 126
122 return {127 return {
123 path: tool.path,128 path: tool.path,
124 sourceKey: tool.source.id,129 sourceKey: tool.source.id,
125 ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}),130 ...(tool.capability.surface.title
131 ? { title: tool.capability.surface.title }
132 : {}),
126 ...(tool.capability.surface.summary || tool.capability.surface.description133 ...(tool.capability.surface.summary || tool.capability.surface.description
127 ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! }134 ? {
135 description:
136 tool.capability.surface.summary ??
137 tool.capability.surface.description!,
138 }
128 : {}),139 : {}),
129 protocol: details.protocol,140 protocol: details.protocol,
130 toolId: tool.path.split(".").at(-1) ?? tool.path,141 toolId: tool.path.split(".").at(-1) ?? tool.path,
@@ -145,7 +156,10 @@ const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInsp · 14 unmodified lines
145};156};
146 157
147const nativeEncodingLanguage = (encoding: string | undefined): string =>158const 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 ? encoding163 ? encoding
150 : "json";164 : "json";
151 165
@@ -171,12 +185,24 @@ export const inspectionToolDetailFromTool = ( · 19 unmodified lines
171 const contract = yield* buildLoadedSourceCatalogToolContract(tool);185 const contract = yield* buildLoadedSourceCatalogToolContract(tool);
172 const overviewItems = [186 const overviewItems = [
173 { label: "Protocol", value: details.protocol },187 { label: "Protocol", value: details.protocol },
174 ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []),188 ...(details.method
175 ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []),189 ? [{ label: "Method", value: details.method, mono: true }]
176 ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []),190 : []),
177 ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []),191 ...(details.pathTemplate
178 ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []),192 ? [{ label: "Target", value: details.pathTemplate, mono: true }]
179 ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []),193 : []),
194 ...(details.operationId
195 ? [{ label: "Operation", value: details.operationId, mono: true }]
196 : []),
197 ...(details.group
198 ? [{ label: "Group", value: details.group, mono: true }]
199 : []),
200 ...(details.leaf
201 ? [{ label: "Leaf", value: details.leaf, mono: true }]
202 : []),
203 ...(details.rawToolId
204 ? [{ label: "Raw tool", value: details.rawToolId, mono: true }]
205 : []),
180 { label: "Signature", value: contract.callSignature, mono: true },206 { label: "Signature", value: contract.callSignature, mono: true },
181 { label: "Call shape", value: contract.callShapeId, mono: true },207 { label: "Call shape", value: contract.callShapeId, mono: true },
182 ...(contract.resultShapeId208 ...(contract.resultShapeId
@@ -211,11 +237,13 @@ export const inspectionToolDetailFromTool = ( · 28 unmodified lines
211 items: overviewItems,237 items: overviewItems,
212 },238 },
213 ...(summary.description239 ...(summary.description
214 ? [{240 ? [
215 kind: "markdown" as const,241 {
216 title: "Description",242 kind: "markdown" as const,
217 body: summary.description,243 title: "Description",
218 }]244 body: summary.description,
245 },
246 ]
219 : []),247 : []),
220 ...([248 ...([
221 jsonSection("Capability", tool.capability),249 jsonSection("Capability", tool.capability),
@@ -231,7 +259,9 @@ export const inspectionToolDetailFromTool = ( · 9 unmodified lines
231 summary: tool.capability.surface.summary,259 summary: tool.capability.surface.summary,
232 description: tool.capability.surface.description,260 description: tool.capability.surface.description,
233 }),261 }),
234 ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>),262 ].filter((section) => section !== null) as Array<
263 SourceInspectionToolDetail["sections"][number]
264 >),
235 ...nativeSections,265 ...nativeSections,
236 ];266 ];
237 267
@@ -323,7 +353,9 @@ const scoreTool = (input: { · 85 unmodified lines
323 const titleTokens = tokenize(summary.title ?? "");353 const titleTokens = tokenize(summary.title ?? "");
324 const descriptionTokens = tokenize(summary.description ?? "");354 const descriptionTokens = tokenize(summary.description ?? "");
325 const tagTokens = summary.tags.flatMap(tokenize);355 const tagTokens = summary.tags.flatMap(tokenize);
326 const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`);356 const methodPathTokens = tokenize(
357 `${summary.method ?? ""} ${summary.pathTemplate ?? ""}`,
358 );
327 359
328 for (const token of input.queryTokens) {360 for (const token of input.queryTokens) {
329 if (pathTokens.includes(token)) {361 if (pathTokens.includes(token)) {
@@ -346,7 +378,10 @@ const scoreTool = (input: { · 16 unmodified lines
346 reasons.push(`method/path matches ${token} (+6)`);378 reasons.push(`method/path matches ${token} (+6)`);
347 continue;379 continue;
348 }380 }
349 if (descriptionTokens.includes(token) || input.tool.searchText.includes(token)) {381 if (
382 descriptionTokens.includes(token) ||
383 input.tool.searchText.includes(token)
384 ) {
350 score += 2;385 score += 2;
351 reasons.push(`description/text matches ${token} (+2)`);386 reasons.push(`description/text matches ${token} (+2)`);
352 }387 }
@@ -360,8 +395,12 @@ const scoreTool = (input: { · 7 unmodified lines
360 path: input.tool.path,395 path: input.tool.path,
361 score,396 score,
362 ...(summary.description ? { description: summary.description } : {}),397 ...(summary.description ? { description: summary.description } : {}),
363 ...(summary.inputTypePreview ? { inputTypePreview: summary.inputTypePreview } : {}),398 ...(summary.inputTypePreview
364 ...(summary.outputTypePreview ? { outputTypePreview: summary.outputTypePreview } : {}),399 ? { inputTypePreview: summary.inputTypePreview }
400 : {}),
401 ...(summary.outputTypePreview
402 ? { outputTypePreview: summary.outputTypePreview }
403 : {}),
365 reasons,404 reasons,
366 };405 };
367};406};
@@ -402,7 +441,9 @@ export const getSourceInspection = (input: { · 34 unmodified lines
402 namespace: inspection.namespace,441 namespace: inspection.namespace,
403 pipelineKind: inspection.pipelineKind,442 pipelineKind: inspection.pipelineKind,
404 toolCount: inspection.tools.length,443 toolCount: inspection.tools.length,
405 tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)),444 tools: inspection.tools.map((tool) =>
445 inspectionToolListItemFromTool(tool),
446 ),
406 } satisfies SourceInspection;447 } satisfies SourceInspection;
407 }).pipe(448 }).pipe(
408 Effect.mapError((cause) =>449 Effect.mapError((cause) =>
@@ -410,7 +451,8 @@ export const getSourceInspection = (input: { · 1 unmodified line
410 sourceInspectOps.bundle,451 sourceInspectOps.bundle,
411 cause,452 cause,
412 "Failed building source inspection bundle",453 "Failed building source inspection bundle",
413 )),454 ),
455 ),
414 );456 );
415 457
416export const getSourceInspectionToolDetail = (input: {458export const getSourceInspectionToolDetail = (input: {
@@ -428,9 +470,9 @@ export const getSourceInspectionToolDetail = (input: { · 11 unmodified lines
428 470
429 if (!tool) {471 if (!tool) {
430 return yield* sourceInspectOps.tool.notFound(472 return yield* sourceInspectOps.tool.notFound(
431 "Tool not found",473 "Tool not found",
432 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`,474 `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`,
433 );475 );
434 }476 }
435 477
436 return yield* inspectionToolDetailFromTool(tool);478 return yield* inspectionToolDetailFromTool(tool);
@@ -440,7 +482,8 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines
440 sourceInspectOps.tool,482 sourceInspectOps.tool,
441 cause,483 cause,
442 "Failed building source inspection tool detail",484 "Failed building source inspection tool detail",
443 )),485 ),
486 ),
444 );487 );
445 488
446export const discoverSourceInspectionTools = (input: {489export const discoverSourceInspectionTools = (input: {
@@ -463,9 +506,12 @@ export const discoverSourceInspectionTools = (input: { · 16 unmodified lines
463 tool,506 tool,
464 }),507 }),
465 )508 )
466 .filter((value): value is SourceInspectionDiscoverResultItem => value !== null)509 .filter(
467 .sort((left, right) =>510 (value): value is SourceInspectionDiscoverResultItem => value !== null,
468 right.score - left.score || left.path.localeCompare(right.path),511 )
512 .sort(
513 (left, right) =>
514 right.score - left.score || left.path.localeCompare(right.path),
469 )515 )
470 .slice(0, input.payload.limit ?? 12);516 .slice(0, input.payload.limit ?? 12);
471 517
@@ -482,5 +528,6 @@ export const discoverSourceInspectionTools = (input: { · 10 unmodified lines
482 sourceInspectOps.discover,528 sourceInspectOps.discover,
483 cause,529 cause,
484 "Failed building source inspection discovery",530 "Failed building source inspection discovery",
485 )),531 ),
532 ),
486 );533 );
packages/platform/sdk/src/sources/operations.tsrenamed from packages/platform/control-plane/src/runtime/sources/sources-operations.ts+85−72
@@ -1,33 +1,30 @@
1import type {1import type {
2 CreateSourcePayload,2 CreateSourcePayload,
3 UpdateSourcePayload,3 UpdateSourcePayload,
4} from "../../api/sources/api";4} from "./contracts";
5import {5import {
6 type AccountId,6 type AccountId,
7 SourceIdSchema,7 SourceIdSchema,
8 type Source,8 type Source,
9 type SourceId,9 type SourceId,
10 type WorkspaceId,10 type WorkspaceId,
11} from "#schema";11} from "../schema";
12import * as Either from "effect/Either";12import * as Either from "effect/Either";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14 14
15import {15import {
16 createSourceFromPayload,16 createSourceFromPayload,
17 updateSourceFromPayload,17 updateSourceFromPayload,
18} from "./source-definitions";18} from "../runtime/sources/source-definitions";
19import { getSourceAdapterForSource } from "./source-adapters";19import { getSourceAdapterForSource } from "../runtime/sources/source-adapters";
20import { mapPersistenceError } from "../runtime/policy/operations-shared";
21import { operationErrors } from "../runtime/policy/operation-errors";
20import {22import {
21 mapPersistenceError,23 ControlPlaneStore,
22} from "../policy/operations-shared";24 type ControlPlaneStoreShape,
23import {25} from "../runtime/store";
24 operationErrors,26import { RuntimeSourceCatalogSyncService } from "../runtime/catalog/source/sync";
25} from "../policy/operation-errors";27import { RuntimeSourceStoreService } from "../runtime/sources/source-store";
26import { ControlPlaneStore, type ControlPlaneStoreShape } from "../store";
27import { RuntimeSourceCatalogSyncService } from "../catalog/source/sync";
28import {
29 RuntimeSourceStoreService,
30} from "./source-store";
31 28
32const sourceOps = {29const sourceOps = {
33 list: operationErrors("sources.list"),30 list: operationErrors("sources.list"),
@@ -45,9 +42,7 @@ const syncArtifactsForSource = (input: { · 11 unmodified lines
45 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;42 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
46 source: Source;43 source: Source;
47 actorAccountId: AccountId;44 actorAccountId: AccountId;
48 operation:45 operation: typeof sourceOps.create | typeof sourceOps.update;
49 | typeof sourceOps.create
50 | typeof sourceOps.update;
51}) =>46}) =>
52 Effect.gen(function* () {47 Effect.gen(function* () {
53 const catalogSyncService = yield* RuntimeSourceCatalogSyncService;48 const catalogSyncService = yield* RuntimeSourceCatalogSyncService;
@@ -96,7 +91,10 @@ const syncArtifactsForSource = (input: { · 42 unmodified lines
96 }),91 }),
97 onLeft: (error) =>92 onLeft: (error) =>
98 Effect.gen(function* () {93 Effect.gen(function* () {
99 if (autoProbe || (input.source.enabled && input.source.status === "connected")) {94 if (
95 autoProbe ||
96 (input.source.enabled && input.source.status === "connected")
97 ) {
100 const erroredSource = yield* updateSourceFromPayload({98 const erroredSource = yield* updateSourceFromPayload({
101 source: input.source,99 source: input.source,
102 payload: {100 payload: {
@@ -104,11 +102,11 @@ const syncArtifactsForSource = (input: { · 1 unmodified line
104 lastError: error.message,102 lastError: error.message,
105 },103 },
106 now: Date.now(),104 now: Date.now(),
107 }).pipe(105 }).pipe(
108 Effect.mapError((cause) =>106 Effect.mapError((cause) =>
109 input.operation.badRequest(107 input.operation.badRequest(
110 "Failed indexing source tools",108 "Failed indexing source tools",
111 cause instanceof Error ? cause.message : String(cause),109 cause instanceof Error ? cause.message : String(cause),
112 ),110 ),
113 ),111 ),
114 );112 );
@@ -121,7 +119,10 @@ const syncArtifactsForSource = (input: { · 6 unmodified lines
121 );119 );
122 }120 }
123 121
124 return yield* input.operation.unknownStorage(error, "Failed syncing source tools");122 return yield* input.operation.unknownStorage(
123 error,
124 "Failed syncing source tools",
125 );
125 }),126 }),
126 });127 });
127 });128 });
@@ -134,17 +135,20 @@ export const listSources = (input: { · 6 unmodified lines
134 Effect.gen(function* () {135 Effect.gen(function* () {
135 const sourceStore = yield* RuntimeSourceStoreService;136 const sourceStore = yield* RuntimeSourceStoreService;
136 137
137 return yield* sourceStore.loadSourcesInWorkspace(input.workspaceId, {138 return yield* sourceStore
138 actorAccountId: input.accountId,139 .loadSourcesInWorkspace(input.workspaceId, {
139 }).pipe(140 actorAccountId: input.accountId,
140 Effect.mapError((error) =>141 })
141 sourceOps.list.unknownStorage(142 .pipe(
142 error,143 Effect.mapError((error) =>
143 "Failed projecting stored sources",144 sourceOps.list.unknownStorage(
145 error,
146 "Failed projecting stored sources",
147 ),
144 ),148 ),
145 ),149 );
146 );150 }),
147 }));151 );
148 152
149export const createSource = (input: {153export const createSource = (input: {
150 workspaceId: WorkspaceId;154 workspaceId: WorkspaceId;
@@ -186,7 +190,8 @@ export const createSource = (input: { · 35 unmodified lines
186 });190 });
187 191
188 return synchronizedSource;192 return synchronizedSource;
189 }));193 }),
194 );
190 195
191export const getSource = (input: {196export const getSource = (input: {
192 workspaceId: WorkspaceId;197 workspaceId: WorkspaceId;
@@ -197,24 +202,28 @@ export const getSource = (input: { · 4 unmodified lines
197 Effect.gen(function* () {202 Effect.gen(function* () {
198 const sourceStore = yield* RuntimeSourceStoreService;203 const sourceStore = yield* RuntimeSourceStoreService;
199 204
200 return yield* sourceStore.loadSourceById({205 return yield* sourceStore
201 workspaceId: input.workspaceId,206 .loadSourceById({
202 sourceId: input.sourceId,207 workspaceId: input.workspaceId,
203 actorAccountId: input.accountId,208 sourceId: input.sourceId,
204 }).pipe(209 actorAccountId: input.accountId,
205 Effect.mapError((cause) =>210 })
206 cause instanceof Error && cause.message.startsWith("Source not found:")211 .pipe(
207 ? sourceOps.get.notFound(212 Effect.mapError((cause) =>
208 "Source not found",213 cause instanceof Error &&
209 `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,214 cause.message.startsWith("Source not found:")
210 )215 ? sourceOps.get.notFound(
211 : sourceOps.get.unknownStorage(216 "Source not found",
212 cause,217 `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
213 "Failed projecting stored source",218 )
214 ),219 : sourceOps.get.unknownStorage(
215 ),220 cause,
216 );221 "Failed projecting stored source",
217 }));222 ),
223 ),
224 );
225 }),
226 );
218 227
219export const updateSource = (input: {228export const updateSource = (input: {
220 workspaceId: WorkspaceId;229 workspaceId: WorkspaceId;
@@ -225,23 +234,26 @@ export const updateSource = (input: { · 4 unmodified lines
225 Effect.flatMap(ControlPlaneStore, (store) =>234 Effect.flatMap(ControlPlaneStore, (store) =>
226 Effect.gen(function* () {235 Effect.gen(function* () {
227 const sourceStore = yield* RuntimeSourceStoreService;236 const sourceStore = yield* RuntimeSourceStoreService;
228 const existingSource = yield* sourceStore.loadSourceById({237 const existingSource = yield* sourceStore
229 workspaceId: input.workspaceId,238 .loadSourceById({
230 sourceId: input.sourceId,239 workspaceId: input.workspaceId,
231 actorAccountId: input.accountId,240 sourceId: input.sourceId,
232 }).pipe(241 actorAccountId: input.accountId,
233 Effect.mapError((cause) =>242 })
234 cause instanceof Error && cause.message.startsWith("Source not found:")243 .pipe(
235 ? sourceOps.update.notFound(244 Effect.mapError((cause) =>
236 "Source not found",245 cause instanceof Error &&
237 `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,246 cause.message.startsWith("Source not found:")
238 )247 ? sourceOps.update.notFound(
239 : sourceOps.update.unknownStorage(248 "Source not found",
240 cause,249 `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
241 "Failed projecting stored source",250 )
242 ),251 : sourceOps.update.unknownStorage(
243 ),252 cause,
244 );253 "Failed projecting stored source",
254 ),
255 ),
256 );
245 257
246 const updatedSource = yield* updateSourceFromPayload({258 const updatedSource = yield* updateSourceFromPayload({
247 source: existingSource,259 source: existingSource,
@@ -272,7 +284,8 @@ export const updateSource = (input: { · 24 unmodified lines
272 });284 });
273 285
274 return synchronizedSource;286 return synchronizedSource;
275 }));287 }),
288 );
276 289
277export const removeSource = (input: {290export const removeSource = (input: {
278 workspaceId: WorkspaceId;291 workspaceId: WorkspaceId;
@@ -290,5 +303,5 @@ export const removeSource = (input: { · 11 unmodified lines
290 );303 );
291 304
292 return { removed };305 return { removed };
293 })306 }),
294 );307 );
packages/platform/sdk/src/string-schemas.tsrenamed from packages/platform/control-plane/src/api/string-schemas.ts+0−0

File renamed without changes.

packages/platform/sdk/tsconfig.json+14−0
@@ -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}
packages/platform/sdk/vitest.config.ts+8−0
@@ -0,0 +1,8 @@
1import { defineConfig } from "vitest/config";
2
3export default defineConfig({
4 test: {
5 include: ["src/**/*.test.ts"],
6 testTimeout: 30_000,
7 },
8});
packages/platform/server/package.json+3−1
@@ -12,8 +12,10 @@ · 11 unmodified lines
12 "@effect/opentelemetry": "^0.61.0",12 "@effect/opentelemetry": "^0.61.0",
13 "@effect/platform": "catalog:",13 "@effect/platform": "catalog:",
14 "@effect/platform-node": "catalog:",14 "@effect/platform-node": "catalog:",
15 "@executor/control-plane": "workspace:*",
16 "@executor/executor-mcp": "workspace:*",15 "@executor/executor-mcp": "workspace:*",
16 "@executor/platform-api": "workspace:*",
17 "@executor/platform-internal": "workspace:*",
18 "@executor/platform-sdk": "workspace:*",
17 "@opentelemetry/api": "^1.9.0",19 "@opentelemetry/api": "^1.9.0",
18 "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",20 "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
19 "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",21 "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
packages/platform/server/src/index.ts+6−2
@@ -10,12 +10,15 @@ import { FileSystem, HttpApiBuilder, HttpServer } from "@effect/platform"; · 9 unmodified lines
10import { NodeFileSystem } from "@effect/platform-node";10import { NodeFileSystem } from "@effect/platform-node";
11import {11import {
12 createControlPlaneApiLayer,12 createControlPlaneApiLayer,
13} from "@executor/platform-api";
14import { createExecutorMcpRequestHandler } from "@executor/executor-mcp";
15import { createExecutorAdminToolMap } from "@executor/platform-internal";
16import {
13 createControlPlaneRuntime,17 createControlPlaneRuntime,
14 type ResolveExecutionEnvironment,18 type ResolveExecutionEnvironment,
15 type ResolveSecretMaterial,19 type ResolveSecretMaterial,
16 type ControlPlaneRuntime,20 type ControlPlaneRuntime,
17} from "@executor/control-plane";21} from "@executor/platform-sdk/runtime";
18import { createExecutorMcpRequestHandler } from "@executor/executor-mcp";
19import * as Effect from "effect/Effect";22import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";23import * as Layer from "effect/Layer";
21import type * as Scope from "effect/Scope";24import type * as Scope from "effect/Scope";
@@ -110,6 +113,7 @@ const createRuntime = ( · 88 unmodified lines
110 createControlPlaneRuntime({113 createControlPlaneRuntime({
111 workspaceRoot: options.workspaceRoot,114 workspaceRoot: options.workspaceRoot,
112 executionResolver: options.executionResolver,115 executionResolver: options.executionResolver,
116 createInternalToolMap: createExecutorAdminToolMap,
113 resolveSecretMaterial: options.resolveSecretMaterial,117 resolveSecretMaterial: options.resolveSecretMaterial,
114 getLocalServerBaseUrl,118 getLocalServerBaseUrl,
115 localDataDir,119 localDataDir,
packages/sources/openapi/src/real-spec-coverage.test.ts+1−1
@@ -20,7 +20,7 @@ const readFixture = (name: string) => · 19 unmodified lines
20 Effect.flatMap((fs) =>20 Effect.flatMap((fs) =>
21 fs.readFileString(21 fs.readFileString(
22 fileURLToPath(22 fileURLToPath(
23 new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),23 new URL(`../../../platform/sdk/src/runtime/fixtures/${name}`, import.meta.url),
24 ),24 ),
25 "utf8",25 "utf8",
26 )26 )
packages/sources/openapi/src/tool-presentation.test.ts+1−1
@@ -15,7 +15,7 @@ const readFixture = (name: string) => · 14 unmodified lines
15 Effect.flatMap((fs) =>15 Effect.flatMap((fs) =>
16 fs.readFileString(16 fs.readFileString(
17 fileURLToPath(17 fileURLToPath(
18 new URL(`../../../platform/control-plane/src/runtime/fixtures/${name}`, import.meta.url),18 new URL(`../../../platform/sdk/src/runtime/fixtures/${name}`, import.meta.url),
19 ),19 ),
20 "utf8",20 "utf8",
21 )21 )