History

Rename the integration entity from 'sources' to 'integrations' (#1370)

Completes the v2 naming fold: config key, SDK types, plugin contract fields, sandbox tool paths, CLI subcommand, UI copy, docs, and e2e selectors now say integration. v1 migration readers keep the old names they read from real data; the v1 sourceUrl field maps to specUrl at the migration boundary.

Rhys Sullivan committed 4 months ago

Commit 77c7cbc1 parent 55a2463151 files · +659 −627Browse files

Unified · Split

151 files
  1. AGENTS.md +1 −1
  2. README.md +7 −7
  3. apps/cli/src/main.ts +11 −7
  4. apps/cli/src/tooling.ts +2 −2
  5. apps/cloud/src/db/db.schema.test.ts +2 −2
  6. apps/cloud/src/db/db.ts +1 −1
  7. apps/cloud/src/mcp-session.e2e.node.test.ts +1 −1
  8. apps/cloud/src/web/pages/login.tsx +3 −1
  9. apps/cloud/vite.config.ts +1 −1
  10. apps/docs/concepts/integrations.mdx +1 −1
  11. apps/docs/local/cli.mdx +3 −3
  12. apps/host-selfhost/src/integrations-mcp.test.ts +2 −2
  13. apps/host-selfhost/src/integrations.test.ts +0 −0
  14. apps/local/src/auth-tool-failures.test.ts +2 −2
  15. apps/local/src/mcp-browser-resume.test.ts +1 −1
  16. apps/local/src/serve.test.ts +1 −1
  17. apps/marketing/src/pages/privacy.astro +2 −2
  18. design.md +6 −6
  19. e2e/cli/service-lifecycle.test.ts +2 −2
  20. e2e/cloud/auth-routing-flow.test.ts +1 −1
  21. e2e/cloud/cli-device-login.test.ts +3 −3
  22. e2e/cloud/integrations-api.test.ts +0 −0
  23. e2e/cloud/integrations-refresh.test.ts +0 −0
  24. e2e/cloud/oauth-callback-unauthenticated.test.ts +1 −1
  25. e2e/cloud/toolkit-opencode-real.test.ts +3 −3
  26. e2e/cloud/toolkit-policy-perf.test.ts +8 −8
  27. e2e/cloud/unauthenticated-skeleton.test.ts +4 −2
  28. e2e/local/auth.test.ts +1 −1
  29. e2e/local/update-notice.test.ts +1 −1
  30. e2e/scenarios/auth-methods.test.ts +4 −4
  31. e2e/scenarios/microsoft-emulator.test.ts +1 −1
  32. e2e/scenarios/microsoft-graph-default.test.ts +3 −1
  33. e2e/scenarios/microsoft-graph-full.test.ts +3 −1
  34. e2e/scenarios/openapi-add-integration-action-bar.test.ts +4 −4
  35. e2e/scenarios/openapi-integration.test.ts +0 −0
  36. e2e/scenarios/openapi-server-selection-ui.test.ts +2 −2
  37. e2e/scenarios/support/large-catalog.ts +29 −29
  38. e2e/scenarios/tool-descriptions.test.ts +2 −2
  39. e2e/selfhost/cli-device-login.test.ts +3 −3
  40. packages/app/src/web/shell.tsx +5 −5
  41. packages/app/vite.ts +1 −1
  42. packages/core/api/src/plugin-routes.ts +1 −1
  43. packages/core/config/src/config.test.ts +37 −35
  44. packages/core/config/src/index.ts +7 −7
  45. packages/core/config/src/schema.ts +16 −16
  46. packages/core/config/src/sink.ts +10 −10
  47. packages/core/config/src/write.ts +24 −24
  48. packages/core/execution/src/engine.test.ts +1 −1
  49. packages/core/execution/src/engine.ts +7 −7
  50. packages/core/execution/src/index.ts +1 −1
  51. packages/core/execution/src/tool-invoker.leak.test.ts +1 −1
  52. packages/core/execution/src/tool-invoker.test.ts +11 −11
  53. packages/core/execution/src/tool-invoker.ts +17 −17
  54. packages/core/sdk/README.md +3 −3
  55. packages/core/sdk/src/auth-tool-failure.ts +7 −5
  56. packages/core/sdk/src/blob.ts +1 −1
  57. packages/core/sdk/src/client.test.ts +2 −2
  58. packages/core/sdk/src/client.ts +5 −5
  59. packages/core/sdk/src/core-tools.ts +1 −1
  60. packages/core/sdk/src/executor.ts +38 −36
  61. packages/core/sdk/src/index.ts +1 −1
  62. packages/core/sdk/src/migration-spec.test.ts +2 −2
  63. packages/core/sdk/src/migration-spec.ts +2 −2
  64. packages/core/sdk/src/plugin.ts +13 −11
  65. packages/core/sdk/src/policies.test.ts +1 −1
  66. packages/core/sdk/src/promise-executor.ts +1 −1
  67. packages/core/sdk/src/promise.test.ts +2 −2
  68. packages/core/sdk/src/sqlite-config-blob-migration.test.ts +2 −2
  69. packages/hosts/mcp/src/tool-server.test.ts +1 −1
  70. packages/kernel/core/src/strip-types.test.ts +1 −1
  71. packages/kernel/ir/src/registry.ts +2 −2
  72. packages/kernel/ir/src/serialize.ts +2 −2
  73. packages/plugins/desktop-settings/src/client.tsx +5 −5
  74. packages/plugins/desktop-settings/src/server.ts +1 −1
  75. packages/plugins/graphql/README.md +2 −2
  76. packages/plugins/graphql/src/react/AddGraphqlIntegration.tsx +6 −6
  77. packages/plugins/graphql/src/react/GraphqlAccountsPanel.tsx +3 −3
  78. packages/plugins/graphql/src/react/GraphqlIntegrationFields.tsx +1 −1
  79. packages/plugins/graphql/src/react/index.ts +1 −1
  80. packages/plugins/graphql/src/react/integration-plugin.ts +1 −1
  81. packages/plugins/graphql/src/react/plugin-client.tsx +1 −1
  82. packages/plugins/graphql/src/sdk/plugin.ts +4 −4
  83. packages/plugins/mcp/README.md +4 −4
  84. packages/plugins/mcp/src/react/AddMcpIntegration.tsx +3 −3
  85. packages/plugins/mcp/src/react/EditMcpIntegration.tsx +8 −5
  86. packages/plugins/mcp/src/react/McpAccountsPanel.tsx +3 −3
  87. packages/plugins/mcp/src/react/McpRemoteIntegrationFields.tsx +3 −3
  88. packages/plugins/mcp/src/react/McpSignInButton.tsx +2 −2
  89. packages/plugins/mcp/src/react/index.ts +2 −2
  90. packages/plugins/mcp/src/react/integration-plugin.tsx +5 −5
  91. packages/plugins/mcp/src/react/plugin-client.tsx +2 −2
  92. packages/plugins/mcp/src/sdk/plugin.test.ts +1 −1
  93. packages/plugins/mcp/src/sdk/plugin.ts +5 −5
  94. packages/plugins/mcp/src/sdk/probe-shape-real-servers.live.test.ts +1 −1
  95. packages/plugins/onepassword/src/sdk/plugin.ts +1 −1
  96. packages/plugins/openapi/README.md +1 −1
  97. packages/plugins/openapi/src/api/group.ts +1 −1
  98. packages/plugins/openapi/src/api/handlers.ts +1 −1
  99. packages/plugins/openapi/src/providers/google/config.ts +1 −1
  100. packages/plugins/openapi/src/providers/google/discovery.test.ts +1 −1
  101. packages/plugins/openapi/src/providers/google/openapi-ownership-migration.test.ts +1 −1
  102. packages/plugins/openapi/src/providers/google/presets.ts +1 −1
  103. packages/plugins/openapi/src/providers/google/spec-format-adapter.ts +1 −1
  104. packages/plugins/openapi/src/providers/microsoft/graph.ts +2 −2
  105. packages/plugins/openapi/src/providers/microsoft/presets.ts +1 −1
  106. packages/plugins/openapi/src/providers/microsoft/spec-format-adapter.ts +1 −1
  107. packages/plugins/openapi/src/react/AddOpenApiIntegration.test.ts +1 −1
  108. packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx +16 −10
  109. packages/plugins/openapi/src/react/OpenApiAccountsPanel.tsx +3 −3
  110. packages/plugins/openapi/src/react/OpenApiIntegrationDetailsFields.tsx +1 −1
  111. packages/plugins/openapi/src/react/UpdateSpecSection.tsx +4 −4
  112. packages/plugins/openapi/src/react/index.ts +2 −2
  113. packages/plugins/openapi/src/react/integration-plugin.ts +1 −1
  114. packages/plugins/openapi/src/react/plugin-client.tsx +2 −2
  115. packages/plugins/openapi/src/sdk/backing.ts +1 −1
  116. packages/plugins/openapi/src/sdk/config.ts +1 −1
  117. packages/plugins/openapi/src/sdk/configure.test.ts +2 −2
  118. packages/plugins/openapi/src/sdk/describe-auth-methods.test.ts +3 −3
  119. packages/plugins/openapi/src/sdk/invoke.ts +6 −6
  120. packages/plugins/openapi/src/sdk/plugin.test.ts +2 −2
  121. packages/plugins/openapi/src/sdk/plugin.ts +26 −26
  122. packages/plugins/openapi/src/sdk/preview.ts +2 −2
  123. packages/plugins/openapi/src/sdk/real-specs.test.ts +1 −1
  124. packages/plugins/openapi/src/sdk/response-headers-timeout.test.ts +2 −2
  125. packages/plugins/openapi/src/sdk/spec-blob.test.ts +2 −2
  126. packages/plugins/openapi/src/sdk/spec-format.ts +1 −1
  127. packages/plugins/openapi/src/sdk/streaming-response.test.ts +2 −2
  128. packages/plugins/openapi/src/sdk/upstream-failures.test.ts +2 −2
  129. packages/plugins/openapi/src/testing/index.ts +20 −20
  130. packages/plugins/provider-service-split/src/planner.test.ts +5 −5
  131. packages/plugins/provider-service-split/src/planner.ts +4 −4
  132. packages/plugins/provider-service-split/src/sqlite.test.ts +1 −1
  133. packages/plugins/toolkits/src/page.tsx +11 −11
  134. packages/react/src/api/error-reporting.test.ts +6 −6
  135. packages/react/src/components/accounts-section.tsx +1 −1
  136. packages/react/src/components/command-palette.tsx +7 −7
  137. packages/react/src/components/integration-favicon.test.tsx +12 −10
  138. packages/react/src/components/integration-favicon.tsx +29 −29
  139. packages/react/src/components/integration-icon-with-account.tsx +2 −2
  140. packages/react/src/components/metadata-edit-sheet.tsx +3 −2
  141. packages/react/src/lib/integration-add.tsx +1 −1
  142. packages/react/src/multiplayer/shell.tsx +1 −1
  143. packages/react/src/pages/integration-detail.tsx +1 −1
  144. packages/react/src/pages/integrations.tsx +2 −2
  145. packages/react/src/plugins/integration-identity.tsx +7 −7
  146. packages/react/src/plugins/oauth-reconnect.test.ts +1 −1
  147. packages/react/src/plugins/oauth-reconnect.ts +1 −1
  148. scripts/agent-config-smoke.ts +5 −5
  149. tests/presets-reachable.test.ts +1 −1
  150. tests/tools-cli.test.ts +5 −5
  151. vision.md +25 −25
AGENTS.md+1−1
@@ -65,7 +65,7 @@ a given Git URL into `.reference` and pull latest before using it. · 64 unmodified lines
65 65
66## Package Roles66## Package Roles
67 67
68- `packages/core/sdk`: core contracts, plugin wiring, scopes, sources, secrets,68- `packages/core/sdk`: core contracts, plugin wiring, scopes, integrations, secrets,
69 policies, fixtures. `@executor-js/sdk/http-auth` carries the shared auth-method69 policies, fixtures. `@executor-js/sdk/http-auth` carries the shared auth-method
70 vocabulary the HTTP protocol plugins compose (core never imports it, keeping it70 vocabulary the HTTP protocol plugins compose (core never imports it, keeping it
71 carrier-agnostic).71 carrier-agnostic).
README.md+7−7
@@ -21,9 +21,9 @@ set its policy once, and every agent shares it over MCP. Your integrations, · 20 unmodified lines
21auth, and policies live in one place instead of being scattered across each21auth, and policies live in one place instead of being scattered across each
22client.22client.
23 23
24- **Any source.** First-party support for MCP servers, OpenAPI, GraphQL, and24- **Any integration.** First-party support for MCP servers, OpenAPI, GraphQL, and
25 Google Discovery. If you can describe it with a JSON schema, it can be an25 Google Discovery. If you can describe it with a JSON schema, it can be an
26 integration. The plugin system is open to any source type.26 integration. The plugin system is open to any integration type.
27- **One catalog, every agent.** Anything MCP-compatible connects to the same27- **One catalog, every agent.** Anything MCP-compatible connects to the same
28 set of tools.28 set of tools.
29- **Governed by policy.** Each tool is allowed, gated behind approval, or29- **Governed by policy.** Each tool is allowed, gated behind approval, or
@@ -100,12 +100,12 @@ appear. · 70 unmodified lines
100 100
101## Add an integration101## Add an integration
102 102
103From the web UI, click **Add Source**, paste an OpenAPI, GraphQL, or MCP URL,103From the web UI, click **Add Integration**, paste an OpenAPI, GraphQL, or MCP URL,
104and Executor detects the type, indexes the tools, and handles auth. Or add one104and Executor detects the type, indexes the tools, and handles auth. Or add one
105from the CLI:105from the CLI:
106 106
107```bash107```bash
108executor call executor openapi addSource '{108executor call executor openapi addIntegration '{
109 "spec": "https://petstore3.swagger.io/api/v3/openapi.json",109 "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
110 "namespace": "petstore",110 "namespace": "petstore",
111 "baseUrl": "https://petstore3.swagger.io/api/v3"111 "baseUrl": "https://petstore3.swagger.io/api/v3"
@@ -113,7 +113,7 @@ executor call executor openapi addSource '{ · 1 unmodified line
113```113```
114 114
115Use `baseUrl` when the OpenAPI document has relative `servers` entries (for115Use `baseUrl` when the OpenAPI document has relative `servers` entries (for
116example `"/api/v3"`). Confirm it is live with `executor tools sources`.116example `"/api/v3"`). Confirm it is live with `executor tools integrations`.
117 117
118## Using tools118## Using tools
119 119
@@ -169,7 +169,7 @@ executor call <path...> --help # browse namespaces/resources/methods · 49 unmodified lines
169executor call <path...> --help --match "<text>" --limit <n> # narrow huge namespaces169executor call <path...> --help --match "<text>" --limit <n> # narrow huge namespaces
170executor resume --execution-id <id> # resume paused execution170executor resume --execution-id <id> # resume paused execution
171executor tools search "<query>" # search tools by intent171executor tools search "<query>" # search tools by intent
172executor tools sources # list configured sources + tool counts172executor tools integrations # list configured integrations + tool counts
173executor tools describe <path> # show tool TypeScript/JSON schema173executor tools describe <path> # show tool TypeScript/JSON schema
174```174```
175 175
@@ -190,7 +190,7 @@ apps/ · 14 unmodified lines
190packages/190packages/
191 core/ contracts, plugin wiring, scopes, policies, SDK, API, CLI core191 core/ contracts, plugin wiring, scopes, policies, SDK, API, CLI core
192 kernel/ execution runtimes (QuickJS, Deno subprocess, dynamic worker)192 kernel/ execution runtimes (QuickJS, Deno subprocess, dynamic worker)
193 plugins/ source and provider plugins (openapi, graphql, mcp, google,193 plugins/ integration and provider plugins (openapi, graphql, mcp, google,
194 microsoft, 1password, keychain, encrypted/file secrets, ...)194 microsoft, 1password, keychain, encrypted/file secrets, ...)
195 hosts/ host adapters (MCP surface, Cloudflare)195 hosts/ host adapters (MCP surface, Cloudflare)
196 react/ shared React UI196 react/ shared React UI
apps/cli/src/main.ts+11−7
@@ -162,7 +162,7 @@ import { · 161 unmodified lines
162 buildDescribeToolCode,162 buildDescribeToolCode,
163 filterToolPathChildren,163 filterToolPathChildren,
164 buildInvokeToolCode,164 buildInvokeToolCode,
165 buildListSourcesCode,165 buildListIntegrationsCode,
166 buildSearchToolsCode,166 buildSearchToolsCode,
167 extractExecutionId,167 extractExecutionId,
168 extractPausedInteraction,168 extractPausedInteraction,
@@ -2052,8 +2052,8 @@ const toolsSearchCommand = Command.make( · 1883 unmodified lines
2052 }),2052 }),
2053).pipe(Command.withDescription("Search tools by natural-language query"));2053).pipe(Command.withDescription("Search tools by natural-language query"));
2054 2054
2055const toolsSourcesCommand = Command.make(2055const toolsIntegrationsCommand = Command.make(
2056 "sources",2056 "integrations",
2057 {2057 {
2058 query: Options.string("query").pipe(Options.optional),2058 query: Options.string("query").pipe(Options.optional),
2059 limit: Options.integer("limit").pipe(Options.withDefault(50)),2059 limit: Options.integer("limit").pipe(Options.withDefault(50)),
@@ -2065,7 +2065,7 @@ const toolsSourcesCommand = Command.make( · 5 unmodified lines
2065 Effect.gen(function* () {2065 Effect.gen(function* () {
2066 applyScope(scope);2066 applyScope(scope);
2067 const target = serverTargetFromOptions({ baseUrl, server });2067 const target = serverTargetFromOptions({ baseUrl, server });
2068 const code = buildListSourcesCode({2068 const code = buildListIntegrationsCode({
2069 query: Option.getOrUndefined(query),2069 query: Option.getOrUndefined(query),
2070 limit,2070 limit,
2071 });2071 });
@@ -2077,7 +2077,7 @@ const toolsSourcesCommand = Command.make( · 5 unmodified lines
2077 outcome: result.outcome,2077 outcome: result.outcome,
2078 });2078 });
2079 }),2079 }),
2080).pipe(Command.withDescription("List configured sources and tool counts"));2080).pipe(Command.withDescription("List configured integrations and tool counts"));
2081 2081
2082const toolsDescribeCommand = Command.make(2082const toolsDescribeCommand = Command.make(
2083 "describe",2083 "describe",
@@ -2102,8 +2102,12 @@ const toolsDescribeCommand = Command.make( · 18 unmodified lines
2102).pipe(Command.withDescription("Describe a tool's TypeScript and JSON schema"));2102).pipe(Command.withDescription("Describe a tool's TypeScript and JSON schema"));
2103 2103
2104const toolsCommand = Command.make("tools").pipe(2104const toolsCommand = Command.make("tools").pipe(
2105 Command.withSubcommands([toolsSearchCommand, toolsSourcesCommand, toolsDescribeCommand] as const),2105 Command.withSubcommands([
2106 Command.withDescription("Discover available tools and sources"),2106 toolsSearchCommand,
2107 toolsIntegrationsCommand,
2108 toolsDescribeCommand,
2109 ] as const),
2110 Command.withDescription("Discover available tools and integrations"),
2107);2111);
2108 2112
2109const profileConnectionInput = (input: {2113const profileConnectionInput = (input: {
apps/cli/src/tooling.ts+2−2
@@ -408,14 +408,14 @@ export const buildSearchToolsCode = (input: { · 407 unmodified lines
408 return `return await tools.search(${JSON.stringify(payload)});`;408 return `return await tools.search(${JSON.stringify(payload)});`;
409};409};
410 410
411export const buildListSourcesCode = (input: { query?: string; limit: number }): string => {411export const buildListIntegrationsCode = (input: { query?: string; limit: number }): string => {
412 const payload: Record<string, unknown> = {412 const payload: Record<string, unknown> = {
413 limit: input.limit,413 limit: input.limit,
414 };414 };
415 if (input.query && input.query.trim().length > 0) {415 if (input.query && input.query.trim().length > 0) {
416 payload.query = input.query;416 payload.query = input.query;
417 }417 }
418 return `return await tools.executor.sources.list(${JSON.stringify(payload)});`;418 return `return await tools.executor.integrations.list(${JSON.stringify(payload)});`;
419};419};
420 420
421export const buildDescribeToolCode = (toolPath: string): string =>421export const buildDescribeToolCode = (toolPath: string): string =>
apps/cloud/src/db/db.schema.test.ts+2−2
@@ -2,7 +2,7 @@ · 1 unmodified line
2// merge of cloud + executor schemas. If ANY call site builds its own2// merge of cloud + executor schemas. If ANY call site builds its own
3// `{ schema }` without spreading both, `db._.fullSchema` comes back3// `{ schema }` without spreading both, `db._.fullSchema` comes back
4// missing tables and the drizzle adapter throws4// missing tables and the drizzle adapter throws
5// `unknown model "source"` at the first request that touches scoped data.5// `unknown model "integration"` at the first request that touches scoped data.
6//6//
7// We hit this in prod (MCP endpoint) when `mcp-session.ts` built7// We hit this in prod (MCP endpoint) when `mcp-session.ts` built
8// `combinedSchema = { ...cloudSchema }` and forgot to spread the executor8// `combinedSchema = { ...cloudSchema }` and forgot to spread the executor
@@ -56,7 +56,7 @@ describe("combinedSchema", () => { · 47 unmodified lines
56 }56 }
57 });57 });
58 58
59 // Executor-schema drives the scope-sharded tables (source/tool/etc). If59 // Executor-schema drives the scope-sharded tables (integration/tool/etc). If
60 // any of these go missing the drizzle adapter's `getTable` lookup throws.60 // any of these go missing the drizzle adapter's `getTable` lookup throws.
61 it("includes scope-sharded executor tables", () => {61 it("includes scope-sharded executor tables", () => {
62 for (const key of Object.keys(executorSchema)) {62 for (const key of Object.keys(executorSchema)) {
apps/cloud/src/db/db.ts+1−1
@@ -24,7 +24,7 @@ import * as executorSchema from "./executor-schema"; · 23 unmodified lines
24 24
25// Exported so every drizzle() call in the cloud app shares one schema25// Exported so every drizzle() call in the cloud app shares one schema
26// object. Historically `mcp-session.ts` built its own and forgot to spread26// object. Historically `mcp-session.ts` built its own and forgot to spread
27// `executorSchema`, producing runtime "unknown model source" errors that27// `executorSchema`, producing runtime "unknown model integration" errors that
28// only surfaced in prod. See apps/cloud/src/db/db.schema.test.ts.28// only surfaced in prod. See apps/cloud/src/db/db.schema.test.ts.
29export const combinedSchema = { ...cloudSchema, ...executorSchema };29export const combinedSchema = { ...cloudSchema, ...executorSchema };
30 30
apps/cloud/src/mcp-session.e2e.node.test.ts+1−1
@@ -52,7 +52,7 @@ const EMPTY_INPUT_SCHEMA = Schema.toStandardSchemaV1( · 51 unmodified lines
52const elicitingTestPlugin = definePlugin(() => ({52const elicitingTestPlugin = definePlugin(() => ({
53 id: "eliciting-test" as const,53 id: "eliciting-test" as const,
54 storage: () => ({}),54 storage: () => ({}),
55 staticSources: () => [55 staticIntegrations: () => [
56 {56 {
57 id: "e2e",57 id: "e2e",
58 kind: "in-memory",58 kind: "in-memory",
apps/cloud/src/web/pages/login.tsx+3−1
@@ -13,7 +13,9 @@ export const LoginPage = ({ returnTo }: { returnTo?: string | undefined }) => { · 12 unmodified lines
13 <div className="mx-auto flex w-full max-w-sm flex-col items-center gap-6">13 <div className="mx-auto flex w-full max-w-sm flex-col items-center gap-6">
14 <div className="flex flex-col items-center gap-2">14 <div className="flex flex-col items-center gap-2">
15 <h1 className="font-mono text-4xl">Executor</h1>15 <h1 className="font-mono text-4xl">Executor</h1>
16 <p className="text-sm text-muted-foreground">Sign in to manage your tools and sources</p>16 <p className="text-sm text-muted-foreground">
17 Sign in to manage your tools and integrations
18 </p>
17 </div>19 </div>
18 <a20 <a
19 href={loginHref}21 href={loginHref}
apps/cloud/vite.config.ts+1−1
@@ -66,7 +66,7 @@ export default defineConfig(({ command, mode }) => { · 65 unmodified lines
66 }66 }
67 67
68 // Deps vite only discovers once a lazy-loaded React chunk actually renders68 // Deps vite only discovers once a lazy-loaded React chunk actually renders
69 // (e.g. opening the MCP/OpenAPI "add source" flow). Discovering them mid-run69 // (e.g. opening the MCP/OpenAPI "add integration" flow). Discovering them mid-run
70 // forces a re-optimize + full program reload; in workerd (apps/cloud's SSR70 // forces a re-optimize + full program reload; in workerd (apps/cloud's SSR
71 // worker) each reload stacks a new isolate's heap on top of the last one71 // worker) each reload stacks a new isolate's heap on top of the last one
72 // without freeing it, so a handful of reloads exhausts the worker's heap72 // without freeing it, so a handful of reloads exhausts the worker's heap
apps/docs/concepts/integrations.mdx+1−1
@@ -4,7 +4,7 @@ description: "An integration is something Executor connects to, defined by an MC · 3 unmodified lines
4---4---
5 5
6An **integration** is something Executor can connect to. Each integration is defined by6An **integration** is something Executor can connect to. Each integration is defined by
7a source that describes the available tools:7a spec that describes the available tools:
8 8
9- an **MCP server**9- an **MCP server**
10- an **OpenAPI** specification10- an **OpenAPI** specification
apps/docs/local/cli.mdx+3−3
@@ -74,12 +74,12 @@ the CLI: · 73 unmodified lines
74 74
75## Add an integration75## Add an integration
76 76
77From the web UI, click **Add Source** and paste an OpenAPI, GraphQL, or MCP URL.77From the web UI, click **Add Integration** and paste an OpenAPI, GraphQL, or MCP URL.
78Executor detects the type, indexes the tools, and handles auth. Or add one from the78Executor detects the type, indexes the tools, and handles auth. Or add one from the
79CLI:79CLI:
80 80
81```bash81```bash
82executor call executor openapi addSource '{82executor call executor openapi addIntegration '{
83 "spec": "https://petstore3.swagger.io/api/v3/openapi.json",83 "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
84 "namespace": "petstore",84 "namespace": "petstore",
85 "baseUrl": "https://petstore3.swagger.io/api/v3"85 "baseUrl": "https://petstore3.swagger.io/api/v3"
@@ -91,7 +91,7 @@ Use `baseUrl` when the OpenAPI document has relative `servers` entries (e.g. `"/ · 5 unmodified lines
91Confirm it's live:91Confirm it's live:
92 92
93```bash93```bash
94executor tools sources # lists configured integrations + tool counts94executor tools integrations # lists configured integrations + tool counts
95```95```
96 96
97## Call tools97## Call tools
apps/host-selfhost/src/integrations-mcp.test.tsrenamed from apps/host-selfhost/src/sources-mcp.test.ts+2−2
@@ -63,7 +63,7 @@ afterAll(() => dispose()); · 62 unmodified lines
63 63
64const BASE = "http://localhost:4788";64const BASE = "http://localhost:4788";
65 65
66const addOrgSource = async (organizationId: string): Promise<void> => {66const addOrgIntegration = async (organizationId: string): Promise<void> => {
67 // Register the integration and attach an org-owned connection, on its own67 // Register the integration and attach an org-owned connection, on its own
68 // connection to the shared DB file. WAL makes the committed rows visible to68 // connection to the shared DB file. WAL makes the committed rows visible to
69 // the server. Org-owned connections (and their per-connection tools) are69 // the server. Org-owned connections (and their per-connection tools) are
@@ -119,7 +119,7 @@ test("a user's MCP execute sandbox can reach an org-owned connection's tools", a · 49 unmodified lines
119 );119 );
120 const organizationId = ((await meRes.json()) as { organization: { id: string } }).organization.id;120 const organizationId = ((await meRes.json()) as { organization: { id: string } }).organization.id;
121 121
122 await addOrgSource(organizationId);122 await addOrgIntegration(organizationId);
123 123
124 const mcp = (body: unknown, sessionId?: string) =>124 const mcp = (body: unknown, sessionId?: string) =>
125 handler(125 handler(
apps/host-selfhost/src/integrations.test.tsrenamed from apps/host-selfhost/src/sources.test.ts+0−0

File renamed without changes.

apps/local/src/auth-tool-failures.test.ts+2−2
@@ -71,7 +71,7 @@ const PingGroup = HttpApiGroup.make("default", { topLevel: true }).add( · 70 unmodified lines
71 HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),71 HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),
72);72);
73 73
74const MissingAuthSourceApi = HttpApi.make("localAuthFailureSource").add(PingGroup);74const MissingAuthIntegrationApi = HttpApi.make("localAuthFailureIntegration").add(PingGroup);
75 75
76const TestApi = addGroup(OpenApiGroup);76const TestApi = addGroup(OpenApiGroup);
77type TestApiShape =77type TestApiShape =
@@ -230,7 +230,7 @@ describe("local auth tool failures", () => { · 152 unmodified lines
230 yield* run((client) =>230 yield* run((client) =>
231 client.openapi.addSpec({231 client.openapi.addSpec({
232 payload: {232 payload: {
233 ...makeOpenApiHttpApiTestAddSpecPayload(MissingAuthSourceApi, {233 ...makeOpenApiHttpApiTestAddSpecPayload(MissingAuthIntegrationApi, {
234 slug: integration,234 slug: integration,
235 authenticationTemplate: [235 authenticationTemplate: [
236 {236 {
apps/local/src/mcp-browser-resume.test.ts+1−1
@@ -44,7 +44,7 @@ const EmptyInputSchema = Schema.toStandardSchemaV1( · 43 unmodified lines
44const approvalPlugin = definePlugin(() => ({44const approvalPlugin = definePlugin(() => ({
45 id: "browser-resume-test" as const,45 id: "browser-resume-test" as const,
46 storage: () => ({}),46 storage: () => ({}),
47 staticSources: () => [47 staticIntegrations: () => [
48 {48 {
49 id: "api",49 id: "api",
50 kind: "in-memory",50 kind: "in-memory",
apps/local/src/serve.test.ts+1−1
@@ -76,7 +76,7 @@ describe("startServer static/SPA routing (unauthenticated)", () => { · 75 unmodified lines
76 const baseUrl = await startTestServer();76 const baseUrl = await startTestServer();
77 // No Authorization header — the shell must still load so the browser can77 // No Authorization header — the shell must still load so the browser can
78 // read its `?_token` and authenticate subsequent /api calls.78 // read its `?_token` and authenticate subsequent /api calls.
79 const response = await fetch(`${baseUrl}/sources/add`);79 const response = await fetch(`${baseUrl}/integrations/add`);
80 80
81 expect(response.status).toBe(200);81 expect(response.status).toBe(200);
82 expect(response.headers.get("content-type")).toContain("text/html");82 expect(response.headers.get("content-type")).toContain("text/html");
apps/marketing/src/pages/privacy.astro+2−2
@@ -39,7 +39,7 @@ import LegalLayout from "../components/LegalLayout.astro"; · 38 unmodified lines
39 <ul>39 <ul>
40 <li>organization and workspace names;</li>40 <li>organization and workspace names;</li>
41 <li>membership and role information;</li>41 <li>membership and role information;</li>
42 <li>source and integration metadata;</li>42 <li>integration metadata;</li>
43 <li>tool catalogs, schemas, policies, and related configuration;</li>43 <li>tool catalogs, schemas, policies, and related configuration;</li>
44 <li>secret metadata and connection settings; and</li>44 <li>secret metadata and connection settings; and</li>
45 <li>billing plan, membership, and execution-usage records.</li>45 <li>billing plan, membership, and execution-usage records.</li>
@@ -155,7 +155,7 @@ import LegalLayout from "../components/LegalLayout.astro"; · 109 unmodified lines
155 </p>155 </p>
156 156
157 <p>157 <p>
158 You can also manage certain information directly in the product, such as organizations, memberships, sources,158 You can also manage certain information directly in the product, such as organizations, memberships,
159 secrets, policies, and integrations. To exercise privacy rights or request account deletion, email159 secrets, policies, and integrations. To exercise privacy rights or request account deletion, email
160 <a href="mailto:rhys@executor.sh">rhys@executor.sh</a>.160 <a href="mailto:rhys@executor.sh">rhys@executor.sh</a>.
161 </p>161 </p>
design.md+6−6
@@ -351,9 +351,9 @@ Do not use a uniform icon pack (Tabler, Lucide-as-decoration, Heroicons) or · 350 unmodified lines
351monogram-in-rounded-square placeholders: both read as generic, generated UI. Identity comes from351monogram-in-rounded-square placeholders: both read as generic, generated UI. Identity comes from
352authentic specifics or from nothing at all.352authentic specifics or from nothing at all.
353 353
354- **App**: source and brand marks use the real favicon service,354- **App**: integration and brand marks use the real favicon service,
355 `https://www.google.com/s2/favicons?domain={host}&sz={n}` (`integration-favicon.tsx`, rendered355 `https://www.google.com/s2/favicons?domain={host}&sz={n}` (`integration-favicon.tsx`, rendered
356 2x for retina), with Executor's own `/favicon-32.png` for the built-in source. The only356 2x for retina), with Executor's own `/favicon-32.png` for the built-in integration. The only
357 lucide import there is `BoxIcon` as a neutral fallback when a favicon fails to load.357 lucide import there is `BoxIcon` as a neutral fallback when a favicon fails to load.
358- **Marketing**: brand marks are local SVGs in `apps/marketing/src/assets/logos` (`github.svg`,358- **Marketing**: brand marks are local SVGs in `apps/marketing/src/assets/logos` (`github.svg`,
359 `stripe.svg`, `linear.svg`, `claude.svg`, ...); the hub uses `/favicon-192.png`.359 `stripe.svg`, `linear.svg`, `claude.svg`, ...); the hub uses `/favicon-192.png`.
@@ -376,13 +376,13 @@ above.) · 16 unmodified lines
376Copy is part of the design; keep it precise, technical, and free of filler.376Copy is part of the design; keep it precise, technical, and free of filler.
377 377
378- Title Case for labels, buttons, titles, and tabs; sentence case for body, helper text, toasts.378- Title Case for labels, buttons, titles, and tabs; sentence case for body, helper text, toasts.
379- Name actions with a verb and a noun (`Add Source`, `Connect Agent`, `Revoke Token`), never379- Name actions with a verb and a noun (`Add Integration`, `Connect Agent`, `Revoke Token`), never
380 `Confirm`, `OK`, or a bare verb.380 `Confirm`, `OK`, or a bare verb.
381- Errors are what happened plus what to do next: `Couldn't reach the source. Check the server is381- Errors are what happened plus what to do next: `Couldn't reach the integration. Check the server is
382running, then retry.`382running, then retry.`
383- Toasts name the specific thing that changed, drop the trailing period, never say383- Toasts name the specific thing that changed, drop the trailing period, never say
384 `successfully`: `Source added`, not `Successfully added the source.`384 `successfully`: `Integration added`, not `Successfully added the integration.`
385- Empty states point to the first action: `No sources yet. Add one to start sharing tools across385- Empty states point to the first action: `No integrations yet. Add one to start sharing tools across
386your agents.`386your agents.`
387- Present participle with an ellipsis for in-progress states: `Connecting...`, `Syncing...`.387- Present participle with an ellipsis for in-progress states: `Connecting...`, `Syncing...`.
388- Use numerals (`3 tools`); skip `please` and marketing superlatives (powerful, seamless, robust,388- Use numerals (`3 tools`); skip `please` and marketing superlatives (powerful, seamless, robust,
e2e/cli/service-lifecycle.test.ts+2−2
@@ -76,7 +76,7 @@ scenario( · 75 unmodified lines
76 76
77 const listed = yield* chat.tool(77 const listed = yield* chat.tool(
78 {78 {
79 name: "executor tools sources",79 name: "executor tools integrations",
80 result: (rows) =>80 result: (rows) =>
81 rows.map((r) => String(r.slug)).includes(slug) ? `${slug} is listed` : "NOT listed",81 rows.map((r) => String(r.slug)).includes(slug) ? `${slug} is listed` : "NOT listed",
82 },82 },
@@ -103,7 +103,7 @@ scenario( · 20 unmodified lines
103 Effect.gen(function* () {103 Effect.gen(function* () {
104 const survived = yield* chat.tool(104 const survived = yield* chat.tool(
105 {105 {
106 name: "executor tools sources",106 name: "executor tools integrations",
107 result: (rows) =>107 result: (rows) =>
108 rows.map((r) => String(r.slug)).includes(slug)108 rows.map((r) => String(r.slug)).includes(slug)
109 ? `${slug} SURVIVED the reboot`109 ? `${slug} SURVIVED the reboot`
e2e/cloud/auth-routing-flow.test.ts+1−1
@@ -41,7 +41,7 @@ scenario( · 40 unmodified lines
41 yield* browser.session(anonymous, async ({ page, step }) => {41 yield* browser.session(anonymous, async ({ page, step }) => {
42 await step("Signed out, the root redirects to /login", async () => {42 await step("Signed out, the root redirects to /login", async () => {
43 await page.goto("/", { waitUntil: "commit" });43 await page.goto("/", { waitUntil: "commit" });
44 await page.getByText("Sign in to manage your tools and sources").waitFor();44 await page.getByText("Sign in to manage your tools and integrations").waitFor();
45 });45 });
46 expect(new URL(page.url()).pathname, "the signed-out root lands on /login").toBe("/login");46 expect(new URL(page.url()).pathname, "the signed-out root lands on /login").toBe("/login");
47 47
e2e/cloud/cli-device-login.test.ts+3−3
@@ -6,7 +6,7 @@ · 5 unmodified lines
6// URL, confirms the code, and clicks "Authorize device" on the WorkOS6// URL, confirms the code, and clicks "Authorize device" on the WorkOS
7// emulator's verification page, exactly the human hop, the way the MCP7// emulator's verification page, exactly the human hop, the way the MCP
8// approval scenarios drive their browser step. The terminal then runs `whoami`8// approval scenarios drive their browser step. The terminal then runs `whoami`
9// and `tools sources`; a clean exit of that chain proves the resulting WorkOS9// and `tools integrations`; a clean exit of that chain proves the resulting WorkOS
10// access token (a JWT) is accepted by the protected `/api/*` plane.10// access token (a JWT) is accepted by the protected `/api/*` plane.
11import { spawn } from "node:child_process";11import { spawn } from "node:child_process";
12import { readFileSync } from "node:fs";12import { readFileSync } from "node:fs";
@@ -83,12 +83,12 @@ scenario( · 70 unmodified lines
83 83
84 // The terminal journey, recorded to terminal.cast. `&&` means a clean84 // The terminal journey, recorded to terminal.cast. `&&` means a clean
85 // exit only happens if every step, including the authenticated /api call85 // exit only happens if every step, including the authenticated /api call
86 // (`tools sources`), succeeded.86 // (`tools integrations`), succeeded.
87 const cli_ = `bun run ${CLI_ENTRY}`;87 const cli_ = `bun run ${CLI_ENTRY}`;
88 const journey =88 const journey =
89 `${cli_} login --base-url ${CLOUD_BASE_URL} --no-browser --name cloud && ` +89 `${cli_} login --base-url ${CLOUD_BASE_URL} --no-browser --name cloud && ` +
90 `${cli_} whoami --server cloud && ` +90 `${cli_} whoami --server cloud && ` +
91 `${cli_} tools sources --server cloud`;91 `${cli_} tools integrations --server cloud`;
92 92
93 const terminal = cli.session(93 const terminal = cli.session(
94 ["bash", "-c", journey],94 ["bash", "-c", journey],
e2e/cloud/integrations-api.test.tsrenamed from e2e/cloud/sources-api.test.ts+0−0

File renamed without changes.

e2e/cloud/integrations-refresh.test.tsrenamed from e2e/cloud/sources-refresh.test.ts+0−0

File renamed without changes.

e2e/cloud/oauth-callback-unauthenticated.test.ts+1−1
@@ -117,7 +117,7 @@ scenario( · 116 unmodified lines
117 await step("Provider sends a signed-out browser to the OAuth callback", async () => {117 await step("Provider sends a signed-out browser to the OAuth callback", async () => {
118 const response = await page.goto(callbackPath, { waitUntil: "commit" });118 const response = await page.goto(callbackPath, { waitUntil: "commit" });
119 expect(response?.status(), "the callback redirects into the login flow").toBe(200);119 expect(response?.status(), "the callback redirects into the login flow").toBe(200);
120 await page.getByText("Sign in to manage your tools and sources").waitFor();120 await page.getByText("Sign in to manage your tools and integrations").waitFor();
121 });121 });
122 122
123 const loginUrl = new URL(page.url());123 const loginUrl = new URL(page.url());
e2e/cloud/toolkit-opencode-real.test.ts+3−3
@@ -1,5 +1,5 @@
1// The toolkit-connect performance bug, reproduced with the REAL opencode1// The toolkit-connect performance bug, reproduced with the REAL opencode
2// binary in a REAL terminal. A user adds a dozen sources to their workspace,2// binary in a REAL terminal. A user adds a dozen integrations to their workspace,
3// makes a toolkit, and points OpenCode at /mcp/toolkits/<slug>. OpenCode runs3// makes a toolkit, and points OpenCode at /mcp/toolkits/<slug>. OpenCode runs
4// its own OAuth (discovery, DCR, PKCE) and then `mcp list`. The whole session4// its own OAuth (discovery, DCR, PKCE) and then `mcp list`. The whole session
5// runs in one recorded PTY: the run's terminal.cast replays exactly what a5// runs in one recorded PTY: the run's terminal.cast replays exactly what a
@@ -37,8 +37,8 @@ scenario( · 31 unmodified lines
37 const email = identity.credentials?.email ?? identity.label;37 const email = identity.credentials?.email ?? identity.label;
38 const client = yield* makeClient(catalogApi, identity);38 const client = yield* makeClient(catalogApi, identity);
39 39
40 // Build a production-shaped workspace: one real spec + ten more sources,40 // Build a production-shaped workspace: one real spec + ten more integrations,
41 // a toolkit scoped to one of them. ~3,300 tools across 11 sources — large41 // a toolkit scoped to one of them. ~3,300 tools across 11 integrations — large
42 // enough that the pre-fix per-tool policy N+1 pushes the toolkit connect42 // enough that the pre-fix per-tool policy N+1 pushes the toolkit connect
43 // past OpenCode's client timeout.43 // past OpenCode's client timeout.
44 const seeded = yield* seedLargeCatalog(client);44 const seeded = yield* seedLargeCatalog(client);
e2e/cloud/toolkit-policy-perf.test.ts+8−8
@@ -7,12 +7,12 @@ · 6 unmodified lines
7// the policy engine over the WHOLE catalog to decide tool visibility. A7// the policy engine over the WHOLE catalog to decide tool visibility. A
8// per-tool policy resolution there is an N+1 that scales with total catalog8// per-tool policy resolution there is an N+1 that scales with total catalog
9// size, not toolkit size: a workspace with thousands of tools across a dozen9// size, not toolkit size: a workspace with thousands of tools across a dozen
10// sources pushes the connect past the MCP client's connect timeout, and the10// integrations pushes the connect past the MCP client's connect timeout, and the
11// toolkit appears permanently "failed" even though nothing is broken.11// toolkit appears permanently "failed" even though nothing is broken.
12//12//
13// This scenario seeds a production-shaped catalog (one real OpenAPI spec plus13// This scenario seeds a production-shaped catalog (one real OpenAPI spec plus
14// enough synthetic sources to look like a working workspace, ~3,300 tools over14// enough synthetic integrations to look like a working workspace, ~3,300 tools over
15// 11 sources) and asserts the catalog size adds only a small, bounded cost to15// 11 integrations) and asserts the catalog size adds only a small, bounded cost to
16// connect. The control is a fresh identity with a near-empty catalog: it pays16// connect. The control is a fresh identity with a near-empty catalog: it pays
17// the same OAuth + MCP handshake, so the DIFFERENCE isolates the catalog-walk17// the same OAuth + MCP handshake, so the DIFFERENCE isolates the catalog-walk
18// cost the fix removes. Before the fix this delta is tens of seconds (and in18// cost the fix removes. Before the fix this delta is tens of seconds (and in
@@ -141,14 +141,14 @@ scenario( · 122 unmodified lines
141 let seededCatalog: SeededCatalog | undefined;141 let seededCatalog: SeededCatalog | undefined;
142 142
143 yield* Effect.gen(function* () {143 yield* Effect.gen(function* () {
144 // 1 real source (Vercel, 322 ops) + 10 synthetic = 11 sources, ~2,200144 // 1 real integration (Vercel, 322 ops) + 10 synthetic = 11 integrations, ~2,200
145 // tools. Sized so the pre-fix N+1 connect (~27s here) still completes145 // tools. Sized so the pre-fix N+1 connect (~27s here) still completes
146 // under the MCP client's connect timeout, so the regression surfaces as146 // under the MCP client's connect timeout, so the regression surfaces as
147 // a failed ASSERTION on the overhead rather than a connect crash.147 // a failed ASSERTION on the overhead rather than a connect crash.
148 const seeded = yield* seedLargeCatalog(bigClient, {148 const seeded = yield* seedLargeCatalog(bigClient, {
149 includeRealSpec: true,149 includeRealSpec: true,
150 syntheticSources: 10,150 syntheticIntegrations: 10,
151 opsPerSource: 190,151 opsPerIntegration: 190,
152 });152 });
153 seededCatalog = seeded;153 seededCatalog = seeded;
154 expect(154 expect(
@@ -157,7 +157,7 @@ scenario( · 2 unmodified lines
157 ).toBeGreaterThan(2_000);157 ).toBeGreaterThan(2_000);
158 expect(158 expect(
159 seeded.integrationSlugs.length,159 seeded.integrationSlugs.length,
160 "the catalog spans a production-like number of sources",160 "the catalog spans a production-like number of integrations",
161 ).toBeGreaterThanOrEqual(11);161 ).toBeGreaterThanOrEqual(11);
162 162
163 const big = yield* timeToolkitConnect(163 const big = yield* timeToolkitConnect(
@@ -172,7 +172,7 @@ scenario( · 8 unmodified lines
172 const overhead = big.elapsedMs - control.elapsedMs;172 const overhead = big.elapsedMs - control.elapsedMs;
173 expect(173 expect(
174 overhead,174 overhead,
175 `catalog of ${seeded.toolCount} tools across ${seeded.integrationSlugs.length} sources ` +175 `catalog of ${seeded.toolCount} tools across ${seeded.integrationSlugs.length} integrations ` +
176 `added ${overhead}ms to connect (big ${big.elapsedMs}ms vs control ${control.elapsedMs}ms); ` +176 `added ${overhead}ms to connect (big ${big.elapsedMs}ms vs control ${control.elapsedMs}ms); ` +
177 `the per-tool policy N+1 would make this scale into tens of seconds`,177 `the per-tool policy N+1 would make this scale into tens of seconds`,
178 ).toBeLessThan(MAX_CATALOG_CONNECT_OVERHEAD_MS);178 ).toBeLessThan(MAX_CATALOG_CONNECT_OVERHEAD_MS);
e2e/cloud/unauthenticated-skeleton.test.ts+4−2
@@ -33,7 +33,7 @@ scenario( · 32 unmodified lines
33 33
34 await step("Open the cloud root while signed out → redirected to /login", async () => {34 await step("Open the cloud root while signed out → redirected to /login", async () => {
35 await page.goto("/", { waitUntil: "commit" });35 await page.goto("/", { waitUntil: "commit" });
36 await page.getByText("Sign in to manage your tools and sources").waitFor();36 await page.getByText("Sign in to manage your tools and integrations").waitFor();
37 });37 });
38 38
39 // Snapshot DURING the /account/me window — where the skeleton used to be.39 // Snapshot DURING the /account/me window — where the skeleton used to be.
@@ -41,7 +41,9 @@ scenario( · 1 unmodified line
41 url: new URL(page.url()).pathname,41 url: new URL(page.url()).pathname,
42 appShellSkeletons: await page.locator('[data-slot="skeleton"]').count(),42 appShellSkeletons: await page.locator('[data-slot="skeleton"]').count(),
43 sidebarShown: await page.locator("aside").first().isVisible(),43 sidebarShown: await page.locator("aside").first().isVisible(),
44 loginShown: await page.getByText("Sign in to manage your tools and sources").isVisible(),44 loginShown: await page
45 .getByText("Sign in to manage your tools and integrations")
46 .isVisible(),
45 };47 };
46 48
47 expect(49 expect(
e2e/local/auth.test.ts+1−1
@@ -35,7 +35,7 @@ scenario( · 34 unmodified lines
35 await step("Open the ?_token URL printed by executor web --foreground", async () => {35 await step("Open the ?_token URL printed by executor web --foreground", async () => {
36 await page.goto(url, { waitUntil: "domcontentloaded" });36 await page.goto(url, { waitUntil: "domcontentloaded" });
37 await page.getByRole("link", { name: "Secrets" }).first().waitFor({ timeout: 30_000 });37 await page.getByRole("link", { name: "Secrets" }).first().waitFor({ timeout: 30_000 });
38 // Integrations actually LOAD (the built-in Executor source) — proves38 // Integrations actually LOAD (the built-in Executor integration) — proves
39 // auth + data, not just the static shell.39 // auth + data, not just the static shell.
40 await page.getByText("built-in").first().waitFor({ timeout: 30_000 });40 await page.getByText("built-in").first().waitFor({ timeout: 30_000 });
41 // The token is moved out of the URL and persisted to localStorage.41 // The token is moved out of the URL and persisted to localStorage.
e2e/local/update-notice.test.ts+1−1
@@ -87,7 +87,7 @@ scenario( · 86 unmodified lines
87 await step("Open the console via the CLI's ?_token URL", async () => {87 await step("Open the console via the CLI's ?_token URL", async () => {
88 await page.goto(url, { waitUntil: "domcontentloaded" });88 await page.goto(url, { waitUntil: "domcontentloaded" });
89 // The console renders past the auth gate: the Integrations page and89 // The console renders past the auth gate: the Integrations page and
90 // its built-in Executor source row load (proves auth + data, not90 // its built-in Executor integration row load (proves auth + data, not
91 // just the static shell).91 // just the static shell).
92 await page.getByRole("heading", { name: "Integrations" }).waitFor({ timeout: 60_000 });92 await page.getByRole("heading", { name: "Integrations" }).waitFor({ timeout: 60_000 });
93 await page.getByText("Tool providers available").waitFor({ timeout: 30_000 });93 await page.getByText("Tool providers available").waitFor({ timeout: 30_000 });
e2e/scenarios/auth-methods.test.ts+4−4
@@ -4,7 +4,7 @@ · 3 unmodified lines
4// each rendered from its own credential input; the merge-append configureAuth4// each rendered from its own credential input; the merge-append configureAuth
5// flow (a custom API key must never displace a detected OAuth method);5// flow (a custom API key must never displace a detected OAuth method);
6// declaring a method on a server that advertises none; GraphQL's multi-method6// declaring a method on a server that advertises none; GraphQL's multi-method
7// add — and the EXTREME case end-to-end: a source declaring7// add — and the EXTREME case end-to-end: an integration declaring
8// [oauth] [bearer header + team-id query] [bearer header] [query token],8// [oauth] [bearer header + team-id query] [bearer header] [query token],
9// one connection per credential method, every invocation asserted against9// one connection per credential method, every invocation asserted against
10// what a real recording MCP server received.10// what a real recording MCP server received.
@@ -192,7 +192,7 @@ scenario( · 181 unmodified lines
192);192);
193 193
194scenario(194scenario(
195 "Auth methods · a GraphQL source registers multiple auth methods at add time",195 "Auth methods · a GraphQL integration registers multiple auth methods at add time",
196 {},196 {},
197 Effect.gen(function* () {197 Effect.gen(function* () {
198 const target = yield* Target;198 const target = yield* Target;
@@ -239,7 +239,7 @@ scenario( · 40 unmodified lines
239);239);
240 240
241// ---------------------------------------------------------------------------241// ---------------------------------------------------------------------------
242// THE EXTREME CASE — one source, four methods, three live connections, every242// THE EXTREME CASE — one integration, four methods, three live connections, every
243// invocation asserted against what the server actually received:243// invocation asserted against what the server actually received:
244// [oauth2] (declared; connect flow not exercised here)244// [oauth2] (declared; connect flow not exercised here)
245// [bearer header + team-id query] (TWO inputs on ONE method)245// [bearer header + team-id query] (TWO inputs on ONE method)
@@ -248,7 +248,7 @@ scenario( · 2 unmodified lines
248// ---------------------------------------------------------------------------248// ---------------------------------------------------------------------------
249 249
250scenario(250scenario(
251 "Auth methods · one source mixes oauth, a 2-input header+query method, a bearer header, and a query token",251 "Auth methods · one integration mixes oauth, a 2-input header+query method, a bearer header, and a query token",
252 {},252 {},
253 Effect.scoped(253 Effect.scoped(
254 Effect.gen(function* () {254 Effect.gen(function* () {
e2e/scenarios/microsoft-emulator.test.ts+1−1
@@ -133,7 +133,7 @@ scenario( · 132 unmodified lines
133 );133 );
134 expect(134 expect(
135 appOnlyTemplate?.kind,135 appOnlyTemplate?.kind,
136 "the Microsoft source exposes an app-only OAuth method",136 "the Microsoft integration exposes an app-only OAuth method",
137 ).toBe("oauth2");137 ).toBe("oauth2");
138 expect(138 expect(
139 appOnlyTemplate?.kind === "oauth2" ? appOnlyTemplate.scopes : [],139 appOnlyTemplate?.kind === "oauth2" ? appOnlyTemplate.scopes : [],
e2e/scenarios/microsoft-graph-default.test.ts+3−1
@@ -67,7 +67,9 @@ scenario( · 66 unmodified lines
67 specFormat: "microsoft-graph",67 specFormat: "microsoft-graph",
68 },68 },
69 });69 });
70 expect(added.slug, "the Microsoft Graph source keeps the requested slug").toBe(integration);70 expect(added.slug, "the Microsoft Graph integration keeps the requested slug").toBe(
71 integration,
72 );
71 expect(73 expect(
72 added.toolCount,74 added.toolCount,
73 "the Microsoft files catalog preset extracts a focused operation subtree",75 "the Microsoft files catalog preset extracts a focused operation subtree",
e2e/scenarios/microsoft-graph-full.test.ts+3−1
@@ -65,7 +65,9 @@ scenario( · 64 unmodified lines
65 specFormat: "microsoft-graph",65 specFormat: "microsoft-graph",
66 },66 },
67 });67 });
68 expect(added.slug, "the Microsoft files source keeps the requested slug").toBe(integration);68 expect(added.slug, "the Microsoft files integration keeps the requested slug").toBe(
69 integration,
70 );
69 expect(71 expect(
70 added.toolCount,72 added.toolCount,
71 "adding the files catalog service extracts a focused Graph operation subtree",73 "adding the files catalog service extracts a focused Graph operation subtree",
e2e/scenarios/openapi-add-integration-action-bar.test.ts+4−4
@@ -1,4 +1,4 @@
1// Cross-target (browser): the Add OpenAPI source form's floating action bar.1// Cross-target (browser): the Add OpenAPI integration form's floating action bar.
2// Rohil (shared-tcc) reported the "Add integration" button rendering doubled /2// Rohil (shared-tcc) reported the "Add integration" button rendering doubled /
3// ghosted on click. This pins the flow: paste a spec, click Add integration,3// ghosted on click. This pins the flow: paste a spec, click Add integration,
4// land on the created integration. The trace's DOM snapshots are the artifact4// land on the created integration. The trace's DOM snapshots are the artifact
@@ -22,7 +22,7 @@ const singleServerSpec = JSON.stringify({ · 17 unmodified lines
22});22});
23 23
24scenario(24scenario(
25 "OpenAPI · Add integration commits the source and the action bar is a single node",25 "OpenAPI · Add integration commits the integration and the action bar is a single node",
26 {},26 {},
27 Effect.scoped(27 Effect.scoped(
28 Effect.gen(function* () {28 Effect.gen(function* () {
@@ -31,7 +31,7 @@ scenario( · 2 unmodified lines
31 const identity = yield* target.newIdentity();31 const identity = yield* target.newIdentity();
32 32
33 yield* browser.session(identity, async ({ page, step }) => {33 yield* browser.session(identity, async ({ page, step }) => {
34 await step("Open the Add OpenAPI source form", async () => {34 await step("Open the Add OpenAPI integration form", async () => {
35 await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });35 await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });
36 await page.getByPlaceholder("https://api.example.com/openapi.json").waitFor();36 await page.getByPlaceholder("https://api.example.com/openapi.json").waitFor();
37 });37 });
@@ -52,7 +52,7 @@ scenario( · 14 unmodified lines
52 });52 });
53 53
54 await step(54 await step(
55 "Submitting commits the source and lands on the created integration",55 "Submitting commits the integration and lands on the created integration",
56 async () => {56 async () => {
57 // The reported ghost was the bar painting doubled when the submit57 // The reported ghost was the bar painting doubled when the submit
58 // button changed width on click. The single-node counts (above and58 // button changed width on click. The single-node counts (above and
e2e/scenarios/openapi-integration.test.tsrenamed from e2e/scenarios/openapi-source.test.ts+0−0

File renamed without changes.

e2e/scenarios/openapi-server-selection-ui.test.ts+2−2
@@ -1,5 +1,5 @@
1// Cross-target (browser): the UI side of per-call server selection. When a1// Cross-target (browser): the UI side of per-call server selection. When a
2// pasted spec declares more than one server, the Add OpenAPI source form turns2// pasted spec declares more than one server, the Add OpenAPI integration form turns
3// the Base URL field into a picker over those servers and relabels it an3// the Base URL field into a picker over those servers and relabels it an
4// optional override — the host is otherwise resolved per tool call. The session4// optional override — the host is otherwise resolved per tool call. The session
5// video + per-step screenshots are the artifact; this scenario skips on targets5// video + per-step screenshots are the artifact; this scenario skips on targets
@@ -34,7 +34,7 @@ scenario( · 28 unmodified lines
34 const identity = yield* target.newIdentity();34 const identity = yield* target.newIdentity();
35 35
36 yield* browser.session(identity, async ({ page, step }) => {36 yield* browser.session(identity, async ({ page, step }) => {
37 await step("Open the Add OpenAPI source form", async () => {37 await step("Open the Add OpenAPI integration form", async () => {
38 await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });38 await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });
39 await page.getByPlaceholder("https://api.example.com/openapi.json").waitFor();39 await page.getByPlaceholder("https://api.example.com/openapi.json").waitFor();
40 });40 });
e2e/scenarios/support/large-catalog.ts+29−29
@@ -5,7 +5,7 @@ · 4 unmodified lines
5// by running the policy engine over the WHOLE catalog on connect (the5// by running the policy engine over the WHOLE catalog on connect (the
6// getDescription -> connections.list -> toolsList path), not just the toolkit's6// getDescription -> connections.list -> toolsList path), not just the toolkit's
7// own connections. A per-tool resolution there is an N+1 that scales with total7// own connections. A per-tool resolution there is an N+1 that scales with total
8// catalog size, so a realistic workspace (a real spec plus enough sources to8// catalog size, so a realistic workspace (a real spec plus enough integrations to
9// look like one) is what surfaces the regression.9// look like one) is what surfaces the regression.
10//10//
11// `catalogApi` is exported so scenarios build their client from the SAME11// `catalogApi` is exported so scenarios build their client from the SAME
@@ -65,7 +65,7 @@ export const syntheticSpec = (title: string, ops: number, baseUrl: string): stri · 53 unmodified lines
65 });65 });
66};66};
67 67
68export interface SeedSource {68export interface SeedIntegration {
69 readonly slug: string;69 readonly slug: string;
70 readonly specText: string;70 readonly specText: string;
71 readonly baseUrl: string;71 readonly baseUrl: string;
@@ -73,53 +73,53 @@ export interface SeedSource { · 1 unmodified line
73}73}
74 74
75export interface SeedPlan {75export interface SeedPlan {
76 readonly sources: ReadonlyArray<SeedSource>;76 readonly integrations: ReadonlyArray<SeedIntegration>;
77 /** The first source's slug — the one the toolkit is scoped to. */77 /** The first integration's slug — the one the toolkit is scoped to. */
78 readonly firstSlug: string;78 readonly firstSlug: string;
79 /** Connection pattern the toolkit binds (the first source, org, conn0). */79 /** Connection pattern the toolkit binds (the first integration, org, conn0). */
80 readonly toolkitConnectionPattern: string;80 readonly toolkitConnectionPattern: string;
81}81}
82 82
83export interface SeedOptions {83export interface SeedOptions {
84 /** Synthetic sources on top of the real spec. Default 10. */84 /** Synthetic integrations on top of the real spec. Default 10. */
85 readonly syntheticSources?: number;85 readonly syntheticIntegrations?: number;
86 /** Operations per synthetic source. Default 300. */86 /** Operations per synthetic integration. Default 300. */
87 readonly opsPerSource?: number;87 readonly opsPerIntegration?: number;
88 /** Include the real Vercel fixture (322 ops) as one source. Default true. */88 /** Include the real Vercel fixture (322 ops) as one integration. Default true. */
89 readonly includeRealSpec?: boolean;89 readonly includeRealSpec?: boolean;
90}90}
91 91
92/**92/**
93 * Plan a large, production-shaped catalog. With the defaults (1 real + 1093 * Plan a large, production-shaped catalog. With the defaults (1 real + 10
94 * synthetic sources, 300 ops each) it is ~3,300 tools across 11 sources, where94 * synthetic integrations, 300 ops each) it is ~3,300 tools across 11 integrations, where
95 * the per-tool policy N+1 turns a toolkit connect from sub-second into a 30s+95 * the per-tool policy N+1 turns a toolkit connect from sub-second into a 30s+
96 * client timeout. Each source gets exactly one org connection at `conn0`.96 * client timeout. Each integration gets exactly one org connection at `conn0`.
97 */97 */
98export const planLargeCatalog = (options: SeedOptions = {}): SeedPlan => {98export const planLargeCatalog = (options: SeedOptions = {}): SeedPlan => {
99 const syntheticSources = options.syntheticSources ?? 10;99 const syntheticIntegrations = options.syntheticIntegrations ?? 10;
100 const opsPerSource = options.opsPerSource ?? 300;100 const opsPerIntegration = options.opsPerIntegration ?? 300;
101 const includeRealSpec = options.includeRealSpec ?? true;101 const includeRealSpec = options.includeRealSpec ?? true;
102 102
103 const sources: SeedSource[] = [];103 const integrations: SeedIntegration[] = [];
104 if (includeRealSpec) {104 if (includeRealSpec) {
105 sources.push({105 integrations.push({
106 slug: unique("vercel"),106 slug: unique("vercel"),
107 specText: vercelSpecText(),107 specText: vercelSpecText(),
108 baseUrl: "https://api.vercel.com",108 baseUrl: "https://api.vercel.com",
109 description: "Vercel API",109 description: "Vercel API",
110 });110 });
111 }111 }
112 for (let s = 0; s < syntheticSources; s++) {112 for (let s = 0; s < syntheticIntegrations; s++) {
113 sources.push({113 integrations.push({
114 slug: unique("svc"),114 slug: unique("svc"),
115 specText: syntheticSpec(`Service ${s}`, opsPerSource, "https://service.example"),115 specText: syntheticSpec(`Service ${s}`, opsPerIntegration, "https://service.example"),
116 baseUrl: "https://service.example",116 baseUrl: "https://service.example",
117 });117 });
118 }118 }
119 119
120 const firstSlug = sources[0]!.slug;120 const firstSlug = integrations[0]!.slug;
121 return {121 return {
122 sources,122 integrations,
123 firstSlug,123 firstSlug,
124 toolkitConnectionPattern: `${firstSlug}.org.conn0.*`,124 toolkitConnectionPattern: `${firstSlug}.org.conn0.*`,
125 };125 };
@@ -151,7 +151,7 @@ export interface SeededCatalog { · 25 unmodified lines
151 151
152/**152/**
153 * Build the planned catalog under the current identity (one org connection per153 * Build the planned catalog under the current identity (one org connection per
154 * source) and a toolkit scoped to the first source, then report the total tool154 * integration) and a toolkit scoped to the first integration, then report the total tool
155 * count and a finalizer. The toolkit's own surface is small; the connect cost155 * count and a finalizer. The toolkit's own surface is small; the connect cost
156 * the scenarios guard comes from the policy engine walking the whole catalog.156 * the scenarios guard comes from the policy engine walking the whole catalog.
157 */157 */
@@ -162,13 +162,13 @@ export const seedLargeCatalog = ( · 4 unmodified lines
162 Effect.gen(function* () {162 Effect.gen(function* () {
163 const plan = planLargeCatalog(options);163 const plan = planLargeCatalog(options);
164 164
165 for (const source of plan.sources) {165 for (const integration of plan.integrations) {
166 yield* client.openapi.addSpec({166 yield* client.openapi.addSpec({
167 payload: {167 payload: {
168 spec: { kind: "blob", value: source.specText },168 spec: { kind: "blob", value: integration.specText },
169 slug: IntegrationSlug.make(source.slug),169 slug: IntegrationSlug.make(integration.slug),
170 baseUrl: source.baseUrl,170 baseUrl: integration.baseUrl,
171 ...(source.description ? { description: source.description } : {}),171 ...(integration.description ? { description: integration.description } : {}),
172 authenticationTemplate: [172 authenticationTemplate: [
173 {173 {
174 slug: "apiKey",174 slug: "apiKey",
@@ -182,7 +182,7 @@ export const seedLargeCatalog = ( · 7 unmodified lines
182 payload: {182 payload: {
183 owner: "org",183 owner: "org",
184 name: ConnectionName.make("conn0"),184 name: ConnectionName.make("conn0"),
185 integration: IntegrationSlug.make(source.slug),185 integration: IntegrationSlug.make(integration.slug),
186 template: AuthTemplateSlug.make("apiKey"),186 template: AuthTemplateSlug.make("apiKey"),
187 value: "unused-token",187 value: "unused-token",
188 },188 },
@@ -200,7 +200,7 @@ export const seedLargeCatalog = ( · 11 unmodified lines
200 payload: { pattern: plan.toolkitConnectionPattern },200 payload: { pattern: plan.toolkitConnectionPattern },
201 });201 });
202 202
203 const integrationSlugs = plan.sources.map((source) => source.slug);203 const integrationSlugs = plan.integrations.map((integration) => integration.slug);
204 const cleanup = Effect.gen(function* () {204 const cleanup = Effect.gen(function* () {
205 yield* client.toolkits.remove({ params: { toolkitId: toolkit.id } }).pipe(Effect.ignore);205 yield* client.toolkits.remove({ params: { toolkitId: toolkit.id } }).pipe(Effect.ignore);
206 yield* Effect.forEach(206 yield* Effect.forEach(
e2e/scenarios/tool-descriptions.test.ts+2−2
@@ -426,7 +426,7 @@ scenario( · 425 unmodified lines
426 "What an agent sees for tools derived from description-rich fixtures:",426 "What an agent sees for tools derived from description-rich fixtures:",
427 "the `tools.list` catalog entry (quoted) and the `tools.schema` view —",427 "the `tools.list` catalog entry (quoted) and the `tools.schema` view —",
428 "the same compiled TypeScript previews `tools.describe.tool()` returns",428 "the same compiled TypeScript previews `tools.describe.tool()` returns",
429 "inside the sandbox. Anything written in the fixture sources (bottom)",429 "inside the sandbox. Anything written in the fixture integrations (bottom)",
430 "but absent here was dropped by the spec→tool pipeline.",430 "but absent here was dropped by the spec→tool pipeline.",
431 "",431 "",
432 ...sections.flatMap(([title, tools]) => [432 ...sections.flatMap(([title, tools]) => [
@@ -442,7 +442,7 @@ scenario( · 9 unmodified lines
442 "",442 "",
443 codeBlock("md", inventory ?? "(no inventory section found)"),443 codeBlock("md", inventory ?? "(no inventory section found)"),
444 "",444 "",
445 "## Fixture sources",445 "## Fixture integrations",
446 "",446 "",
447 "<details><summary>OpenAPI spec</summary>",447 "<details><summary>OpenAPI spec</summary>",
448 "",448 "",
e2e/selfhost/cli-device-login.test.ts+3−3
@@ -4,7 +4,7 @@ · 3 unmodified lines
4// discovers Better Auth's device endpoints via /api/auth/cli-login, prints the4// discovers Better Auth's device endpoints via /api/auth/cli-login, prints the
5// verification URL, and polls. The browser leg is REAL (session.mp4): Playwright5// verification URL, and polls. The browser leg is REAL (session.mp4): Playwright
6// opens the self-host /device page (signed in via the session cookie) and clicks6// opens the self-host /device page (signed in via the session cookie) and clicks
7// "Authorize device". The terminal then runs `whoami` and `tools sources`; a7// "Authorize device". The terminal then runs `whoami` and `tools integrations`; a
8// clean exit of that chain proves the Better Auth device token is accepted as a8// clean exit of that chain proves the Better Auth device token is accepted as a
9// Bearer on the protected /api/* plane.9// Bearer on the protected /api/* plane.
10import { readFileSync } from "node:fs";10import { readFileSync } from "node:fs";
@@ -64,7 +64,7 @@ scenario( · 53 unmodified lines
64 const journey =64 const journey =
65 `${cli_} login --base-url ${SELFHOST_BASE_URL} --no-browser --name selfhost && ` +65 `${cli_} login --base-url ${SELFHOST_BASE_URL} --no-browser --name selfhost && ` +
66 `${cli_} whoami --server selfhost && ` +66 `${cli_} whoami --server selfhost && ` +
67 `${cli_} tools sources --server selfhost`;67 `${cli_} tools integrations --server selfhost`;
68 68
69 const terminal = cli.session(69 const terminal = cli.session(
70 ["bash", "-c", journey],70 ["bash", "-c", journey],
@@ -113,7 +113,7 @@ scenario( · 42 unmodified lines
113 });113 });
114 114
115 // Reaching here means the whole `&&` chain exited 0, including the115 // Reaching here means the whole `&&` chain exited 0, including the
116 // authenticated `tools sources` /api call.116 // authenticated `tools integrations` /api call.
117 yield* Effect.all([terminal, browserApproval], { concurrency: "unbounded" });117 yield* Effect.all([terminal, browserApproval], { concurrency: "unbounded" });
118 118
119 // The stored profile carries an oauth device-login credential, not a key.119 // The stored profile carries an oauth device-login credential, not a key.
packages/app/src/web/shell.tsx+5−5
@@ -138,7 +138,7 @@ function IntegrationList(props: { pathname: string; onNavigate?: () => void }) { · 137 unmodified lines
138 { id: slug, kind: integration.kind },138 { id: slug, kind: integration.kind },
139 integrationPlugins,139 integrationPlugins,
140 )}140 )}
141 sourceId={slug}141 integrationId={slug}
142 size="sm"142 size="sm"
143 />143 />
144 <span className="flex-1 truncate">{name}</span>144 <span className="flex-1 truncate">{name}</span>
@@ -204,7 +204,7 @@ function SidebarContent(props: { · 59 unmodified lines
204 204
205 <PluginNav pathname={props.pathname} onNavigate={props.onNavigate} />205 <PluginNav pathname={props.pathname} onNavigate={props.onNavigate} />
206 206
207 {/* Sources list */}207 {/* Integrations list */}
208 <Link208 <Link
209 to="/{-$orgSlug}"209 to="/{-$orgSlug}"
210 className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"210 className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"
@@ -268,7 +268,7 @@ function SidebarContent(props: { · 57 unmodified lines
268export function Shell() {268export function Shell() {
269 const location = useLocation();269 const location = useLocation();
270 const pathname = location.pathname;270 const pathname = location.pathname;
271 const refreshSources = useAtomRefresh(integrationsAtom);271 const refreshIntegrations = useAtomRefresh(integrationsAtom);
272 const refreshTools = useAtomRefresh(toolsAllAtom);272 const refreshTools = useAtomRefresh(toolsAllAtom);
273 const lastPathname = useRef(pathname);273 const lastPathname = useRef(pathname);
274 const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);274 const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
@@ -294,7 +294,7 @@ export function Shell() { · 19 unmodified lines
294 }294 }
295 295
296 const refreshBackendData = () => {296 const refreshBackendData = () => {
297 refreshSources();297 refreshIntegrations();
298 refreshTools();298 refreshTools();
299 };299 };
300 300
@@ -303,7 +303,7 @@ export function Shell() { · 2 unmodified lines
303 return () => {303 return () => {
304 import.meta.hot?.off("executor:backend-updated", refreshBackendData);304 import.meta.hot?.off("executor:backend-updated", refreshBackendData);
305 };305 };
306 }, [refreshSources, refreshTools]);306 }, [refreshIntegrations, refreshTools]);
307 307
308 return (308 return (
309 <div className="flex h-screen overflow-hidden">309 <div className="flex h-screen overflow-hidden">
packages/app/vite.ts+1−1
@@ -17,7 +17,7 @@ interface AppPluginOptions { · 16 unmodified lines
17 * point this at their own (typically `apps/local/executor.config.ts`).17 * point this at their own (typically `apps/local/executor.config.ts`).
18 *18 *
19 * If omitted, no plugin UIs get bundled — the renderer still works19 * If omitted, no plugin UIs get bundled — the renderer still works
20 * for built-in pages but Sources / Connections / etc. plugin pages20 * for built-in pages but Integrations / Connections / etc. plugin pages
21 * stay empty.21 * stay empty.
22 */22 */
23 readonly executorConfigPath?: string;23 readonly executorConfigPath?: string;
packages/core/api/src/plugin-routes.ts+1−1
@@ -57,7 +57,7 @@ export type PluginGroups<TPlugins extends readonly AnyPlugin[]> = ExtractPluginG · 56 unmodified lines
57 TPlugins[number]57 TPlugins[number]
58>;58>;
59 59
60/** Group identities baked into `CoreExecutorApi` (tools, sources, secrets,60/** Group identities baked into `CoreExecutorApi` (tools, integrations, secrets,
61 * …). Extracted via inference so adding a core group flows through61 * …). Extracted via inference so adding a core group flows through
62 * without touching this helper. */62 * without touching this helper. */
63type CoreGroups =63type CoreGroups =
packages/core/config/src/config.test.ts+37−35
@@ -7,8 +7,8 @@ import { join } from "node:path"; · 6 unmodified lines
7import { ExecutorFileConfig } from "./schema";7import { ExecutorFileConfig } from "./schema";
8import { ConfigParseError, loadConfig } from "./load";8import { ConfigParseError, loadConfig } from "./load";
9import {9import {
10 addSourceToConfig,10 addIntegrationToConfig,
11 removeSourceFromConfig,11 removeIntegrationFromConfig,
12 writeConfig,12 writeConfig,
13 addSecretToConfig,13 addSecretToConfig,
14 removeSecretFromConfig,14 removeSecretFromConfig,
@@ -27,15 +27,15 @@ const withTmpDir = <A, E>(fn: (dir: string) => Effect.Effect<A, E, FileSystem.Fi · 12 unmodified lines
27 27
28describe("ExecutorFileConfig schema", () => {28describe("ExecutorFileConfig schema", () => {
29 it("decodes a minimal config", () => {29 it("decodes a minimal config", () => {
30 const raw = { sources: [] };30 const raw = { integrations: [] };
31 const result = decodeExecutorFileConfig(raw);31 const result = decodeExecutorFileConfig(raw);
32 expect(result.sources).toEqual([]);32 expect(result.integrations).toEqual([]);
33 });33 });
34 34
35 it("decodes a full config with all source types", () => {35 it("decodes a full config with all integration types", () => {
36 const raw = {36 const raw = {
37 name: "test",37 name: "test",
38 sources: [38 integrations: [
39 {39 {
40 kind: "openapi",40 kind: "openapi",
41 spec: "https://example.com/openapi.json",41 spec: "https://example.com/openapi.json",
@@ -73,14 +73,14 @@ describe("ExecutorFileConfig schema", () => { · 31 unmodified lines
73 };73 };
74 74
75 const result = decodeExecutorFileConfig(raw);75 const result = decodeExecutorFileConfig(raw);
76 expect(result.sources).toHaveLength(4);76 expect(result.integrations).toHaveLength(4);
77 expect(result.name).toBe("test");77 expect(result.name).toBe("test");
78 expect(result.secrets!["my-token"]!.name).toBe("My Token");78 expect(result.secrets!["my-token"]!.name).toBe("My Token");
79 });79 });
80 80
81 it("rejects invalid source kind", () => {81 it("rejects invalid integration kind", () => {
82 const raw = {82 const raw = {
83 sources: [{ kind: "invalid", endpoint: "http://example.com" }],83 integrations: [{ kind: "invalid", endpoint: "http://example.com" }],
84 };84 };
85 expect(() => decodeExecutorFileConfig(raw)).toThrow();85 expect(() => decodeExecutorFileConfig(raw)).toThrow();
86 });86 });
@@ -106,7 +106,7 @@ describe("loadConfig", () => { · 19 unmodified lines
106 `{106 `{
107 // This is a comment107 // This is a comment
108 "name": "test",108 "name": "test",
109 "sources": [109 "integrations": [
110 {110 {
111 "kind": "openapi",111 "kind": "openapi",
112 "spec": "https://example.com/spec.json"112 "spec": "https://example.com/spec.json"
@@ -118,7 +118,7 @@ describe("loadConfig", () => { · 5 unmodified lines
118 const result = yield* loadConfig(path);118 const result = yield* loadConfig(path);
119 expect(result).not.toBeNull();119 expect(result).not.toBeNull();
120 expect(result!.name).toBe("test");120 expect(result!.name).toBe("test");
121 expect(result!.sources).toHaveLength(1);121 expect(result!.integrations).toHaveLength(1);
122 }),122 }),
123 ),123 ),
124 );124 );
@@ -140,13 +140,13 @@ describe("loadConfig", () => { · 15 unmodified lines
140});140});
141 141
142describe("write operations", () => {142describe("write operations", () => {
143 it.effect("addSourceToConfig creates file and adds source", () =>143 it.effect("addIntegrationToConfig creates file and adds integration", () =>
144 withTmpDir((dir) =>144 withTmpDir((dir) =>
145 Effect.gen(function* () {145 Effect.gen(function* () {
146 const fs = yield* FileSystem.FileSystem;146 const fs = yield* FileSystem.FileSystem;
147 const path = join(dir, "executor.jsonc");147 const path = join(dir, "executor.jsonc");
148 148
149 yield* addSourceToConfig(path, {149 yield* addIntegrationToConfig(path, {
150 kind: "openapi",150 kind: "openapi",
151 spec: "https://example.com/spec.json",151 spec: "https://example.com/spec.json",
152 namespace: "example",152 namespace: "example",
@@ -157,38 +157,38 @@ describe("write operations", () => { · 4 unmodified lines
157 expect(content).toContain("example");157 expect(content).toContain("example");
158 158
159 const config = yield* loadConfig(path);159 const config = yield* loadConfig(path);
160 expect(config!.sources).toHaveLength(1);160 expect(config!.integrations).toHaveLength(1);
161 expect(config!.sources![0]!.kind).toBe("openapi");161 expect(config!.integrations![0]!.kind).toBe("openapi");
162 }),162 }),
163 ),163 ),
164 );164 );
165 165
166 it.effect("addSourceToConfig appends to existing sources", () =>166 it.effect("addIntegrationToConfig appends to existing integrations", () =>
167 withTmpDir((dir) =>167 withTmpDir((dir) =>
168 Effect.gen(function* () {168 Effect.gen(function* () {
169 const path = join(dir, "executor.jsonc");169 const path = join(dir, "executor.jsonc");
170 170
171 yield* addSourceToConfig(path, {171 yield* addIntegrationToConfig(path, {
172 kind: "openapi",172 kind: "openapi",
173 spec: "https://example.com/spec.json",173 spec: "https://example.com/spec.json",
174 namespace: "first",174 namespace: "first",
175 });175 });
176 176
177 yield* addSourceToConfig(path, {177 yield* addIntegrationToConfig(path, {
178 kind: "graphql",178 kind: "graphql",
179 endpoint: "https://example.com/graphql",179 endpoint: "https://example.com/graphql",
180 namespace: "second",180 namespace: "second",
181 });181 });
182 182
183 const config = yield* loadConfig(path);183 const config = yield* loadConfig(path);
184 expect(config!.sources).toHaveLength(2);184 expect(config!.integrations).toHaveLength(2);
185 expect(config!.sources![0]!.kind).toBe("openapi");185 expect(config!.integrations![0]!.kind).toBe("openapi");
186 expect(config!.sources![1]!.kind).toBe("graphql");186 expect(config!.integrations![1]!.kind).toBe("graphql");
187 }),187 }),
188 ),188 ),
189 );189 );
190 190
191 it.effect("addSourceToConfig preserves comments", () =>191 it.effect("addIntegrationToConfig preserves comments", () =>
192 withTmpDir((dir) =>192 withTmpDir((dir) =>
193 Effect.gen(function* () {193 Effect.gen(function* () {
194 const fs = yield* FileSystem.FileSystem;194 const fs = yield* FileSystem.FileSystem;
@@ -199,7 +199,7 @@ describe("write operations", () => { · 4 unmodified lines
199 `{199 `{
200 // My project config200 // My project config
201 "name": "test",201 "name": "test",
202 "sources": [202 "integrations": [
203 {203 {
204 "kind": "openapi",204 "kind": "openapi",
205 "spec": "https://example.com/spec.json",205 "spec": "https://example.com/spec.json",
@@ -209,7 +209,7 @@ describe("write operations", () => { · 3 unmodified lines
209}`,209}`,
210 );210 );
211 211
212 yield* addSourceToConfig(path, {212 yield* addIntegrationToConfig(path, {
213 kind: "graphql",213 kind: "graphql",
214 endpoint: "https://example.com/graphql",214 endpoint: "https://example.com/graphql",
215 namespace: "second",215 namespace: "second",
@@ -222,19 +222,19 @@ describe("write operations", () => { · 6 unmodified lines
222 ),222 ),
223 );223 );
224 224
225 it.effect("addSourceToConfig replaces existing source with same namespace", () =>225 it.effect("addIntegrationToConfig replaces existing integration with same namespace", () =>
226 withTmpDir((dir) =>226 withTmpDir((dir) =>
227 Effect.gen(function* () {227 Effect.gen(function* () {
228 const path = join(dir, "executor.jsonc");228 const path = join(dir, "executor.jsonc");
229 229
230 yield* addSourceToConfig(path, {230 yield* addIntegrationToConfig(path, {
231 kind: "openapi",231 kind: "openapi",
232 spec: "https://example.com/v1.json",232 spec: "https://example.com/v1.json",
233 namespace: "example",233 namespace: "example",
234 });234 });
235 235
236 // Add again with same namespace but different spec236 // Add again with same namespace but different spec
237 yield* addSourceToConfig(path, {237 yield* addIntegrationToConfig(path, {
238 kind: "openapi",238 kind: "openapi",
239 spec: "https://example.com/v2.json",239 spec: "https://example.com/v2.json",
240 namespace: "example",240 namespace: "example",
@@ -242,19 +242,21 @@ describe("write operations", () => { · 1 unmodified line
242 242
243 const config = yield* loadConfig(path);243 const config = yield* loadConfig(path);
244 // Should have 1, not 2244 // Should have 1, not 2
245 expect(config!.sources).toHaveLength(1);245 expect(config!.integrations).toHaveLength(1);
246 expect((config!.sources![0] as { spec: string }).spec).toBe("https://example.com/v2.json");246 expect((config!.integrations![0] as { spec: string }).spec).toBe(
247 "https://example.com/v2.json",
248 );
247 }),249 }),
248 ),250 ),
249 );251 );
250 252
251 it.effect("removeSourceFromConfig removes by namespace", () =>253 it.effect("removeIntegrationFromConfig removes by namespace", () =>
252 withTmpDir((dir) =>254 withTmpDir((dir) =>
253 Effect.gen(function* () {255 Effect.gen(function* () {
254 const path = join(dir, "executor.jsonc");256 const path = join(dir, "executor.jsonc");
255 257
256 yield* writeConfig(path, {258 yield* writeConfig(path, {
257 sources: [259 integrations: [
258 {260 {
259 kind: "openapi",261 kind: "openapi",
260 spec: "https://example.com/spec.json",262 spec: "https://example.com/spec.json",
@@ -268,11 +270,11 @@ describe("write operations", () => { · 7 unmodified lines
268 ],270 ],
269 });271 });
270 272
271 yield* removeSourceFromConfig(path, "remove-me");273 yield* removeIntegrationFromConfig(path, "remove-me");
272 274
273 const config = yield* loadConfig(path);275 const config = yield* loadConfig(path);
274 expect(config!.sources).toHaveLength(1);276 expect(config!.integrations).toHaveLength(1);
275 expect(config!.sources![0]!.kind).toBe("openapi");277 expect(config!.integrations![0]!.kind).toBe("openapi");
276 }),278 }),
277 ),279 ),
278 );280 );
@@ -282,7 +284,7 @@ describe("write operations", () => { · 3 unmodified lines
282 Effect.gen(function* () {284 Effect.gen(function* () {
283 const path = join(dir, "executor.jsonc");285 const path = join(dir, "executor.jsonc");
284 286
285 yield* writeConfig(path, { sources: [] });287 yield* writeConfig(path, { integrations: [] });
286 288
287 yield* addSecretToConfig(path, "my-token", {289 yield* addSecretToConfig(path, "my-token", {
288 name: "My Token",290 name: "My Token",
packages/core/config/src/index.ts+7−7
@@ -1,11 +1,11 @@
1export {1export {
2 ExecutorFileConfig,2 ExecutorFileConfig,
3 PluginConfig,3 PluginConfig,
4 SourceConfig,4 IntegrationConfig,
5 OpenApiSourceConfig,5 OpenApiIntegrationConfig,
6 GraphqlSourceConfig,6 GraphqlIntegrationConfig,
7 McpRemoteSourceConfig,7 McpRemoteIntegrationConfig,
8 McpStdioSourceConfig,8 McpStdioIntegrationConfig,
9 McpAuthConfig,9 McpAuthConfig,
10 SecretMetadata,10 SecretMetadata,
11 ConfigHeaderValue,11 ConfigHeaderValue,
@@ -17,8 +17,8 @@ export { loadPluginsFromJsonc } from "./load-plugins"; · 5 unmodified lines
17export type { LoadPluginsFromJsoncOptions } from "./load-plugins";17export type { LoadPluginsFromJsoncOptions } from "./load-plugins";
18 18
19export {19export {
20 addSourceToConfig,20 addIntegrationToConfig,
21 removeSourceFromConfig,21 removeIntegrationFromConfig,
22 writeConfig,22 writeConfig,
23 addSecretToConfig,23 addSecretToConfig,
24 removeSecretFromConfig,24 removeSecretFromConfig,
packages/core/config/src/schema.ts+16−16
@@ -23,26 +23,26 @@ export type ConfigHeaderValue = typeof ConfigHeaderValue.Type; · 22 unmodified lines
23const ConfigHeaders = Schema.Record(Schema.String, ConfigHeaderValue);23const ConfigHeaders = Schema.Record(Schema.String, ConfigHeaderValue);
24 24
25// ---------------------------------------------------------------------------25// ---------------------------------------------------------------------------
26// Source configs — discriminated union on "kind"26// Integration configs — discriminated union on "kind"
27// ---------------------------------------------------------------------------27// ---------------------------------------------------------------------------
28 28
29export const OpenApiSourceConfig = Schema.Struct({29export const OpenApiIntegrationConfig = Schema.Struct({
30 kind: Schema.Literal("openapi"),30 kind: Schema.Literal("openapi"),
31 spec: Schema.String,31 spec: Schema.String,
32 baseUrl: Schema.optional(Schema.String),32 baseUrl: Schema.optional(Schema.String),
33 namespace: Schema.optional(Schema.String),33 namespace: Schema.optional(Schema.String),
34 headers: Schema.optional(ConfigHeaders),34 headers: Schema.optional(ConfigHeaders),
35});35});
36export type OpenApiSourceConfig = typeof OpenApiSourceConfig.Type;36export type OpenApiIntegrationConfig = typeof OpenApiIntegrationConfig.Type;
37 37
38export const GraphqlSourceConfig = Schema.Struct({38export const GraphqlIntegrationConfig = Schema.Struct({
39 kind: Schema.Literal("graphql"),39 kind: Schema.Literal("graphql"),
40 endpoint: Schema.String,40 endpoint: Schema.String,
41 introspectionJson: Schema.optional(Schema.NullOr(Schema.String)),41 introspectionJson: Schema.optional(Schema.NullOr(Schema.String)),
42 namespace: Schema.optional(Schema.String),42 namespace: Schema.optional(Schema.String),
43 headers: Schema.optional(ConfigHeaders),43 headers: Schema.optional(ConfigHeaders),
44});44});
45export type GraphqlSourceConfig = typeof GraphqlSourceConfig.Type;45export type GraphqlIntegrationConfig = typeof GraphqlIntegrationConfig.Type;
46 46
47const StringMap = Schema.Record(Schema.String, Schema.String);47const StringMap = Schema.Record(Schema.String, Schema.String);
48 48
@@ -66,7 +66,7 @@ export const McpAuthConfig = Schema.Union([ · 17 unmodified lines
66]);66]);
67export type McpAuthConfig = typeof McpAuthConfig.Type;67export type McpAuthConfig = typeof McpAuthConfig.Type;
68 68
69export const McpRemoteSourceConfig = Schema.Struct({69export const McpRemoteIntegrationConfig = Schema.Struct({
70 kind: Schema.Literal("mcp"),70 kind: Schema.Literal("mcp"),
71 transport: Schema.Literal("remote"),71 transport: Schema.Literal("remote"),
72 name: Schema.String,72 name: Schema.String,
@@ -77,9 +77,9 @@ export const McpRemoteSourceConfig = Schema.Struct({ · 4 unmodified lines
77 headers: Schema.optional(ConfigHeaders),77 headers: Schema.optional(ConfigHeaders),
78 auth: Schema.optional(McpAuthConfig),78 auth: Schema.optional(McpAuthConfig),
79});79});
80export type McpRemoteSourceConfig = typeof McpRemoteSourceConfig.Type;80export type McpRemoteIntegrationConfig = typeof McpRemoteIntegrationConfig.Type;
81 81
82export const McpStdioSourceConfig = Schema.Struct({82export const McpStdioIntegrationConfig = Schema.Struct({
83 kind: Schema.Literal("mcp"),83 kind: Schema.Literal("mcp"),
84 transport: Schema.Literal("stdio"),84 transport: Schema.Literal("stdio"),
85 name: Schema.String,85 name: Schema.String,
@@ -89,15 +89,15 @@ export const McpStdioSourceConfig = Schema.Struct({ · 3 unmodified lines
89 cwd: Schema.optional(Schema.String),89 cwd: Schema.optional(Schema.String),
90 namespace: Schema.optional(Schema.String),90 namespace: Schema.optional(Schema.String),
91});91});
92export type McpStdioSourceConfig = typeof McpStdioSourceConfig.Type;92export type McpStdioIntegrationConfig = typeof McpStdioIntegrationConfig.Type;
93 93
94export const SourceConfig = Schema.Union([94export const IntegrationConfig = Schema.Union([
95 OpenApiSourceConfig,95 OpenApiIntegrationConfig,
96 GraphqlSourceConfig,96 GraphqlIntegrationConfig,
97 McpRemoteSourceConfig,97 McpRemoteIntegrationConfig,
98 McpStdioSourceConfig,98 McpStdioIntegrationConfig,
99]);99]);
100export type SourceConfig = typeof SourceConfig.Type;100export type IntegrationConfig = typeof IntegrationConfig.Type;
101 101
102// ---------------------------------------------------------------------------102// ---------------------------------------------------------------------------
103// Secret metadata103// Secret metadata
@@ -136,7 +136,7 @@ export const ExecutorFileConfig = Schema.Struct({ · 32 unmodified lines
136 $schema: Schema.optional(Schema.String),136 $schema: Schema.optional(Schema.String),
137 name: Schema.optional(Schema.String),137 name: Schema.optional(Schema.String),
138 plugins: Schema.optional(Schema.Array(PluginConfig)),138 plugins: Schema.optional(Schema.Array(PluginConfig)),
139 sources: Schema.optional(Schema.Array(SourceConfig)),139 integrations: Schema.optional(Schema.Array(IntegrationConfig)),
140 secrets: Schema.optional(Schema.Record(Schema.String, SecretMetadata)),140 secrets: Schema.optional(Schema.Record(Schema.String, SecretMetadata)),
141});141});
142export type ExecutorFileConfig = typeof ExecutorFileConfig.Type;142export type ExecutorFileConfig = typeof ExecutorFileConfig.Type;
packages/core/config/src/sink.ts+10−10
@@ -1,7 +1,7 @@
1// ---------------------------------------------------------------------------1// ---------------------------------------------------------------------------
2// ConfigFileSink — best-effort write-back of source changes to executor.jsonc.2// ConfigFileSink — best-effort write-back of integration changes to executor.jsonc.
3//3//
4// Plugins (openapi, graphql, mcp) call `sink.upsertSource` after their DB4// Plugins (openapi, graphql, mcp) call `sink.upsertIntegration` after their DB
5// writes so the committable file stays in sync with runtime state. Errors5// writes so the committable file stays in sync with runtime state. Errors
6// are logged and swallowed — a failed file write must never fail a DB6// are logged and swallowed — a failed file write must never fail a DB
7// mutation, and the next successful mutation (or a boot-time sync) will7// mutation, and the next successful mutation (or a boot-time sync) will
@@ -15,8 +15,8 @@ import { Effect } from "effect"; · 7 unmodified lines
15import type { Layer } from "effect";15import type { Layer } from "effect";
16import type { FileSystem } from "effect";16import type { FileSystem } from "effect";
17 17
18import { SECRET_REF_PREFIX, type ConfigHeaderValue, type SourceConfig } from "./schema";18import { SECRET_REF_PREFIX, type ConfigHeaderValue, type IntegrationConfig } from "./schema";
19import { addSourceToConfig, removeSourceFromConfig } from "./write";19import { addIntegrationToConfig, removeIntegrationFromConfig } from "./write";
20 20
21// Translate a plugin-side header value (`{ secretId, prefix? }` for secret21// Translate a plugin-side header value (`{ secretId, prefix? }` for secret
22// refs) into the config file's `secret-public-ref:<id>` string form.22// refs) into the config file's `secret-public-ref:<id>` string form.
@@ -38,8 +38,8 @@ export const headersToConfigValues = ( · 15 unmodified lines
38};38};
39 39
40export interface ConfigFileSink {40export interface ConfigFileSink {
41 readonly upsertSource: (source: SourceConfig) => Effect.Effect<void>;41 readonly upsertIntegration: (integration: IntegrationConfig) => Effect.Effect<void>;
42 readonly removeSource: (namespace: string) => Effect.Effect<void>;42 readonly removeIntegration: (namespace: string) => Effect.Effect<void>;
43}43}
44 44
45export interface ConfigFileSinkOptions {45export interface ConfigFileSinkOptions {
@@ -57,14 +57,14 @@ export const makeFileConfigSink = (options: ConfigFileSinkOptions): ConfigFileSi · 11 unmodified lines
57 const { path, fsLayer, onError = defaultOnError } = options;57 const { path, fsLayer, onError = defaultOnError } = options;
58 58
59 return {59 return {
60 upsertSource: (source) =>60 upsertIntegration: (integration) =>
61 addSourceToConfig(path, source).pipe(61 addIntegrationToConfig(path, integration).pipe(
62 Effect.provide(fsLayer),62 Effect.provide(fsLayer),
63 Effect.catch((err: unknown) => Effect.sync(() => onError("upsert", err))),63 Effect.catch((err: unknown) => Effect.sync(() => onError("upsert", err))),
64 ),64 ),
65 65
66 removeSource: (namespace) =>66 removeIntegration: (namespace) =>
67 removeSourceFromConfig(path, namespace).pipe(67 removeIntegrationFromConfig(path, namespace).pipe(
68 Effect.provide(fsLayer),68 Effect.provide(fsLayer),
69 Effect.catch((err: unknown) => Effect.sync(() => onError("remove", err))),69 Effect.catch((err: unknown) => Effect.sync(() => onError("remove", err))),
70 ),70 ),
packages/core/config/src/write.ts+24−24
@@ -2,7 +2,7 @@ import { Effect } from "effect"; · 1 unmodified line
2import { FileSystem } from "effect";2import { FileSystem } from "effect";
3import type { PlatformError } from "effect/PlatformError";3import type { PlatformError } from "effect/PlatformError";
4import * as jsonc from "jsonc-parser";4import * as jsonc from "jsonc-parser";
5import type { SourceConfig, ExecutorFileConfig } from "./schema";5import type { IntegrationConfig, ExecutorFileConfig } from "./schema";
6 6
7export class ConfigWriteError {7export class ConfigWriteError {
8 readonly _tag = "ConfigWriteError";8 readonly _tag = "ConfigWriteError";
@@ -19,7 +19,7 @@ const FORMATTING: jsonc.FormattingOptions = { · 10 unmodified lines
19};19};
20 20
21const DEFAULT_CONFIG = `{21const DEFAULT_CONFIG = `{
22 "sources": []22 "integrations": []
23}23}
24`;24`;
25 25
@@ -36,35 +36,35 @@ const readOrCreate = ( · 10 unmodified lines
36 });36 });
37 37
38/**38/**
39 * Add a source entry to the config file. Creates the file if it doesn't exist.39 * Add an integration entry to the config file. Creates the file if it doesn't exist.
40 * Preserves existing comments and formatting.40 * Preserves existing comments and formatting.
41 */41 */
42export const addSourceToConfig = (42export const addIntegrationToConfig = (
43 path: string,43 path: string,
44 source: SourceConfig,44 integration: IntegrationConfig,
45): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>45): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
46 Effect.gen(function* () {46 Effect.gen(function* () {
47 const fs = yield* FileSystem.FileSystem;47 const fs = yield* FileSystem.FileSystem;
48 let text = yield* readOrCreate(fs, path);48 let text = yield* readOrCreate(fs, path);
49 49
50 // Ensure "sources" array exists50 // Ensure "integrations" array exists
51 let tree = jsonc.parseTree(text);51 let tree = jsonc.parseTree(text);
52 let sourcesNode = tree ? jsonc.findNodeAtLocation(tree, ["sources"]) : undefined;52 let integrationsNode = tree ? jsonc.findNodeAtLocation(tree, ["integrations"]) : undefined;
53 53
54 if (!sourcesNode) {54 if (!integrationsNode) {
55 const edits = jsonc.modify(text, ["sources"], [source], {55 const edits = jsonc.modify(text, ["integrations"], [integration], {
56 formattingOptions: FORMATTING,56 formattingOptions: FORMATTING,
57 });57 });
58 text = jsonc.applyEdits(text, edits);58 text = jsonc.applyEdits(text, edits);
59 } else {59 } else {
60 // Remove existing entry with same namespace (if any) to avoid duplicates60 // Remove existing entry with same namespace (if any) to avoid duplicates
61 const ns = "namespace" in source ? source.namespace : undefined;61 const ns = "namespace" in integration ? integration.namespace : undefined;
62 if (ns && sourcesNode.children) {62 if (ns && integrationsNode.children) {
63 for (let i = sourcesNode.children.length - 1; i >= 0; i--) {63 for (let i = integrationsNode.children.length - 1; i >= 0; i--) {
64 const child = sourcesNode.children[i]!;64 const child = integrationsNode.children[i]!;
65 const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);65 const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);
66 if (nsNode && jsonc.getNodeValue(nsNode) === ns) {66 if (nsNode && jsonc.getNodeValue(nsNode) === ns) {
67 const edits = jsonc.modify(text, ["sources", i], undefined, {67 const edits = jsonc.modify(text, ["integrations", i], undefined, {
68 formattingOptions: FORMATTING,68 formattingOptions: FORMATTING,
69 });69 });
70 text = jsonc.applyEdits(text, edits);70 text = jsonc.applyEdits(text, edits);
@@ -72,11 +72,11 @@ export const addSourceToConfig = ( · 1 unmodified line
72 }72 }
73 // Re-parse after removals73 // Re-parse after removals
74 tree = jsonc.parseTree(text);74 tree = jsonc.parseTree(text);
75 sourcesNode = tree ? jsonc.findNodeAtLocation(tree, ["sources"]) : undefined;75 integrationsNode = tree ? jsonc.findNodeAtLocation(tree, ["integrations"]) : undefined;
76 }76 }
77 77
78 const count = sourcesNode?.children?.length ?? 0;78 const count = integrationsNode?.children?.length ?? 0;
79 const edits = jsonc.modify(text, ["sources", count], source, {79 const edits = jsonc.modify(text, ["integrations", count], integration, {
80 formattingOptions: FORMATTING,80 formattingOptions: FORMATTING,
81 });81 });
82 text = jsonc.applyEdits(text, edits);82 text = jsonc.applyEdits(text, edits);
@@ -86,9 +86,9 @@ export const addSourceToConfig = ( · 3 unmodified lines
86 });86 });
87 87
88/**88/**
89 * Remove a source from the config file by namespace.89 * Remove an integration from the config file by namespace.
90 */90 */
91export const removeSourceFromConfig = (91export const removeIntegrationFromConfig = (
92 path: string,92 path: string,
93 namespace: string,93 namespace: string,
94): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>94): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
@@ -102,15 +102,15 @@ export const removeSourceFromConfig = ( · 7 unmodified lines
102 const tree = jsonc.parseTree(text);102 const tree = jsonc.parseTree(text);
103 if (!tree) return;103 if (!tree) return;
104 104
105 const sourcesNode = jsonc.findNodeAtLocation(tree, ["sources"]);105 const integrationsNode = jsonc.findNodeAtLocation(tree, ["integrations"]);
106 if (!sourcesNode?.children) return;106 if (!integrationsNode?.children) return;
107 107
108 // Walk backwards so indices stay valid after each removal108 // Walk backwards so indices stay valid after each removal
109 for (let i = sourcesNode.children.length - 1; i >= 0; i--) {109 for (let i = integrationsNode.children.length - 1; i >= 0; i--) {
110 const child = sourcesNode.children[i]!;110 const child = integrationsNode.children[i]!;
111 const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);111 const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);
112 if (nsNode && jsonc.getNodeValue(nsNode) === namespace) {112 if (nsNode && jsonc.getNodeValue(nsNode) === namespace) {
113 const edits = jsonc.modify(text, ["sources", i], undefined, {113 const edits = jsonc.modify(text, ["integrations", i], undefined, {
114 formattingOptions: FORMATTING,114 formattingOptions: FORMATTING,
115 });115 });
116 text = jsonc.applyEdits(text, edits);116 text = jsonc.applyEdits(text, edits);
packages/core/execution/src/engine.test.ts+1−1
@@ -31,7 +31,7 @@ const succeedingExecutor: CodeExecutor<FakeRuntimeError> = { · 30 unmodified lines
31const emptyPlugin = definePlugin(() => ({31const emptyPlugin = definePlugin(() => ({
32 id: "empty-test" as const,32 id: "empty-test" as const,
33 storage: () => ({}),33 storage: () => ({}),
34 staticSources: () => [],34 staticIntegrations: () => [],
35}));35}));
36 36
37const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const }));37const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const }));
packages/core/execution/src/engine.ts+7−7
@@ -15,7 +15,7 @@ import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor- · 14 unmodified lines
15import {15import {
16 defaultToolDiscoveryProvider,16 defaultToolDiscoveryProvider,
17 makeExecutorToolInvoker,17 makeExecutorToolInvoker,
18 listExecutorSources,18 listExecutorIntegrations,
19 describeTool,19 describeTool,
20 type ToolDiscoveryProvider,20 type ToolDiscoveryProvider,
21} from "./tool-invoker";21} from "./tool-invoker";
@@ -298,12 +298,12 @@ const makeFullInvoker = ( · 276 unmodified lines
298 }),298 }),
299 );299 );
300 }300 }
301 if (path === "executor.sources.list") {301 if (path === "executor.integrations.list") {
302 if (args !== undefined && !isRecord(args)) {302 if (args !== undefined && !isRecord(args)) {
303 return Effect.fail(303 return Effect.fail(
304 new ExecutionToolError({304 new ExecutionToolError({
305 message:305 message:
306 "tools.executor.sources.list expects an object: { query?: string; limit?: number; offset?: number }",306 "tools.executor.integrations.list expects an object: { query?: string; limit?: number; offset?: number }",
307 }),307 }),
308 );308 );
309 }309 }
@@ -311,14 +311,14 @@ const makeFullInvoker = ( · 1 unmodified line
311 if (isRecord(args) && args.query !== undefined && typeof args.query !== "string") {311 if (isRecord(args) && args.query !== undefined && typeof args.query !== "string") {
312 return Effect.fail(312 return Effect.fail(
313 new ExecutionToolError({313 new ExecutionToolError({
314 message: "tools.executor.sources.list query must be a string when provided",314 message: "tools.executor.integrations.list query must be a string when provided",
315 }),315 }),
316 );316 );
317 }317 }
318 318
319 const limit = readOptionalLimit(319 const limit = readOptionalLimit(
320 isRecord(args) ? args.limit : undefined,320 isRecord(args) ? args.limit : undefined,
321 "tools.executor.sources.list",321 "tools.executor.integrations.list",
322 );322 );
323 if (Predicate.isTagged(limit, "ExecutionToolError")) {323 if (Predicate.isTagged(limit, "ExecutionToolError")) {
324 return Effect.fail(limit);324 return Effect.fail(limit);
@@ -326,13 +326,13 @@ const makeFullInvoker = ( · 1 unmodified line
326 326
327 const offset = readOptionalOffset(327 const offset = readOptionalOffset(
328 isRecord(args) ? args.offset : undefined,328 isRecord(args) ? args.offset : undefined,
329 "tools.executor.sources.list",329 "tools.executor.integrations.list",
330 );330 );
331 if (Predicate.isTagged(offset, "ExecutionToolError")) {331 if (Predicate.isTagged(offset, "ExecutionToolError")) {
332 return Effect.fail(offset);332 return Effect.fail(offset);
333 }333 }
334 334
335 return listExecutorSources(executor, {335 return listExecutorIntegrations(executor, {
336 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,336 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,
337 limit,337 limit,
338 offset,338 offset,
packages/core/execution/src/index.ts+1−1
@@ -18,7 +18,7 @@ export { · 17 unmodified lines
18 defaultToolDiscoveryProvider,18 defaultToolDiscoveryProvider,
19 makeExecutorToolInvoker,19 makeExecutorToolInvoker,
20 searchTools,20 searchTools,
21 listExecutorSources,21 listExecutorIntegrations,
22 describeTool,22 describeTool,
23 type ToolDiscoveryInput,23 type ToolDiscoveryInput,
24 type ToolDiscoveryProvider,24 type ToolDiscoveryProvider,
packages/core/execution/src/tool-invoker.leak.test.ts+1−1
@@ -34,7 +34,7 @@ class FakeOpenApiInvocationError extends Data.TaggedError("OpenApiInvocationErro · 33 unmodified lines
34const leakyPlugin = definePlugin(() => ({34const leakyPlugin = definePlugin(() => ({
35 id: "leaky-test" as const,35 id: "leaky-test" as const,
36 storage: () => ({}),36 storage: () => ({}),
37 staticSources: () => [37 staticIntegrations: () => [
38 {38 {
39 id: "leaky",39 id: "leaky",
40 kind: "in-memory",40 kind: "in-memory",
packages/core/execution/src/tool-invoker.test.ts+11−11
@@ -630,7 +630,7 @@ describe("tool discovery", () => { · 629 unmodified lines
630 const executor = yield* makeSearchExecutor();630 const executor = yield* makeSearchExecutor();
631 631
632 const listed = yield* createExecutionEngine({ executor, codeExecutor }).execute(632 const listed = yield* createExecutionEngine({ executor, codeExecutor }).execute(
633 "return await tools.executor.sources.list();",633 "return await tools.executor.integrations.list();",
634 { onElicitation: acceptAll },634 { onElicitation: acceptAll },
635 );635 );
636 expect(listed.error).toBeUndefined();636 expect(listed.error).toBeUndefined();
@@ -666,7 +666,7 @@ describe("tool discovery", () => { · 29 unmodified lines
666 666
667 // total = 2 (github, crm), sorted by id ("crm" < "github")667 // total = 2 (github, crm), sorted by id ("crm" < "github")
668 const firstPage = yield* engine.execute(668 const firstPage = yield* engine.execute(
669 "return await tools.executor.sources.list({ limit: 1 });",669 "return await tools.executor.integrations.list({ limit: 1 });",
670 { onElicitation: acceptAll },670 { onElicitation: acceptAll },
671 );671 );
672 expect(firstPage.error).toBeUndefined();672 expect(firstPage.error).toBeUndefined();
@@ -680,7 +680,7 @@ describe("tool discovery", () => { · 7 unmodified lines
680 );680 );
681 681
682 const secondPage = yield* engine.execute(682 const secondPage = yield* engine.execute(
683 "return await tools.executor.sources.list({ limit: 1, offset: 1 });",683 "return await tools.executor.integrations.list({ limit: 1, offset: 1 });",
684 { onElicitation: acceptAll },684 { onElicitation: acceptAll },
685 );685 );
686 expect(secondPage.error).toBeUndefined();686 expect(secondPage.error).toBeUndefined();
@@ -719,7 +719,7 @@ describe("tool discovery", () => { · 32 unmodified lines
719 const badList = yield* engine.execute(719 const badList = yield* engine.execute(
720 [720 [
721 "try {",721 "try {",
722 " await tools.executor.sources.list({ offset: -5 });",722 " await tools.executor.integrations.list({ offset: -5 });",
723 ' return "unexpected";',723 ' return "unexpected";',
724 "} catch (error) {",724 "} catch (error) {",
725 " return error instanceof Error ? error.message : String(error);",725 " return error instanceof Error ? error.message : String(error);",
@@ -729,7 +729,7 @@ describe("tool discovery", () => { · 3 unmodified lines
729 );729 );
730 expect(badList.error).toBeUndefined();730 expect(badList.error).toBeUndefined();
731 expect(String(badList.result)).toContain(731 expect(String(badList.result)).toContain(
732 "tools.executor.sources.list offset must be a non-negative number when provided",732 "tools.executor.integrations.list offset must be a non-negative number when provided",
733 );733 );
734 }),734 }),
735 );735 );
@@ -876,13 +876,13 @@ describe("tool discovery", () => { · 140 unmodified lines
876 const execution = yield* engine.execute(876 const execution = yield* engine.execute(
877 [877 [
878 "const searchDetails = await tools.describe.tool({ path: 'search' });",878 "const searchDetails = await tools.describe.tool({ path: 'search' });",
879 "const sourceDetails = await tools.describe.tool({ path: 'executor.sources.list' });",879 "const integrationDetails = await tools.describe.tool({ path: 'executor.integrations.list' });",
880 "const describeDetails = await tools.describe.tool({ path: 'describe.tool' });",880 "const describeDetails = await tools.describe.tool({ path: 'describe.tool' });",
881 "return {",881 "return {",
882 " searchDetails,",882 " searchDetails,",
883 " searchResult: await tools.search({ query: 'repo details', limit: 2 }),",883 " searchResult: await tools.search({ query: 'repo details', limit: 2 }),",
884 " sourceDetails,",884 " integrationDetails,",
885 " sourceResult: await tools.executor.sources.list({ limit: 2 }),",885 " integrationResult: await tools.executor.integrations.list({ limit: 2 }),",
886 " describeDetails,",886 " describeDetails,",
887 " describeResult: await tools.describe.tool({ path: 'github.org.main.getRepositoryDetails' }),",887 " describeResult: await tools.describe.tool({ path: 'github.org.main.getRepositoryDetails' }),",
888 "};",888 "};",
@@ -894,8 +894,8 @@ describe("tool discovery", () => { · 5 unmodified lines
894 const observed = execution.result as {894 const observed = execution.result as {
895 readonly searchDetails: DescribedToolContract;895 readonly searchDetails: DescribedToolContract;
896 readonly searchResult: unknown;896 readonly searchResult: unknown;
897 readonly sourceDetails: DescribedToolContract;897 readonly integrationDetails: DescribedToolContract;
898 readonly sourceResult: unknown;898 readonly integrationResult: unknown;
899 readonly describeDetails: DescribedToolContract;899 readonly describeDetails: DescribedToolContract;
900 readonly describeResult: unknown;900 readonly describeResult: unknown;
901 };901 };
@@ -904,7 +904,7 @@ describe("tool discovery", () => { · 2 unmodified lines
904 typeCheckDescribedInvocation(observed.searchDetails, observed.searchResult, ""),904 typeCheckDescribedInvocation(observed.searchDetails, observed.searchResult, ""),
905 ).toEqual([]);905 ).toEqual([]);
906 expect(906 expect(
907 typeCheckDescribedInvocation(observed.sourceDetails, observed.sourceResult, ""),907 typeCheckDescribedInvocation(observed.integrationDetails, observed.integrationResult, ""),
908 ).toEqual([]);908 ).toEqual([]);
909 expect(909 expect(
910 typeCheckDescribedInvocation(observed.describeDetails, observed.describeResult, ""),910 typeCheckDescribedInvocation(observed.describeDetails, observed.describeResult, ""),
packages/core/execution/src/tool-invoker.ts+17−17
@@ -105,16 +105,16 @@ const BUILTIN_TOOL_DESCRIPTIONS: ReadonlyMap<string, DescribedTool> = new Map< · 104 unmodified lines
105 },105 },
106 ],106 ],
107 [107 [
108 "executor.sources.list",108 "executor.integrations.list",
109 {109 {
110 path: "executor.sources.list",110 path: "executor.integrations.list",
111 name: "executor.sources.list",111 name: "executor.integrations.list",
112 description: "List configured Executor integrations.",112 description: "List configured Executor integrations.",
113 inputTypeScript: "{ query?: string; limit?: number; offset?: number; }",113 inputTypeScript: "{ query?: string; limit?: number; offset?: number; }",
114 outputTypeScript:114 outputTypeScript:
115 "{ items: ExecutorSourceListItem[]; total: number; hasMore: boolean; nextOffset: number | null; }",115 "{ items: ExecutorIntegrationListItem[]; total: number; hasMore: boolean; nextOffset: number | null; }",
116 typeScriptDefinitions: {116 typeScriptDefinitions: {
117 ExecutorSourceListItem:117 ExecutorIntegrationListItem:
118 "{ id: string; name: string; description?: string; kind: string; canRemove?: boolean; canRefresh?: boolean; toolCount: number; }",118 "{ id: string; name: string; description?: string; kind: string; canRemove?: boolean; canRefresh?: boolean; toolCount: number; }",
119 },119 },
120 },120 },
@@ -245,7 +245,7 @@ const expectedToolFailure = (value: unknown): ToolError | null => { · 124 unmodified lines
245/**245/**
246 * Extract the integration namespace from a tool path. v2 addresses look like246 * Extract the integration namespace from a tool path. v2 addresses look like
247 * `<integration>.<owner>.<connection>.<tool>`; static fqids look like247 * `<integration>.<owner>.<connection>.<tool>`; static fqids look like
248 * `<source>.<op>`. We take the first segment as a cheap, non-lookup namespace248 * `<integration>.<op>`. We take the first segment as a cheap, non-lookup namespace
249 * for the span attribute so it's always populated without a catalog read.249 * for the span attribute so it's always populated without a catalog read.
250 */250 */
251const extractNamespace = (path: string): string => {251const extractNamespace = (path: string): string => {
@@ -364,7 +364,7 @@ export type ToolDiscoveryResult = { · 112 unmodified lines
364 readonly score: number;364 readonly score: number;
365};365};
366 366
367export type ExecutorSourceListItem = {367export type ExecutorIntegrationListItem = {
368 readonly id: string;368 readonly id: string;
369 readonly name: string;369 readonly name: string;
370 readonly description?: string;370 readonly description?: string;
@@ -390,7 +390,7 @@ export interface ToolDiscoveryProvider { · 19 unmodified lines
390 390
391/**391/**
392 * Page of results from a list-style discovery tool. Shared by392 * Page of results from a list-style discovery tool. Shared by
393 * `tools.search` and `tools.executor.sources.list` so the model sees one393 * `tools.search` and `tools.executor.integrations.list` so the model sees one
394 * consistent shape:394 * consistent shape:
395 *395 *
396 * - `items` — the page (slice).396 * - `items` — the page (slice).
@@ -674,10 +674,10 @@ export const defaultToolDiscoveryProvider: ToolDiscoveryProvider = { · 277 unmodified lines
674 searchTools(executor, query, limit, { namespace, offset }),674 searchTools(executor, query, limit, { namespace, offset }),
675};675};
676 676
677/** What `tools.executor.sources.list()` calls inside the sandbox. v2: the677/** What `tools.executor.integrations.list()` calls inside the sandbox. v2: the
678 * "sources" are the integration catalog; tool counts come from the678 * integrations are the integration catalog; tool counts come from the
679 * per-connection tool list. */679 * per-connection tool list. */
680export const listExecutorSources = Effect.fn("executor.sources.list")(function* (680export const listExecutorIntegrations = Effect.fn("executor.integrations.list")(function* (
681 executor: Executor,681 executor: Executor,
682 options?: {682 options?: {
683 readonly query?: string;683 readonly query?: string;
@@ -731,7 +731,7 @@ export const listExecutorSources = Effect.fn("executor.sources.list")(function* · 47 unmodified lines
731 id: String(integration.slug),731 id: String(integration.slug),
732 name: String(integration.slug),732 name: String(integration.slug),
733 // The integration's catalog description — user-editable context the733 // The integration's catalog description — user-editable context the
734 // agent can use to pick a source. Omitted when it just repeats the734 // agent can use to pick an integration. Omitted when it just repeats the
735 // slug or display name (no information beyond identity).735 // slug or display name (no information beyond identity).
736 ...(integration.description &&736 ...(integration.description &&
737 integration.description.toLowerCase() !== String(integration.slug).toLowerCase() &&737 integration.description.toLowerCase() !== String(integration.slug).toLowerCase() &&
@@ -742,17 +742,17 @@ export const listExecutorSources = Effect.fn("executor.sources.list")(function* · 4 unmodified lines
742 canRemove: integration.canRemove,742 canRemove: integration.canRemove,
743 canRefresh: integration.canRefresh,743 canRefresh: integration.canRefresh,
744 toolCount: toolCountByIntegration.get(String(integration.slug)) ?? 0,744 toolCount: toolCountByIntegration.get(String(integration.slug)) ?? 0,
745 }) satisfies ExecutorSourceListItem,745 }) satisfies ExecutorIntegrationListItem,
746 )746 )
747 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));747 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
748 748
749 const page = paginate(sortedWithCounts, offset, limit);749 const page = paginate(sortedWithCounts, offset, limit);
750 750
751 yield* Effect.annotateCurrentSpan({751 yield* Effect.annotateCurrentSpan({
752 "executor.sources.candidate_count": integrations.length,752 "executor.integrations.candidate_count": integrations.length,
753 "executor.sources.match_count": sortedWithCounts.length,753 "executor.integrations.match_count": sortedWithCounts.length,
754 "executor.sources.result_count": page.items.length,754 "executor.integrations.result_count": page.items.length,
755 "executor.sources.has_more": page.hasMore,755 "executor.integrations.has_more": page.hasMore,
756 });756 });
757 return page;757 return page;
758});758});
packages/core/sdk/README.md+3−3
@@ -1,6 +1,6 @@
1# @executor-js/sdk1# @executor-js/sdk
2 2
3A TypeScript SDK for building executors that wire together tool sources, secrets, and policies across MCP, OpenAPI, GraphQL, and custom plugins.3A TypeScript SDK for building executors that wire together integrations, secrets, and policies across MCP, OpenAPI, GraphQL, and custom plugins.
4 4
5The default surface is `Promise`-based — plugins are built on [Effect](https://effect.website/) under the hood, but consumers never have to touch it.5The default surface is `Promise`-based — plugins are built on [Effect](https://effect.website/) under the hood, but consumers never have to touch it.
6 6
@@ -132,7 +132,7 @@ The same pattern is what every shipped `@executor-js/plugin-*` package does inte · 125 unmodified lines
132 132
133These plugin packages are published from the monorepo:133These plugin packages are published from the monorepo:
134 134
135- [`@executor-js/plugin-mcp`](https://www.npmjs.com/package/@executor-js/plugin-mcp) — Model Context Protocol sources (stdio + remote)135- [`@executor-js/plugin-mcp`](https://www.npmjs.com/package/@executor-js/plugin-mcp) — Model Context Protocol integrations (stdio + remote)
136- [`@executor-js/plugin-openapi`](https://www.npmjs.com/package/@executor-js/plugin-openapi) — OpenAPI specs and Google Discovery documents as tools136- [`@executor-js/plugin-openapi`](https://www.npmjs.com/package/@executor-js/plugin-openapi) — OpenAPI specs and Google Discovery documents as tools
137- [`@executor-js/plugin-graphql`](https://www.npmjs.com/package/@executor-js/plugin-graphql) — GraphQL endpoints as tools137- [`@executor-js/plugin-graphql`](https://www.npmjs.com/package/@executor-js/plugin-graphql) — GraphQL endpoints as tools
138- [`@executor-js/plugin-file-secrets`](https://www.npmjs.com/package/@executor-js/plugin-file-secrets) — file-backed secret store138- [`@executor-js/plugin-file-secrets`](https://www.npmjs.com/package/@executor-js/plugin-file-secrets) — file-backed secret store
@@ -161,7 +161,7 @@ const value = await executor.secrets.get("github-token"); · 22 unmodified lines
161const refs = await executor.secrets.list();161const refs = await executor.secrets.list();
162```162```
163 163
164Plugins that need a token — HTTP-backed sources, OAuth flows, etc. — accept a secret id at the source-config layer and resolve through the executor, so token strings never live in your config files.164Plugins that need a token — HTTP-backed integrations, OAuth flows, etc. — accept a secret id at the integration-config layer and resolve through the executor, so token strings never live in your config files.
165 165
166## Status166## Status
167 167
packages/core/sdk/src/auth-tool-failure.ts+7−5
@@ -10,7 +10,7 @@ export type AuthToolFailureCode = · 9 unmodified lines
10export type AuthToolFailureInput = {10export type AuthToolFailureInput = {
11 readonly code: AuthToolFailureCode;11 readonly code: AuthToolFailureCode;
12 readonly message: string;12 readonly message: string;
13 readonly source?: {13 readonly integration?: {
14 readonly id: string;14 readonly id: string;
15 readonly scope?: string;15 readonly scope?: string;
16 };16 };
@@ -27,12 +27,12 @@ export type AuthToolFailureInput = { · 10 unmodified lines
27 readonly details?: unknown;27 readonly details?: unknown;
28 };28 };
29 readonly recovery?: {29 readonly recovery?: {
30 readonly configureSourceTool?: string;30 readonly configureIntegrationTool?: string;
31 };31 };
32};32};
33 33
34// In v1.5 a connection IS the credential: there is no standalone secret to34// In v1.5 a connection IS the credential: there is no standalone secret to
35// "bind" to a source afterward. Manually-entered credentials are created via35// "bind" to an integration afterward. Manually-entered credentials are created via
36// the connection handoff (the user enters the value in the web UI, which36// the connection handoff (the user enters the value in the web UI, which
37// creates the bound connection in one step); OAuth credentials are minted by37// creates the bound connection in one step); OAuth credentials are minted by
38// the OAuth start flow. These strings are read by the agent resolving the38// the OAuth start flow. These strings are read by the agent resolving the
@@ -41,7 +41,9 @@ const authRecovery = (input?: AuthToolFailureInput["recovery"]) => ({ · 2 unmodified lines
41 createConnectionTool: "executor.coreTools.connections.createHandoff",41 createConnectionTool: "executor.coreTools.connections.createHandoff",
42 startOAuthTool: "executor.coreTools.oauth.start",42 startOAuthTool: "executor.coreTools.oauth.start",
43 listConnectionsTool: "executor.coreTools.connections.list",43 listConnectionsTool: "executor.coreTools.connections.list",
44 ...(input?.configureSourceTool ? { configureSourceTool: input.configureSourceTool } : {}),44 ...(input?.configureIntegrationTool
45 ? { configureIntegrationTool: input.configureIntegrationTool }
46 : {}),
45 connectionInstructions:47 connectionInstructions:
46 "For API keys and tokens, call createConnectionTool for the integration to get a browser URL; the user enters the credential there, which creates the bound connection. Do not ask the user to paste secrets into chat. Then call listConnectionsTool to confirm the connection exists before retrying this tool.",48 "For API keys and tokens, call createConnectionTool for the integration to get a browser URL; the user enters the credential there, which creates the bound connection. Do not ask the user to paste secrets into chat. Then call listConnectionsTool to confirm the connection exists before retrying this tool.",
47 oauthInstructions:49 oauthInstructions:
@@ -56,7 +58,7 @@ export const authToolFailure = <T = never>(input: AuthToolFailureInput): ToolRes · 8 unmodified lines
56 ...(input.status !== undefined ? { status: input.status } : {}),58 ...(input.status !== undefined ? { status: input.status } : {}),
57 details: {59 details: {
58 category: "authentication",60 category: "authentication",
59 ...(input.source ? { source: input.source } : {}),61 ...(input.integration ? { integration: input.integration } : {}),
60 ...(input.credential ? { credential: input.credential } : {}),62 ...(input.credential ? { credential: input.credential } : {}),
61 ...(input.upstream ? { upstream: input.upstream } : {}),63 ...(input.upstream ? { upstream: input.upstream } : {}),
62 recovery: authRecovery(input.recovery),64 recovery: authRecovery(input.recovery),
packages/core/sdk/src/blob.ts+1−1
@@ -1,6 +1,6 @@
1// ---------------------------------------------------------------------------1// ---------------------------------------------------------------------------
2// BlobStore — the seam for large, opaque, write-once data. Blobs are stored2// BlobStore — the seam for large, opaque, write-once data. Blobs are stored
3// in FumaDB with their own lifecycle and namespacing, separate from source3// in FumaDB with their own lifecycle and namespacing, separate from integration
4// metadata and plugin-owned config rows.4// metadata and plugin-owned config rows.
5//5//
6// Plugins see a `PluginBlobStore` that's already namespaced to the6// Plugins see a `PluginBlobStore` that's already namespaced to the
packages/core/sdk/src/client.test.ts+2−2
@@ -68,13 +68,13 @@ describe("createPluginAtomClient", () => { · 67 unmodified lines
68 });68 });
69 69
70 it("can apply dynamic server connection URL and auth to plugin requests", () => {70 it("can apply dynamic server connection URL and auth to plugin requests", () => {
71 const request = HttpClientRequest.get("/graphql/sources");71 const request = HttpClientRequest.get("/graphql/integrations");
72 const transformed = applyPluginAtomClientRequestTransform(request, {72 const transformed = applyPluginAtomClientRequestTransform(request, {
73 baseUrl: () => "https://executor.example/api",73 baseUrl: () => "https://executor.example/api",
74 authorizationHeader: () => "Bearer key_123",74 authorizationHeader: () => "Bearer key_123",
75 });75 });
76 76
77 expect(transformed.url).toBe("https://executor.example/api/graphql/sources");77 expect(transformed.url).toBe("https://executor.example/api/graphql/integrations");
78 expect(transformed.headers.authorization).toBe("Bearer key_123");78 expect(transformed.headers.authorization).toBe("Bearer key_123");
79 });79 });
80 80
packages/core/sdk/src/client.ts+5−5
@@ -177,7 +177,7 @@ export type EditSheetApplyResult = · 176 unmodified lines
177 | { readonly ok: false };177 | { readonly ok: false };
178 178
179export interface EditSheetSectionProps {179export interface EditSheetSectionProps {
180 readonly sourceId: string;180 readonly integrationId: string;
181 readonly onPendingChange?: (apply: (() => Promise<EditSheetApplyResult>) | null) => void;181 readonly onPendingChange?: (apply: (() => Promise<EditSheetApplyResult>) | null) => void;
182}182}
183 183
@@ -199,7 +199,7 @@ export interface IntegrationPlugin { · 15 unmodified lines
199 /** Legacy full-page edit surface. No host renders this anymore — plugin199 /** Legacy full-page edit surface. No host renders this anymore — plugin
200 * configuration lives in the integration Edit sheet via `editSheet`. */200 * configuration lives in the integration Edit sheet via `editSheet`. */
201 readonly edit?: ComponentType<{201 readonly edit?: ComponentType<{
202 readonly sourceId: string;202 readonly integrationId: string;
203 readonly onSave: () => void;203 readonly onSave: () => void;
204 }>;204 }>;
205 /** Plugin-owned configuration rendered inside the integration's Edit sheet,205 /** Plugin-owned configuration rendered inside the integration's Edit sheet,
@@ -210,7 +210,7 @@ export interface IntegrationPlugin { · 4 unmodified lines
210 * apply keeps the sheet open with the section showing its own error. */210 * apply keeps the sheet open with the section showing its own error. */
211 readonly editSheet?: ComponentType<EditSheetSectionProps>;211 readonly editSheet?: ComponentType<EditSheetSectionProps>;
212 readonly summary?: ComponentType<{212 readonly summary?: ComponentType<{
213 readonly sourceId: string;213 readonly integrationId: string;
214 readonly variant?: "badge" | "panel";214 readonly variant?: "badge" | "panel";
215 readonly onAction?: () => void;215 readonly onAction?: () => void;
216 }>;216 }>;
@@ -218,7 +218,7 @@ export interface IntegrationPlugin { · 1 unmodified line
218 * the detail page's Accounts tab. Plugins that declare auth methods implement218 * the detail page's Accounts tab. Plugins that declare auth methods implement
219 * this; the page falls back to a generic accounts list when absent. */219 * this; the page falls back to a generic accounts list when absent. */
220 readonly accounts?: ComponentType<{220 readonly accounts?: ComponentType<{
221 readonly sourceId: string;221 readonly integrationId: string;
222 readonly integrationName: string;222 readonly integrationName: string;
223 readonly accountHandoff?: IntegrationAccountHandoff | null;223 readonly accountHandoff?: IntegrationAccountHandoff | null;
224 }>;224 }>;
@@ -248,7 +248,7 @@ export interface ClientPluginSpec<TId extends string = string> { · 23 unmodified lines
248 readonly widgets?: readonly WidgetDecl[];248 readonly widgets?: readonly WidgetDecl[];
249 readonly slots?: Record<string, SlotComponent>;249 readonly slots?: Record<string, SlotComponent>;
250 /** Integration plugin contribution — populated by plugins that expose250 /** Integration plugin contribution — populated by plugins that expose
251 * `kind` rows in the core `source` table (openapi, mcp, graphql).251 * `kind` rows in the core `integration` table (openapi, mcp, graphql).
252 * The host's integrations page derives its provider252 * The host's integrations page derives its provider
253 * list from the union of every loaded plugin's `integrationPlugin`. */253 * list from the union of every loaded plugin's `integrationPlugin`. */
254 readonly integrationPlugin?: IntegrationPlugin;254 readonly integrationPlugin?: IntegrationPlugin;
packages/core/sdk/src/core-tools.ts+1−1
@@ -514,7 +514,7 @@ export const coreToolsPlugin = definePlugin((options: CoreToolsPluginOptions = { · 513 unmodified lines
514 storage: () => ({}),514 storage: () => ({}),
515 extension: () => ({}),515 extension: () => ({}),
516 516
517 staticSources: () => [517 staticIntegrations: () => [
518 {518 {
519 id: "coreTools",519 id: "coreTools",
520 kind: "executor",520 kind: "executor",
packages/core/sdk/src/executor.ts+38−36
@@ -118,7 +118,7 @@ import type { · 117 unmodified lines
118 PluginCtx,118 PluginCtx,
119 PluginExtensions,119 PluginExtensions,
120 ResolveToolsResult,120 ResolveToolsResult,
121 StaticSourceDecl,121 StaticIntegrationDecl,
122 StaticToolDecl,122 StaticToolDecl,
123 StorageDeps,123 StorageDeps,
124 ToolPolicyProvider,124 ToolPolicyProvider,
@@ -1241,7 +1241,7 @@ const approvalArgumentPreview = (args: unknown): string => { · 1116 unmodified lines
1241// ---------------------------------------------------------------------------1241// ---------------------------------------------------------------------------
1242 1242
1243interface StaticTools {1243interface StaticTools {
1244 readonly source: StaticSourceDecl;1244 readonly integration: StaticIntegrationDecl;
1245 readonly tool: StaticToolDecl;1245 readonly tool: StaticToolDecl;
1246 readonly pluginId: string;1246 readonly pluginId: string;
1247 readonly ctx: PluginCtx<unknown>;1247 readonly ctx: PluginCtx<unknown>;
@@ -1253,9 +1253,9 @@ interface PluginRuntime { · 5 unmodified lines
1253 readonly ctx: PluginCtx<unknown>;1253 readonly ctx: PluginCtx<unknown>;
1254}1254}
1255 1255
1256const EXECUTOR_SOURCE_ID = "executor";1256const EXECUTOR_INTEGRATION_ID = "executor";
1257const EXECUTOR_SOURCE: StaticSourceDecl = {1257const EXECUTOR_INTEGRATION: StaticIntegrationDecl = {
1258 id: EXECUTOR_SOURCE_ID,1258 id: EXECUTOR_INTEGRATION_ID,
1259 kind: "built-in",1259 kind: "built-in",
1260 name: "Executor",1260 name: "Executor",
1261 canRemove: false,1261 canRemove: false,
@@ -1374,32 +1374,32 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 112 unmodified lines
1374 const credentialProviderOrder: string[] = [];1374 const credentialProviderOrder: string[] = [];
1375 1375
1376 const staticToolOwner = (): Owner => (subject == null ? "org" : "user");1376 const staticToolOwner = (): Owner => (subject == null ? "org" : "user");
1377 const staticToolConnection = (source: StaticSourceDecl): ConnectionName =>1377 const staticToolConnection = (integration: StaticIntegrationDecl): ConnectionName =>
1378 ConnectionName.make(source.id === EXECUTOR_SOURCE_ID ? "coreTools" : "static");1378 ConnectionName.make(integration.id === EXECUTOR_INTEGRATION_ID ? "coreTools" : "static");
1379 1379
1380 const staticSources = (): readonly StaticSourceDecl[] => {1380 const staticIntegrations = (): readonly StaticIntegrationDecl[] => {
1381 const byId = new Map<string, StaticSourceDecl>();1381 const byId = new Map<string, StaticIntegrationDecl>();
1382 for (const entry of staticTools.values()) {1382 for (const entry of staticTools.values()) {
1383 if (!byId.has(entry.source.id)) byId.set(entry.source.id, entry.source);1383 if (!byId.has(entry.integration.id)) byId.set(entry.integration.id, entry.integration);
1384 }1384 }
1385 return [...byId.values()];1385 return [...byId.values()];
1386 };1386 };
1387 1387
1388 const staticSourceToIntegration = (source: StaticSourceDecl): Integration => ({1388 const staticDeclToIntegration = (integration: StaticIntegrationDecl): Integration => ({
1389 slug: IntegrationSlug.make(source.id),1389 slug: IntegrationSlug.make(integration.id),
1390 name: source.name,1390 name: integration.name,
1391 description: source.name,1391 description: integration.name,
1392 kind: source.kind,1392 kind: integration.kind,
1393 canRemove: source.canRemove ?? false,1393 canRemove: integration.canRemove ?? false,
1394 canRefresh: source.canRefresh ?? false,1394 canRefresh: integration.canRefresh ?? false,
1395 authMethods: [],1395 authMethods: [],
1396 });1396 });
1397 1397
1398 const staticToolToTool = (entry: StaticTools): Tool => ({1398 const staticToolToTool = (entry: StaticTools): Tool => ({
1399 address: ToolAddress.make(`${entry.source.id}.${entry.tool.name}`),1399 address: ToolAddress.make(`${entry.integration.id}.${entry.tool.name}`),
1400 owner: staticToolOwner(),1400 owner: staticToolOwner(),
1401 integration: IntegrationSlug.make(entry.source.id),1401 integration: IntegrationSlug.make(entry.integration.id),
1402 connection: staticToolConnection(entry.source),1402 connection: staticToolConnection(entry.integration),
1403 name: ToolName.make(entry.tool.name),1403 name: ToolName.make(entry.tool.name),
1404 pluginId: entry.pluginId,1404 pluginId: entry.pluginId,
1405 description: entry.tool.description,1405 description: entry.tool.description,
@@ -1814,21 +1814,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 408 unmodified lines
1814 const integrationsList = (): Effect.Effect<readonly Integration[], StorageFailure> =>1814 const integrationsList = (): Effect.Effect<readonly Integration[], StorageFailure> =>
1815 Effect.gen(function* () {1815 Effect.gen(function* () {
1816 const rows = yield* core.findMany("integration", {});1816 const rows = yield* core.findMany("integration", {});
1817 const staticIntegrations = staticSources().map(staticSourceToIntegration);1817 const staticIntegrationList = staticIntegrations().map(staticDeclToIntegration);
1818 const dbIntegrations = rows.map((row) =>1818 const dbIntegrations = rows.map((row) =>
1819 rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayForRow(row)),1819 rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayForRow(row)),
1820 );1820 );
1821 // A scoped toolkit must not advertise providers it grants no tools from1821 // A scoped toolkit must not advertise providers it grants no tools from
1822 // (mirrors `connectionsList`). Static sources are system namespaces, not1822 // (mirrors `connectionsList`). Static integrations are system namespaces, not
1823 // user providers, so they stay; DB-backed integrations are filtered to1823 // user providers, so they stay; DB-backed integrations are filtered to
1824 // those that contribute at least one visible tool under the active policy.1824 // those that contribute at least one visible tool under the active policy.
1825 if (!activeToolPolicyProvider) return [...staticIntegrations, ...dbIntegrations];1825 if (!activeToolPolicyProvider) return [...staticIntegrationList, ...dbIntegrations];
1826 const visibleTools = yield* toolsList({ includeAnnotations: false });1826 const visibleTools = yield* toolsList({ includeAnnotations: false });
1827 const visibleIntegrationSlugs = new Set(1827 const visibleIntegrationSlugs = new Set(
1828 visibleTools.filter((tool) => !tool.static).map((tool) => String(tool.integration)),1828 visibleTools.filter((tool) => !tool.static).map((tool) => String(tool.integration)),
1829 );1829 );
1830 return [1830 return [
1831 ...staticIntegrations,1831 ...staticIntegrationList,
1832 ...dbIntegrations.filter((integration) =>1832 ...dbIntegrations.filter((integration) =>
1833 visibleIntegrationSlugs.has(String(integration.slug)),1833 visibleIntegrationSlugs.has(String(integration.slug)),
1834 ),1834 ),
@@ -1839,8 +1839,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 4 unmodified lines
1839 slug: IntegrationSlug,1839 slug: IntegrationSlug,
1840 ): Effect.Effect<Integration | null, StorageFailure> =>1840 ): Effect.Effect<Integration | null, StorageFailure> =>
1841 Effect.gen(function* () {1841 Effect.gen(function* () {
1842 const staticSource = staticSources().find((source) => source.id === String(slug));1842 const staticIntegration = staticIntegrations().find(
1843 if (staticSource) return staticSourceToIntegration(staticSource);1843 (integration) => integration.id === String(slug),
1844 );
1845 if (staticIntegration) return staticDeclToIntegration(staticIntegration);
1844 const row = yield* findIntegrationRow(slug);1846 const row = yield* findIntegrationRow(slug);
1845 return row1847 return row
1846 ? rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayForRow(row))1848 ? rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayForRow(row))
@@ -2135,7 +2137,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 288 unmodified lines
2135 );2137 );
2136 2138
2137 if (result.incomplete === true) {2139 if (result.incomplete === true) {
2138 // Non-authoritative listing (source unreachable, auth not ready).2140 // Non-authoritative listing (integration unreachable, auth not ready).
2139 // Keep the existing catalog — replacing it would wipe working tools2141 // Keep the existing catalog — replacing it would wipe working tools
2140 // over a transient outage — and stamp the sync time anyway so a down2142 // over a transient outage — and stamp the sync time anyway so a down
2141 // server isn't re-dialed on every read; the freshness TTL re-attempts2143 // server isn't re-dialed on every read; the freshness TTL re-attempts
@@ -3871,21 +3873,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1729 unmodified lines
3871 }3873 }
3872 }3874 }
3873 3875
3874 // Build extension FIRST so it's available as `self` for staticSources.3876 // Build extension FIRST so it's available as `self` for staticIntegrations.
3875 const extension: object = plugin.extension ? plugin.extension(ctx) : {};3877 const extension: object = plugin.extension ? plugin.extension(ctx) : {};
3876 if (plugin.extension) {3878 if (plugin.extension) {
3877 extensions[plugin.id] = extension;3879 extensions[plugin.id] = extension;
3878 }3880 }
3879 3881
3880 const decls = plugin.staticSources ? plugin.staticSources(extension) : [];3882 const decls = plugin.staticIntegrations ? plugin.staticIntegrations(extension) : [];
3881 for (const source of decls) {3883 for (const integration of decls) {
3882 const mountUnderExecutor = source.kind === "executor";3884 const mountUnderExecutor = integration.kind === "executor";
3883 const mountedSource = mountUnderExecutor ? EXECUTOR_SOURCE : source;3885 const mountedIntegration = mountUnderExecutor ? EXECUTOR_INTEGRATION : integration;
3884 for (const tool of source.tools) {3886 for (const tool of integration.tools) {
3885 const mountedTool = mountUnderExecutor3887 const mountedTool = mountUnderExecutor
3886 ? { ...tool, name: `${source.id}.${tool.name}` }3888 ? { ...tool, name: `${integration.id}.${tool.name}` }
3887 : tool;3889 : tool;
3888 const fqid = `${mountedSource.id}.${mountedTool.name}`;3890 const fqid = `${mountedIntegration.id}.${mountedTool.name}`;
3889 if (staticTools.has(fqid)) {3891 if (staticTools.has(fqid)) {
3890 return yield* new StorageError({3892 return yield* new StorageError({
3891 message: `Duplicate static tool id: ${fqid} (plugin ${plugin.id})`,3893 message: `Duplicate static tool id: ${fqid} (plugin ${plugin.id})`,
@@ -3893,7 +3895,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1 unmodified line
3893 });3895 });
3894 }3896 }
3895 staticTools.set(fqid, {3897 staticTools.set(fqid, {
3896 source: mountedSource,3898 integration: mountedIntegration,
3897 tool: mountedTool,3899 tool: mountedTool,
3898 pluginId: plugin.id,3900 pluginId: plugin.id,
3899 ctx,3901 ctx,
packages/core/sdk/src/index.ts+1−1
@@ -303,7 +303,7 @@ export { · 302 unmodified lines
303 type ToolPolicyProvider,303 type ToolPolicyProvider,
304 type ToolPolicyProviderRule,304 type ToolPolicyProviderRule,
305 type IntegrationRecord,305 type IntegrationRecord,
306 type StaticSourceDecl,306 type StaticIntegrationDecl,
307 type StaticToolDecl,307 type StaticToolDecl,
308 type StaticToolSchema,308 type StaticToolSchema,
309 type StaticToolExecuteContext,309 type StaticToolExecuteContext,
packages/core/sdk/src/migration-spec.test.ts+2−2
@@ -484,13 +484,13 @@ describe("migrateOpenApiSourceConfig", () => { · 483 unmodified lines
484 });484 });
485 const config = r.config as {485 const config = r.config as {
486 readonly spec?: string;486 readonly spec?: string;
487 readonly sourceUrl?: string;487 readonly specUrl?: string;
488 readonly baseUrl?: string;488 readonly baseUrl?: string;
489 readonly headers?: Record<string, string>;489 readonly headers?: Record<string, string>;
490 readonly authenticationTemplate?: unknown;490 readonly authenticationTemplate?: unknown;
491 };491 };
492 expect(config.spec).toBe("{openapi}");492 expect(config.spec).toBe("{openapi}");
493 expect(config.sourceUrl).toBe("https://api.example.com/openapi.json");493 expect(config.specUrl).toBe("https://api.example.com/openapi.json");
494 expect(config.baseUrl).toBe("https://api.example.com");494 expect(config.baseUrl).toBe("https://api.example.com");
495 // The literal header is static config; the credential header became a template.495 // The literal header is static config; the credential header became a template.
496 expect(config.headers).toEqual({ "User-Agent": "executor/1.0" });496 expect(config.headers).toEqual({ "User-Agent": "executor/1.0" });
packages/core/sdk/src/migration-spec.ts+2−2
@@ -608,7 +608,7 @@ export interface V1OpenApiSourceConfig extends V1OpenApiAuthConfig { · 607 unmodified lines
608 608
609export interface V2OpenApiIntegrationConfig {609export interface V2OpenApiIntegrationConfig {
610 readonly spec?: string;610 readonly spec?: string;
611 readonly sourceUrl?: string;611 readonly specUrl?: string;
612 readonly baseUrl?: string;612 readonly baseUrl?: string;
613 readonly googleDiscoveryUrls?: readonly string[];613 readonly googleDiscoveryUrls?: readonly string[];
614 readonly headers?: Record<string, string>;614 readonly headers?: Record<string, string>;
@@ -630,7 +630,7 @@ export const migrateOpenApiSourceConfig = (v1: V1OpenApiSourceConfig): MigratedS · 15 unmodified lines
630 const auth = migrateOpenApiAuthTemplate(v1);630 const auth = migrateOpenApiAuthTemplate(v1);
631 const config: V2OpenApiIntegrationConfig = {631 const config: V2OpenApiIntegrationConfig = {
632 ...(v1.spec !== undefined ? { spec: v1.spec } : {}),632 ...(v1.spec !== undefined ? { spec: v1.spec } : {}),
633 ...(v1.sourceUrl !== undefined ? { sourceUrl: v1.sourceUrl } : {}),633 ...(v1.sourceUrl !== undefined ? { specUrl: v1.sourceUrl } : {}),
634 ...(v1.baseUrl !== undefined ? { baseUrl: v1.baseUrl } : {}),634 ...(v1.baseUrl !== undefined ? { baseUrl: v1.baseUrl } : {}),
635 ...(v1.googleDiscoveryUrls !== undefined635 ...(v1.googleDiscoveryUrls !== undefined
636 ? { googleDiscoveryUrls: v1.googleDiscoveryUrls }636 ? { googleDiscoveryUrls: v1.googleDiscoveryUrls }
packages/core/sdk/src/plugin.ts+13−11
@@ -145,9 +145,9 @@ export interface IntegrationRecord extends Integration { · 144 unmodified lines
145 145
146// ---------------------------------------------------------------------------146// ---------------------------------------------------------------------------
147// PluginCtx — threaded into every extension method, static tool handler, and147// PluginCtx — threaded into every extension method, static tool handler, and
148// dynamic tool handler. The v2 fold: `core.sources` → `core.integrations`,148// dynamic tool handler. The v2 fold folded `core.sources` into
149// `secrets`/`connections`/`credentialBindings` → `connections` (provider-149// `core.integrations`, `secrets`/`connections`/`credentialBindings` into
150// resolved) + `providers`, `scopes` → `owner`.150// `connections` (provider-resolved) + `providers`, and `scopes` into `owner`.
151// ---------------------------------------------------------------------------151// ---------------------------------------------------------------------------
152 152
153export interface PluginCtx<TStore = unknown> {153export interface PluginCtx<TStore = unknown> {
@@ -290,11 +290,11 @@ export interface ResolveToolsResult { · 136 unmodified lines
290 readonly tools: readonly ToolDef[];290 readonly tools: readonly ToolDef[];
291 /** Shared JSON-schema `$defs` reachable from the tools' `$ref`s. */291 /** Shared JSON-schema `$defs` reachable from the tools' `$ref`s. */
292 readonly definitions?: Record<string, unknown>;292 readonly definitions?: Record<string, unknown>;
293 /** The source could not be (fully) enumerated: unreachable server, auth not293 /** The integration could not be (fully) enumerated: unreachable server, auth not
294 * ready, listing aborted. The result is non-authoritative, so the executor294 * ready, listing aborted. The result is non-authoritative, so the executor
295 * keeps the connection's existing persisted catalog instead of replacing it295 * keeps the connection's existing persisted catalog instead of replacing it
296 * (a transient outage must not wipe working tools). Omit / `false` when the296 * (a transient outage must not wipe working tools). Omit / `false` when the
297 * listing is authoritative, including a genuine "this source has zero297 * listing is authoritative, including a genuine "this integration has zero
298 * tools". */298 * tools". */
299 readonly incomplete?: boolean;299 readonly incomplete?: boolean;
300 /** Human-readable reason for an incomplete listing. Persisted by core when it300 /** Human-readable reason for an incomplete listing. Persisted by core when it
@@ -354,7 +354,7 @@ export interface HealthCheckCandidatesInput<TStore = unknown> { · 53 unmodified lines
354}354}
355 355
356// ---------------------------------------------------------------------------356// ---------------------------------------------------------------------------
357// Static tool / source declarations. Unchanged from v1 except the ctx shape.357// Static tool / integration declarations. Unchanged from v1 except the ctx shape.
358// ---------------------------------------------------------------------------358// ---------------------------------------------------------------------------
359 359
360export interface StaticToolHandlerInput<TStore = unknown> {360export interface StaticToolHandlerInput<TStore = unknown> {
@@ -433,7 +433,7 @@ export const tool = < · 72 unmodified lines
433 ),433 ),
434});434});
435 435
436export interface StaticSourceDecl<TStore = unknown> {436export interface StaticIntegrationDecl<TStore = unknown> {
437 readonly id: string;437 readonly id: string;
438 readonly kind: string;438 readonly kind: string;
439 readonly name: string;439 readonly name: string;
@@ -574,12 +574,14 @@ export interface PluginSpec< · 134 unmodified lines
574 readonly integrationPresets?: readonly IntegrationPreset[];574 readonly integrationPresets?: readonly IntegrationPreset[];
575 575
576 /** Build the plugin's extension API — becomes `executor[plugin.id]` and the576 /** Build the plugin's extension API — becomes `executor[plugin.id]` and the
577 * `self` passed to `staticSources`. Field order matters: `extension` MUST577 * `self` passed to `staticIntegrations`. Field order matters: `extension` MUST
578 * appear before `staticSources`. */578 * appear before `staticIntegrations`. */
579 readonly extension?: (ctx: PluginCtx<TStore>) => TExtension;579 readonly extension?: (ctx: PluginCtx<TStore>) => TExtension;
580 580
581 /** Static sources contributed by this plugin with inline tool handlers. */581 /** Static integrations contributed by this plugin with inline tool handlers. */
582 readonly staticSources?: (self: NoInfer<TExtension>) => readonly StaticSourceDecl<TStore>[];582 readonly staticIntegrations?: (
583 self: NoInfer<TExtension>,
584 ) => readonly StaticIntegrationDecl<TStore>[];
583 585
584 /** HttpApiGroup contributed by this plugin. */586 /** HttpApiGroup contributed by this plugin. */
585 readonly routes?: () => TGroup;587 readonly routes?: () => TGroup;
packages/core/sdk/src/policies.test.ts+1−1
@@ -516,7 +516,7 @@ describe("active tool-policy provider", () => { · 515 unmodified lines
516 const staticPlugin = definePlugin(() => ({516 const staticPlugin = definePlugin(() => ({
517 id: "toolkit-fixture" as const,517 id: "toolkit-fixture" as const,
518 storage: () => ({}),518 storage: () => ({}),
519 staticSources: () => [519 staticIntegrations: () => [
520 {520 {
521 id: "toolkit-fixture.ctl",521 id: "toolkit-fixture.ctl",
522 kind: "control" as const,522 kind: "control" as const,
packages/core/sdk/src/promise-executor.ts+1−1
@@ -8,7 +8,7 @@ · 7 unmodified lines
8// that — the proxy flattens plugin extension methods too.8// that — the proxy flattens plugin extension methods too.
9//9//
10// Not a goal: authoring plugins in Promise style. The plugin model10// Not a goal: authoring plugins in Promise style. The plugin model
11// (storage, schema, staticSources, Effect ctx) is Effect-only. Bring11// (storage, schema, staticIntegrations, Effect ctx) is Effect-only. Bring
12// your own `@executor-js/plugin-*` from the Effect side.12// your own `@executor-js/plugin-*` from the Effect side.
13// ---------------------------------------------------------------------------13// ---------------------------------------------------------------------------
14 14
packages/core/sdk/src/promise.test.ts+2−2
@@ -15,7 +15,7 @@ import { Effect, Schema } from "effect"; · 14 unmodified lines
15const echoPlugin = definePlugin(() => ({15const echoPlugin = definePlugin(() => ({
16 id: "echo" as const,16 id: "echo" as const,
17 storage: () => ({}),17 storage: () => ({}),
18 staticSources: () => [18 staticIntegrations: () => [
19 {19 {
20 id: "echo.ctl",20 id: "echo.ctl",
21 kind: "control" as const,21 kind: "control" as const,
@@ -72,7 +72,7 @@ describe("promise/createExecutor", () => { · 50 unmodified lines
72 const approvedPlugin = definePlugin(() => ({72 const approvedPlugin = definePlugin(() => ({
73 id: "ap" as const,73 id: "ap" as const,
74 storage: () => ({}),74 storage: () => ({}),
75 staticSources: () => [75 staticIntegrations: () => [
76 {76 {
77 id: "ap.ctl",77 id: "ap.ctl",
78 kind: "control" as const,78 kind: "control" as const,
packages/core/sdk/src/sqlite-config-blob-migration.test.ts+2−2
@@ -49,7 +49,7 @@ describe("runSqliteConfigBlobMigration", () => { · 48 unmodified lines
49 tenant: "t1",49 tenant: "t1",
50 slug: "legacy_api",50 slug: "legacy_api",
51 pluginId: "openapi",51 pluginId: "openapi",
52 config: { spec, sourceUrl: "https://example.com/spec.json" },52 config: { spec, specUrl: "https://example.com/spec.json" },
53 }),53 }),
54 );54 );
55 55
@@ -63,7 +63,7 @@ describe("runSqliteConfigBlobMigration", () => { · 7 unmodified lines
63 expect(config.spec).toBeUndefined();63 expect(config.spec).toBeUndefined();
64 expect(typeof config.specHash).toBe("string");64 expect(typeof config.specHash).toBe("string");
65 // Untouched fields survive the rewrite.65 // Untouched fields survive the rewrite.
66 expect(config.sourceUrl).toBe("https://example.com/spec.json");66 expect(config.specUrl).toBe("https://example.com/spec.json");
67 67
68 // The blob row uses the runtime's exact naming: namespace68 // The blob row uses the runtime's exact naming: namespace
69 // `o:<tenant>/<pluginId>`, key `spec/<hash>`, id JSON.stringify pair.69 // `o:<tenant>/<pluginId>`, key `spec/<hash>`, id JSON.stringify pair.
packages/hosts/mcp/src/tool-server.test.ts+1−1
@@ -1095,7 +1095,7 @@ describe("MCP host server — client without elicitation (pause/resume)", () => · 1094 unmodified lines
1095 Effect.succeed(1095 Effect.succeed(
1096 makePausedResult(1096 makePausedResult(
1097 "exec_confirm",1097 "exec_confirm",
1098 FormElicitation.make({ message: "Confirm source add", requestedSchema: {} }),1098 FormElicitation.make({ message: "Confirm integration add", requestedSchema: {} }),
1099 ),1099 ),
1100 ),1100 ),
1101 });1101 });
packages/kernel/core/src/strip-types.test.ts+1−1
@@ -60,7 +60,7 @@ describe("stripTypeScript", () => { · 59 unmodified lines
60 // a function expression.60 // a function expression.
61 const code = `61 const code = `
62 const fetchResources = async (sourceId: string): Promise<Array<{ id: string }>> => {62 const fetchResources = async (sourceId: string): Promise<Array<{ id: string }>> => {
63 const result = await tools.executor.sources.list();63 const result = await tools.executor.integrations.list();
64 return result.items;64 return result.items;
65 };65 };
66 return fetchResources('example-source');66 return fetchResources('example-source');
packages/kernel/ir/src/registry.ts+2−2
@@ -4,7 +4,7 @@ import type { JsonSchema } from "effect/JsonSchema"; · 3 unmodified lines
4export const ToolRegistration = Schema.Struct({4export const ToolRegistration = Schema.Struct({
5 path: Schema.String,5 path: Schema.String,
6 description: Schema.optional(Schema.String),6 description: Schema.optional(Schema.String),
7 sourceId: Schema.String,7 integrationId: Schema.String,
8 input: Schema.optional(Schema.String),8 input: Schema.optional(Schema.String),
9 output: Schema.optional(Schema.String),9 output: Schema.optional(Schema.String),
10 error: Schema.optional(Schema.String),10 error: Schema.optional(Schema.String),
@@ -21,7 +21,7 @@ export type SerializedCatalog = typeof SerializedCatalog.Type; · 10 unmodified lines
21export interface LiveToolRegistration {21export interface LiveToolRegistration {
22 readonly path: string;22 readonly path: string;
23 readonly description?: string;23 readonly description?: string;
24 readonly sourceId: string;24 readonly integrationId: string;
25 readonly input?: Schema.Top;25 readonly input?: Schema.Top;
26 readonly output?: Schema.Top;26 readonly output?: Schema.Top;
27 readonly error?: Schema.Top;27 readonly error?: Schema.Top;
packages/kernel/ir/src/serialize.ts+2−2
@@ -29,7 +29,7 @@ export function serialize(catalog: LiveCatalog): typeof SerializedCatalog.Type { · 28 unmodified lines
29 const tools = catalog.tools.map((tool) => ({29 const tools = catalog.tools.map((tool) => ({
30 path: tool.path,30 path: tool.path,
31 description: tool.description,31 description: tool.description,
32 sourceId: tool.sourceId,32 integrationId: tool.integrationId,
33 input: schemaToRef(tool.input),33 input: schemaToRef(tool.input),
34 output: schemaToRef(tool.output),34 output: schemaToRef(tool.output),
35 error: schemaToRef(tool.error),35 error: schemaToRef(tool.error),
@@ -70,7 +70,7 @@ export function deserializeToJsonSchema(serialized: typeof SerializedCatalog.Typ · 34 unmodified lines
70 tools: serialized.tools.map((tool) => ({70 tools: serialized.tools.map((tool) => ({
71 path: tool.path,71 path: tool.path,
72 description: tool.description,72 description: tool.description,
73 sourceId: tool.sourceId,73 integrationId: tool.integrationId,
74 input: resolveRef(tool.input),74 input: resolveRef(tool.input),
75 output: resolveRef(tool.output),75 output: resolveRef(tool.output),
76 error: resolveRef(tool.error),76 error: resolveRef(tool.error),
packages/plugins/desktop-settings/src/client.tsx+5−5
@@ -198,8 +198,8 @@ function SettingsPage() { · 197 unmodified lines
198 const authLabel = "Bearer token";198 const authLabel = "Bearer token";
199 const cliProfileCommand = `executor server add desktop ${connection.origin} --default`;199 const cliProfileCommand = `executor server add desktop ${connection.origin} --default`;
200 const cliUseCommand = authToken200 const cliUseCommand = authToken
201 ? `EXECUTOR_AUTH_TOKEN=${authToken} executor tools sources --server desktop`201 ? `EXECUTOR_AUTH_TOKEN=${authToken} executor tools integrations --server desktop`
202 : "executor tools sources --server desktop";202 : "executor tools integrations --server desktop";
203 203
204 return (204 return (
205 <div style={pageFrameStyle}>205 <div style={pageFrameStyle}>
@@ -360,8 +360,8 @@ function SettingsPage() { · 154 unmodified lines
360 <div style={{ fontSize: "0.875rem", fontWeight: 600 }}>Diagnostics</div>360 <div style={{ fontSize: "0.875rem", fontWeight: 600 }}>Diagnostics</div>
361 <span style={{ fontSize: "0.75rem", color: "var(--muted-foreground, #888)" }}>361 <span style={{ fontSize: "0.75rem", color: "var(--muted-foreground, #888)" }}>
362 Packs app and server logs, crash dumps, and version info into a zip in your362 Packs app and server logs, crash dumps, and version info into a zip in your
363 Downloads folder — attach it when reporting a bug. Your sources, secrets, and bearer363 Downloads folder — attach it when reporting a bug. Your integrations, secrets, and
364 token are not included.364 bearer token are not included.
365 </span>365 </span>
366 <div style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>366 <div style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
367 {/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */}367 {/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */}
@@ -430,7 +430,7 @@ export default defineClientPlugin({ · 62 unmodified lines
430 path: "/",430 path: "/",
431 component: SettingsPage,431 component: SettingsPage,
432 // Only contribute a nav entry when running inside the desktop app.432 // Only contribute a nav entry when running inside the desktop app.
433 // Web users see an empty Sources nav without a non-functional433 // Web users see an empty Integrations nav without a non-functional
434 // "Settings" link.434 // "Settings" link.
435 ...(inDesktop ? { nav: { label: "Settings" } } : {}),435 ...(inDesktop ? { nav: { label: "Settings" } } : {}),
436 },436 },
packages/plugins/desktop-settings/src/server.ts+1−1
@@ -37,7 +37,7 @@ export const desktopSettingsPlugin = definePlugin((options: DesktopSettingsPlugi · 36 unmodified lines
37 id: "desktop-settings" as const,37 id: "desktop-settings" as const,
38 packageName: "@executor-js/plugin-desktop-settings",38 packageName: "@executor-js/plugin-desktop-settings",
39 storage: () => ({}),39 storage: () => ({}),
40 staticSources: () => [40 staticIntegrations: () => [
41 {41 {
42 id: "desktopSettings",42 id: "desktopSettings",
43 kind: "executor",43 kind: "executor",
packages/plugins/graphql/README.md+2−2
@@ -22,7 +22,7 @@ const executor = await createExecutor({ · 21 unmodified lines
22});22});
23 23
24// Public endpoint — no auth24// Public endpoint — no auth
25await executor.graphql.addSource({25await executor.graphql.addIntegration({
26 scope: executor.scopes[0]!.id,26 scope: executor.scopes[0]!.id,
27 endpoint: "https://graphql.anilist.co",27 endpoint: "https://graphql.anilist.co",
28 namespace: "anilist",28 namespace: "anilist",
@@ -55,7 +55,7 @@ await executor.secrets.set({ · 26 unmodified lines
55 scope,55 scope,
56});56});
57 57
58await executor.graphql.addSource({58await executor.graphql.addIntegration({
59 scope,59 scope,
60 endpoint: "https://api.github.com/graphql",60 endpoint: "https://api.github.com/graphql",
61 namespace: "github",61 namespace: "github",
packages/plugins/graphql/src/react/AddGraphqlIntegration.tsxrenamed from packages/plugins/graphql/src/react/AddGraphqlSource.tsx+6−6
@@ -24,7 +24,7 @@ import { · 23 unmodified lines
24} from "@executor-js/react/lib/integration-add";24} from "@executor-js/react/lib/integration-add";
25 25
26import { createGraphqlIntegrationOptimistic } from "./atoms";26import { createGraphqlIntegrationOptimistic } from "./atoms";
27import { GraphqlSourceFields } from "./GraphqlSourceFields";27import { GraphqlIntegrationFields } from "./GraphqlIntegrationFields";
28import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config";28import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config";
29import type { GraphqlAuthMethodInput } from "../sdk/types";29import type { GraphqlAuthMethodInput } from "../sdk/types";
30 30
@@ -38,7 +38,7 @@ import type { GraphqlAuthMethodInput } from "../sdk/types"; · 7 unmodified lines
38// — a fresh [] every render would re-seed the list each render).38// — a fresh [] every render would re-seed the list each render).
39const NO_SEEDS: readonly AuthMethodSeed[] = [];39const NO_SEEDS: readonly AuthMethodSeed[] = [];
40 40
41export default function AddGraphqlSource(props: {41export default function AddGraphqlIntegration(props: {
42 onComplete: (slug?: string) => void;42 onComplete: (slug?: string) => void;
43 onCancel: () => void;43 onCancel: () => void;
44 initialUrl?: string;44 initialUrl?: string;
@@ -93,7 +93,7 @@ export default function AddGraphqlSource(props: { · 48 unmodified lines
93 93
94 const canAdd = endpoint.trim().length > 0 && apiKeyComplete && !adding && !slugAlreadyExists;94 const canAdd = endpoint.trim().length > 0 && apiKeyComplete && !adding && !slugAlreadyExists;
95 95
96 const sourceIdentity = useCallback(() => {96 const integrationIdentity = useCallback(() => {
97 const trimmedEndpoint = endpoint.trim();97 const trimmedEndpoint = endpoint.trim();
98 const slug = resolvedSlug;98 const slug = resolvedSlug;
99 const displayName =99 const displayName =
@@ -104,7 +104,7 @@ export default function AddGraphqlSource(props: { · 4 unmodified lines
104 const handleAdd = async (): Promise<void> => {104 const handleAdd = async (): Promise<void> => {
105 setAdding(true);105 setAdding(true);
106 setAddError(null);106 setAddError(null);
107 const { trimmedEndpoint, slug, displayName } = sourceIdentity();107 const { trimmedEndpoint, slug, displayName } = integrationIdentity();
108 108
109 const integrationExit = await doAddIntegration({109 const integrationExit = await doAddIntegration({
110 payload: {110 payload: {
@@ -119,7 +119,7 @@ export default function AddGraphqlSource(props: { · 8 unmodified lines
119 reactivityKeys: integrationWriteKeys,119 reactivityKeys: integrationWriteKeys,
120 });120 });
121 if (Exit.isFailure(integrationExit)) {121 if (Exit.isFailure(integrationExit)) {
122 setAddError(addIntegrationErrorMessage(integrationExit, slug, "Failed to add source"));122 setAddError(addIntegrationErrorMessage(integrationExit, slug, "Failed to add integration"));
123 setAdding(false);123 setAdding(false);
124 return;124 return;
125 }125 }
@@ -132,7 +132,7 @@ export default function AddGraphqlSource(props: { · 6 unmodified lines
132 <div className="flex flex-1 flex-col gap-6">132 <div className="flex flex-1 flex-col gap-6">
133 <h1 className="text-xl font-semibold text-foreground">Add GraphQL integration</h1>133 <h1 className="text-xl font-semibold text-foreground">Add GraphQL integration</h1>
134 134
135 <GraphqlSourceFields135 <GraphqlIntegrationFields
136 endpoint={endpoint}136 endpoint={endpoint}
137 onEndpointChange={setEndpoint}137 onEndpointChange={setEndpoint}
138 identity={identity}138 identity={identity}
packages/plugins/graphql/src/react/GraphqlAccountsPanel.tsx+3−3
@@ -36,12 +36,12 @@ import type { GraphqlAuthMethod } from "../sdk/types"; · 35 unmodified lines
36// ---------------------------------------------------------------------------36// ---------------------------------------------------------------------------
37 37
38export default function GraphqlAccountsPanel(props: {38export default function GraphqlAccountsPanel(props: {
39 readonly sourceId: string;39 readonly integrationId: string;
40 readonly integrationName: string;40 readonly integrationName: string;
41 readonly accountHandoff?: IntegrationAccountHandoff | null;41 readonly accountHandoff?: IntegrationAccountHandoff | null;
42}) {42}) {
43 const { sourceId, integrationName, accountHandoff } = props;43 const { integrationId, integrationName, accountHandoff } = props;
44 const slug = IntegrationSlug.make(sourceId);44 const slug = IntegrationSlug.make(integrationId);
45 const configResult = useAtomValue(graphqlConfigAtom(slug));45 const configResult = useAtomValue(graphqlConfigAtom(slug));
46 const doConfigure = useAtomSet(graphqlConfigure, { mode: "promiseExit" });46 const doConfigure = useAtomSet(graphqlConfigure, { mode: "promiseExit" });
47 47
packages/plugins/graphql/src/react/GraphqlIntegrationFields.tsxrenamed from packages/plugins/graphql/src/react/GraphqlSourceFields.tsx+1−1
@@ -10,7 +10,7 @@ import { · 9 unmodified lines
10 type IntegrationIdentity,10 type IntegrationIdentity,
11} from "@executor-js/react/plugins/integration-identity";11} from "@executor-js/react/plugins/integration-identity";
12 12
13export function GraphqlSourceFields(props: {13export function GraphqlIntegrationFields(props: {
14 readonly endpoint: string;14 readonly endpoint: string;
15 readonly onEndpointChange: (endpoint: string) => void;15 readonly onEndpointChange: (endpoint: string) => void;
16 readonly identity: IntegrationIdentity;16 readonly identity: IntegrationIdentity;
packages/plugins/graphql/src/react/index.ts+1−1
@@ -1,3 +1,3 @@
1export { graphqlIntegrationPlugin } from "./source-plugin";1export { graphqlIntegrationPlugin } from "./integration-plugin";
2export { GraphqlClient } from "./client";2export { GraphqlClient } from "./client";
3export { createGraphqlIntegration, graphqlIntegrationConfigAtom } from "./atoms";3export { createGraphqlIntegration, graphqlIntegrationConfigAtom } from "./atoms";
packages/plugins/graphql/src/react/integration-plugin.tsrenamed from packages/plugins/graphql/src/react/source-plugin.ts+1−1
@@ -2,7 +2,7 @@ import { lazy } from "react"; · 1 unmodified line
2import type { IntegrationPlugin } from "@executor-js/sdk/client";2import type { IntegrationPlugin } from "@executor-js/sdk/client";
3import { graphqlPresets } from "../sdk/presets";3import { graphqlPresets } from "../sdk/presets";
4 4
5const importAdd = () => import("./AddGraphqlSource");5const importAdd = () => import("./AddGraphqlIntegration");
6const importAccounts = () => import("./GraphqlAccountsPanel");6const importAccounts = () => import("./GraphqlAccountsPanel");
7 7
8// No `editSheet`: GraphQL has no plugin-specific configuration beyond auth8// No `editSheet`: GraphQL has no plugin-specific configuration beyond auth
packages/plugins/graphql/src/react/plugin-client.tsx+1−1
@@ -1,6 +1,6 @@
1import { defineClientPlugin } from "@executor-js/sdk/client";1import { defineClientPlugin } from "@executor-js/sdk/client";
2 2
3import { graphqlIntegrationPlugin } from "./source-plugin";3import { graphqlIntegrationPlugin } from "./integration-plugin";
4 4
5export default defineClientPlugin({5export default defineClientPlugin({
6 id: "graphql" as const,6 id: "graphql" as const,
packages/plugins/graphql/src/sdk/plugin.ts+4−4
@@ -166,7 +166,7 @@ const graphqlAuthToolFailure = (failure: GraphqlAuthRequiredError) => · 165 unmodified lines
166 authToolFailure({166 authToolFailure({
167 code: failure.code,167 code: failure.code,
168 message: failure.message,168 message: failure.message,
169 source: { id: failure.integration, scope: failure.owner },169 integration: { id: failure.integration, scope: failure.owner },
170 credential: {170 credential: {
171 kind: failure.credentialKind,171 kind: failure.credentialKind,
172 ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),172 ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),
@@ -641,7 +641,7 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => { · 468 unmodified lines
641 : [],641 : [],
642 });642 });
643 643
644 /** Register the integration in the catalog. Registering a source is a644 /** Register the integration in the catalog. Registering an integration is a
645 * catalog statement ("we use this GraphQL endpoint now") and MUST NOT make a645 * catalog statement ("we use this GraphQL endpoint now") and MUST NOT make a
646 * network call or require auth — exactly like MCP defers discovery. Live646 * network call or require auth — exactly like MCP defers discovery. Live
647 * introspection (and the operation bindings it yields) is deferred to647 * introspection (and the operation bindings it yields) is deferred to
@@ -899,7 +899,7 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 251 unmodified lines
899 describeAuthMethods: describeGraphqlAuthMethods,899 describeAuthMethods: describeGraphqlAuthMethods,
900 describeIntegrationDisplay: describeGraphqlIntegrationDisplay,900 describeIntegrationDisplay: describeGraphqlIntegrationDisplay,
901 901
902 staticSources: (self: GraphqlPluginExtension) => [902 staticIntegrations: (self: GraphqlPluginExtension) => [
903 {903 {
904 id: "graphql",904 id: "graphql",
905 kind: "executor",905 kind: "executor",
@@ -1149,7 +1149,7 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 243 unmodified lines
1149 code: "connection_rejected",1149 code: "connection_rejected",
1150 status: result.status,1150 status: result.status,
1151 message: `Upstream rejected credentials for GraphQL integration "${integration}" with HTTP ${result.status}. Re-authenticate or update the connection before retrying this tool.`,1151 message: `Upstream rejected credentials for GraphQL integration "${integration}" with HTTP ${result.status}. Re-authenticate or update the connection before retrying this tool.`,
1152 source: { id: integration, scope: credential.owner },1152 integration: { id: integration, scope: credential.owner },
1153 credential: { kind: "upstream", label: "Upstream authorization" },1153 credential: { kind: "upstream", label: "Upstream authorization" },
1154 upstream: {1154 upstream: {
1155 status: result.status,1155 status: result.status,
packages/plugins/mcp/README.md+4−4
@@ -1,6 +1,6 @@
1# @executor-js/plugin-mcp1# @executor-js/plugin-mcp
2 2
3Register [Model Context Protocol](https://modelcontextprotocol.io) servers as tool sources for an executor. Supports both stdio-launched servers and remote (HTTP) servers, with optional OAuth.3Register [Model Context Protocol](https://modelcontextprotocol.io) servers as tool integrations for an executor. Supports both stdio-launched servers and remote (HTTP) servers, with optional OAuth.
4 4
5## Install5## Install
6 6
@@ -18,7 +18,7 @@ import { mcpPlugin } from "@executor-js/plugin-mcp"; · 11 unmodified lines
18 18
19const executor = await createExecutor({19const executor = await createExecutor({
20 onElicitation: "accept-all",20 onElicitation: "accept-all",
21 // Stdio sources spawn a local subprocess and inherit `process.env` —21 // Stdio integrations spawn a local subprocess and inherit `process.env` —
22 // only enable for trusted single-user contexts.22 // only enable for trusted single-user contexts.
23 plugins: [mcpPlugin({ dangerouslyAllowStdioMCP: true })] as const,23 plugins: [mcpPlugin({ dangerouslyAllowStdioMCP: true })] as const,
24});24});
@@ -26,7 +26,7 @@ const executor = await createExecutor({ · 1 unmodified line
26const scope = executor.scopes[0]!.id;26const scope = executor.scopes[0]!.id;
27 27
28// Remote MCP server28// Remote MCP server
29await executor.mcp.addSource({29await executor.mcp.addIntegration({
30 scope,30 scope,
31 transport: "remote",31 transport: "remote",
32 name: "Context7",32 name: "Context7",
@@ -34,7 +34,7 @@ await executor.mcp.addSource({ · 1 unmodified line
34});34});
35 35
36// Stdio MCP server (requires `dangerouslyAllowStdioMCP: true` above)36// Stdio MCP server (requires `dangerouslyAllowStdioMCP: true` above)
37await executor.mcp.addSource({37await executor.mcp.addIntegration({
38 scope,38 scope,
39 transport: "stdio",39 transport: "stdio",
40 name: "My Server",40 name: "My Server",
packages/plugins/mcp/src/react/AddMcpIntegration.tsxrenamed from packages/plugins/mcp/src/react/AddMcpSource.tsx+3−3
@@ -36,7 +36,7 @@ import { · 35 unmodified lines
36import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";36import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
37import type { McpAuthMethodInput } from "../sdk/types";37import type { McpAuthMethodInput } from "../sdk/types";
38import { probeMcpEndpoint, addMcpServer } from "./atoms";38import { probeMcpEndpoint, addMcpServer } from "./atoms";
39import { McpRemoteSourceFields } from "./McpRemoteSourceFields";39import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";
40import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";40import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
41import { mcpPresets, type McpPreset } from "../sdk/presets";41import { mcpPresets, type McpPreset } from "../sdk/presets";
42 42
@@ -161,7 +161,7 @@ function reducer(state: State, action: Action): State { · 118 unmodified lines
161// Component161// Component
162// ---------------------------------------------------------------------------162// ---------------------------------------------------------------------------
163 163
164export default function AddMcpSource(props: {164export default function AddMcpIntegration(props: {
165 onComplete: (slug?: string) => void;165 onComplete: (slug?: string) => void;
166 onCancel: () => void;166 onCancel: () => void;
167 initialUrl?: string;167 initialUrl?: string;
@@ -418,7 +418,7 @@ export default function AddMcpSource(props: { · 250 unmodified lines
418 418
419 {transport === "remote" ? (419 {transport === "remote" ? (
420 <>420 <>
421 <McpRemoteSourceFields421 <McpRemoteIntegrationFields
422 url={state.url}422 url={state.url}
423 onUrlChange={(url) => dispatch({ type: "set-url", url })}423 onUrlChange={(url) => dispatch({ type: "set-url", url })}
424 identity={remoteIdentity}424 identity={remoteIdentity}
packages/plugins/mcp/src/react/EditMcpIntegration.tsxrenamed from packages/plugins/mcp/src/react/EditMcpSource.tsx+8−5
@@ -186,8 +186,8 @@ function StdioReadOnly(props: { · 185 unmodified lines
186 <div className="space-y-1">186 <div className="space-y-1">
187 <p className="text-sm font-medium text-foreground">Server command</p>187 <p className="text-sm font-medium text-foreground">Server command</p>
188 <p className="text-xs text-muted-foreground">188 <p className="text-xs text-muted-foreground">
189 Stdio MCP sources cannot be edited. Remove and recreate the source with the updated189 Stdio MCP integrations cannot be edited. Remove and recreate the integration with the
190 command.190 updated command.
191 </p>191 </p>
192 </div>192 </div>
193 <div className="flex items-center gap-3 rounded-md border border-border/60 bg-muted/40 px-3 py-2">193 <div className="flex items-center gap-3 rounded-md border border-border/60 bg-muted/40 px-3 py-2">
@@ -204,11 +204,14 @@ function StdioReadOnly(props: { · 10 unmodified lines
204 204
205// ---------------------------------------------------------------------------205// ---------------------------------------------------------------------------
206// Main component — the mcp plugin's section of the integration Edit sheet.206// Main component — the mcp plugin's section of the integration Edit sheet.
207// `sourceId` is the integration slug (v2).207// `integrationId` is the integration slug (v2).
208// ---------------------------------------------------------------------------208// ---------------------------------------------------------------------------
209 209
210export default function EditMcpSource({ sourceId, onPendingChange }: EditSheetSectionProps) {210export default function EditMcpIntegration({
211 const slug = IntegrationSlug.make(sourceId);211 integrationId,
212 onPendingChange,
213}: EditSheetSectionProps) {
214 const slug = IntegrationSlug.make(integrationId);
212 const serverResult = useAtomValue(mcpServerAtom(slug));215 const serverResult = useAtomValue(mcpServerAtom(slug));
213 const server = AsyncResult.isSuccess(serverResult) ? serverResult.value : null;216 const server = AsyncResult.isSuccess(serverResult) ? serverResult.value : null;
214 217
packages/plugins/mcp/src/react/McpAccountsPanel.tsx+3−3
@@ -36,12 +36,12 @@ import type { McpAuthMethod } from "../sdk/types"; · 35 unmodified lines
36// ---------------------------------------------------------------------------36// ---------------------------------------------------------------------------
37 37
38export default function McpAccountsPanel(props: {38export default function McpAccountsPanel(props: {
39 readonly sourceId: string;39 readonly integrationId: string;
40 readonly integrationName: string;40 readonly integrationName: string;
41 readonly accountHandoff?: IntegrationAccountHandoff | null;41 readonly accountHandoff?: IntegrationAccountHandoff | null;
42}) {42}) {
43 const { sourceId, integrationName, accountHandoff } = props;43 const { integrationId, integrationName, accountHandoff } = props;
44 const slug = IntegrationSlug.make(sourceId);44 const slug = IntegrationSlug.make(integrationId);
45 const serverResult = useAtomValue(mcpServerAtom(slug));45 const serverResult = useAtomValue(mcpServerAtom(slug));
46 const doConfigureAuth = useAtomSet(configureMcpAuth, { mode: "promiseExit" });46 const doConfigureAuth = useAtomSet(configureMcpAuth, { mode: "promiseExit" });
47 47
packages/plugins/mcp/src/react/McpRemoteIntegrationFields.tsxrenamed from packages/plugins/mcp/src/react/McpRemoteSourceFields.tsx+3−3
@@ -22,7 +22,7 @@ import { · 21 unmodified lines
22 type IntegrationIdentity,22 type IntegrationIdentity,
23} from "@executor-js/react/plugins/integration-identity";23} from "@executor-js/react/plugins/integration-identity";
24 24
25export type McpRemoteSourcePreview = {25export type McpRemoteIntegrationPreview = {
26 readonly name: string;26 readonly name: string;
27 readonly serverName: string | null;27 readonly serverName: string | null;
28 readonly connected: boolean;28 readonly connected: boolean;
@@ -31,7 +31,7 @@ export type McpRemoteSourcePreview = { · 2 unmodified lines
31 readonly toolCount: number | null;31 readonly toolCount: number | null;
32};32};
33 33
34export function McpRemoteSourceFields(props: {34export function McpRemoteIntegrationFields(props: {
35 readonly url: string;35 readonly url: string;
36 readonly onUrlChange: (url: string) => void;36 readonly onUrlChange: (url: string) => void;
37 readonly identity: IntegrationIdentity;37 readonly identity: IntegrationIdentity;
@@ -39,7 +39,7 @@ export function McpRemoteSourceFields(props: { · 1 unmodified line
39 * `instructions` when the probe connected). */39 * `instructions` when the probe connected). */
40 readonly description?: string;40 readonly description?: string;
41 readonly onDescriptionChange?: (value: string) => void;41 readonly onDescriptionChange?: (value: string) => void;
42 readonly preview: McpRemoteSourcePreview | null;42 readonly preview: McpRemoteIntegrationPreview | null;
43 readonly probing?: boolean;43 readonly probing?: boolean;
44 readonly error?: string | null;44 readonly error?: string | null;
45 readonly onRetry?: () => void;45 readonly onRetry?: () => void;
packages/plugins/mcp/src/react/McpSignInButton.tsx+2−2
@@ -28,8 +28,8 @@ import type { McpAuthMethod } from "../sdk/types"; · 27 unmodified lines
28// a non-org host like local), never read from an ambient owner.28// a non-org host like local), never read from an ambient owner.
29// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
30 30
31export default function McpSignInButton(props: { sourceId: string; owner?: Owner }) {31export default function McpSignInButton(props: { integrationId: string; owner?: Owner }) {
32 const slug = IntegrationSlug.make(props.sourceId);32 const slug = IntegrationSlug.make(props.integrationId);
33 const targetOwner: Owner = props.owner ?? "org";33 const targetOwner: Owner = props.owner ?? "org";
34 const serverResult = useAtomValue(mcpServerAtom(slug));34 const serverResult = useAtomValue(mcpServerAtom(slug));
35 const connectionsResult = useAtomValue(connectionsAllAtom);35 const connectionsResult = useAtomValue(connectionsAllAtom);
packages/plugins/mcp/src/react/index.ts+2−2
@@ -1,5 +1,5 @@
1export { mcpIntegrationPlugin, createMcpIntegrationPlugin } from "./source-plugin";1export { mcpIntegrationPlugin, createMcpIntegrationPlugin } from "./integration-plugin";
2export type { McpIntegrationPluginOptions } from "./source-plugin";2export type { McpIntegrationPluginOptions } from "./integration-plugin";
3export { McpClient } from "./client";3export { McpClient } from "./client";
4export {4export {
5 probeMcpEndpoint,5 probeMcpEndpoint,
packages/plugins/mcp/src/react/integration-plugin.tsxrenamed from packages/plugins/mcp/src/react/source-plugin.tsx+5−5
@@ -2,11 +2,11 @@ import { lazy, type ComponentProps, type ComponentType } from "react"; · 1 unmodified line
2import type { IntegrationPlugin } from "@executor-js/sdk/client";2import type { IntegrationPlugin } from "@executor-js/sdk/client";
3import { mcpPresets } from "../sdk/presets";3import { mcpPresets } from "../sdk/presets";
4 4
5const importAdd = () => import("./AddMcpSource");5const importAdd = () => import("./AddMcpIntegration");
6const importEditSheet = () => import("./EditMcpSource");6const importEditSheet = () => import("./EditMcpIntegration");
7const importAccounts = () => import("./McpAccountsPanel");7const importAccounts = () => import("./McpAccountsPanel");
8 8
9const LazyAddMcpSource = lazy(importAdd);9const LazyAddMcpIntegration = lazy(importAdd);
10const LazyEditMcpSheet = lazy(importEditSheet);10const LazyEditMcpSheet = lazy(importEditSheet);
11const LazyMcpAccountsPanel = lazy(importAccounts);11const LazyMcpAccountsPanel = lazy(importAccounts);
12 12
@@ -14,7 +14,7 @@ type AddProps = ComponentProps<IntegrationPlugin["add"]>; · 1 unmodified line
14 14
15export interface McpIntegrationPluginOptions {15export interface McpIntegrationPluginOptions {
16 /**16 /**
17 * Enable the stdio transport in the add-source UI (tab + presets).17 * Enable the stdio transport in the add-integration UI (tab + presets).
18 *18 *
19 * Off by default — stdio is a high-risk transport on any server deployment19 * Off by default — stdio is a high-risk transport on any server deployment
20 * (see `dangerouslyAllowStdioMCP` on the server-side plugin). Only enable in20 * (see `dangerouslyAllowStdioMCP` on the server-side plugin). Only enable in
@@ -29,7 +29,7 @@ export const createMcpIntegrationPlugin = ( · 8 unmodified lines
29 const allowStdio = options?.allowStdio ?? false;29 const allowStdio = options?.allowStdio ?? false;
30 30
31 const AddWithFlag: ComponentType<AddProps> = (props) => (31 const AddWithFlag: ComponentType<AddProps> = (props) => (
32 <LazyAddMcpSource {...props} allowStdio={allowStdio} />32 <LazyAddMcpIntegration {...props} allowStdio={allowStdio} />
33 );33 );
34 34
35 const presets = allowStdio35 const presets = allowStdio
packages/plugins/mcp/src/react/plugin-client.tsx+2−2
@@ -12,12 +12,12 @@ · 11 unmodified lines
12 12
13import { defineClientPlugin } from "@executor-js/sdk/client";13import { defineClientPlugin } from "@executor-js/sdk/client";
14 14
15import { createMcpIntegrationPlugin } from "./source-plugin";15import { createMcpIntegrationPlugin } from "./integration-plugin";
16 16
17export interface McpClientConfig {17export interface McpClientConfig {
18 /**18 /**
19 * Mirrors `dangerouslyAllowStdioMCP` on the server-side plugin. When19 * Mirrors `dangerouslyAllowStdioMCP` on the server-side plugin. When
20 * false, the AddMcpSource UI hides the stdio tab and stdio presets.20 * false, the AddMcpIntegration UI hides the stdio tab and stdio presets.
21 * Defaults to false — same default as the server flag.21 * Defaults to false — same default as the server flag.
22 */22 */
23 readonly allowStdio?: boolean;23 readonly allowStdio?: boolean;
packages/plugins/mcp/src/sdk/plugin.test.ts+1−1
@@ -624,7 +624,7 @@ describe("mcpPlugin", () => { · 623 unmodified lines
624 retryable: false,624 retryable: false,
625 details: {625 details: {
626 category: "authentication",626 category: "authentication",
627 source: { id: slug },627 integration: { id: slug },
628 credential: { kind: "upstream", label: "main" },628 credential: { kind: "upstream", label: "main" },
629 upstream: { status },629 upstream: { status },
630 },630 },
packages/plugins/mcp/src/sdk/plugin.ts+5−5
@@ -299,7 +299,7 @@ const mcpInvocationAuthFailure = (input: { · 298 unmodified lines
299 input.status === 403299 input.status === 403
300 ? `MCP server rejected connection "${input.connection}" with HTTP 403. The credential may lack access or required scope; re-authenticate or update the connection before retrying this tool.`300 ? `MCP server rejected connection "${input.connection}" with HTTP 403. The credential may lack access or required scope; re-authenticate or update the connection before retrying this tool.`
301 : `MCP server rejected connection "${input.connection}" with HTTP 401. Re-authenticate or update the connection before retrying this tool.`,301 : `MCP server rejected connection "${input.connection}" with HTTP 401. Re-authenticate or update the connection before retrying this tool.`,
302 source: { id: input.integration },302 integration: { id: input.integration },
303 credential: { kind: "upstream", label: input.connection },303 credential: { kind: "upstream", label: input.connection },
304 status: input.status,304 status: input.status,
305 upstream: { status: input.status },305 upstream: { status: input.status },
@@ -312,7 +312,7 @@ const mcpInvocationOAuthReauthFailure = (input: { · 6 unmodified lines
312 authToolFailure({312 authToolFailure({
313 code: "oauth_reauth_required",313 code: "oauth_reauth_required",
314 message: `OAuth connection "${input.connection}" requires reauthorization before retrying this MCP tool.`,314 message: `OAuth connection "${input.connection}" requires reauthorization before retrying this MCP tool.`,
315 source: { id: input.integration },315 integration: { id: input.integration },
316 credential: { kind: "oauth", label: input.connection },316 credential: { kind: "oauth", label: input.connection },
317 });317 });
318 318
@@ -1244,7 +1244,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 925 unmodified lines
1244 return authToolFailure({1244 return authToolFailure({
1245 code: "connection_value_missing",1245 code: "connection_value_missing",
1246 message: `Connection has no resolvable credential value for input(s): ${missing.join(", ")}. Re-create the connection with the required value(s).`,1246 message: `Connection has no resolvable credential value for input(s): ${missing.join(", ")}. Re-create the connection with the required value(s).`,
1247 source: { id: String(credential.integration) },1247 integration: { id: String(credential.integration) },
1248 credential: { kind: "upstream", label: String(credential.connection) },1248 credential: { kind: "upstream", label: String(credential.connection) },
1249 });1249 });
1250 }1250 }
@@ -1335,7 +1335,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 84 unmodified lines
1335 authToolFailure({1335 authToolFailure({
1336 code: "connection_rejected",1336 code: "connection_rejected",
1337 message: error.message,1337 message: error.message,
1338 source: { id: String(credential.integration) },1338 integration: { id: String(credential.integration) },
1339 credential: { kind: "upstream", label: String(credential.connection) },1339 credential: { kind: "upstream", label: String(credential.connection) },
1340 }),1340 }),
1341 );1341 );
@@ -1518,7 +1518,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 176 unmodified lines
1518 }),1518 }),
1519 },1519 },
1520 1520
1521 staticSources: (self) => [1521 staticIntegrations: (self) => [
1522 {1522 {
1523 id: "mcp",1523 id: "mcp",
1524 kind: "executor",1524 kind: "executor",
packages/plugins/mcp/src/sdk/probe-shape-real-servers.live.test.ts+1−1
@@ -151,7 +151,7 @@ const live = process.env.MCP_PROBE_LIVE === "1"; · 150 unmodified lines
151// Run the full probe path (the function the React UI calls). The result151// Run the full probe path (the function the React UI calls). The result
152// surfaces `requiresOAuth` / `supportsDynamicRegistration`, which is what152// surfaces `requiresOAuth` / `supportsDynamicRegistration`, which is what
153// drives the OAuth popup vs. credentials-editor branches in153// drives the OAuth popup vs. credentials-editor branches in
154// AddMcpSource. For OAuth-protected servers we want this to be `true`;154// AddMcpIntegration. For OAuth-protected servers we want this to be `true`;
155// for API-key MCPs (Cubic) it should fail with the auth-required155// for API-key MCPs (Cubic) it should fail with the auth-required
156// message; for unauth public servers (Hugging Face, etc.) it should156// message; for unauth public servers (Hugging Face, etc.) it should
157// succeed with `requiresOAuth: false` and a tool count.157// succeed with `requiresOAuth: false` and a tool count.
packages/plugins/onepassword/src/sdk/plugin.ts+1−1
@@ -336,7 +336,7 @@ export const onepasswordPlugin = definePlugin((options?: OnePasswordPluginOption · 335 unmodified lines
336 336
337 extension: (ctx) => makeOnePasswordExtension(ctx, timeoutMs, preferSdk),337 extension: (ctx) => makeOnePasswordExtension(ctx, timeoutMs, preferSdk),
338 338
339 staticSources: (self) => [339 staticIntegrations: (self) => [
340 {340 {
341 id: "onepassword",341 id: "onepassword",
342 kind: "executor",342 kind: "executor",
packages/plugins/openapi/README.md+1−1
@@ -35,7 +35,7 @@ const result = await executor.tools.invoke("petstore.listPets", {}); · 34 unmodified lines
35 35
36## Secret-backed auth headers36## Secret-backed auth headers
37 37
38Wire API keys or bearer tokens through the executor's secret store — never hard-code them in source configs:38Wire API keys or bearer tokens through the executor's secret store — never hard-code them in integration configs:
39 39
40```ts40```ts
41import { createExecutor } from "@executor-js/sdk";41import { createExecutor } from "@executor-js/sdk";
packages/plugins/openapi/src/api/group.ts+1−1
@@ -134,7 +134,7 @@ const IntegrationView = Schema.Struct({ · 133 unmodified lines
134// deliberately NOT served: it's a multi-MB build artifact in the plugin blob134// deliberately NOT served: it's a multi-MB build artifact in the plugin blob
135// store, and no client reads it (the configure UI only touches the template).135// store, and no client reads it (the configure UI only touches the template).
136const OpenApiConfigView = Schema.Struct({136const OpenApiConfigView = Schema.Struct({
137 sourceUrl: Schema.optional(Schema.String),137 specUrl: Schema.optional(Schema.String),
138 baseUrl: Schema.optional(Schema.String),138 baseUrl: Schema.optional(Schema.String),
139 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),139 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
140 queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),140 queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
packages/plugins/openapi/src/api/handlers.ts+1−1
@@ -96,7 +96,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 95 unmodified lines
96 const config = yield* ext.getConfig(params.slug);96 const config = yield* ext.getConfig(params.slug);
97 return config97 return config
98 ? {98 ? {
99 sourceUrl: config.sourceUrl,99 specUrl: config.specUrl,
100 baseUrl: config.baseUrl,100 baseUrl: config.baseUrl,
101 headers: config.headers ? { ...config.headers } : undefined,101 headers: config.headers ? { ...config.headers } : undefined,
102 queryParams: config.queryParams ? { ...config.queryParams } : undefined,102 queryParams: config.queryParams ? { ...config.queryParams } : undefined,
packages/plugins/openapi/src/providers/google/config.ts+1−1
@@ -5,7 +5,7 @@ import type { Authentication } from "../../sdk/types"; · 4 unmodified lines
5 5
6export const GoogleIntegrationConfigSchema = Schema.Struct({6export const GoogleIntegrationConfigSchema = Schema.Struct({
7 specHash: Schema.optional(Schema.String),7 specHash: Schema.optional(Schema.String),
8 sourceUrl: Schema.optional(Schema.String),8 specUrl: Schema.optional(Schema.String),
9 googleDiscoveryUrls: Schema.optional(Schema.Array(Schema.String)),9 googleDiscoveryUrls: Schema.optional(Schema.Array(Schema.String)),
10 baseUrl: Schema.optional(Schema.String),10 baseUrl: Schema.optional(Schema.String),
11 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),11 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
packages/plugins/openapi/src/providers/google/discovery.test.ts+1−1
@@ -723,7 +723,7 @@ it.effect( · 722 unmodified lines
723 }),723 }),
724);724);
725 725
726it.effect("bundles Google Discovery documents into one Google OpenAPI source", () =>726it.effect("bundles Google Discovery documents into one Google OpenAPI integration", () =>
727 Effect.gen(function* () {727 Effect.gen(function* () {
728 const result = yield* convertGoogleDiscoveryBundleToOpenApi({728 const result = yield* convertGoogleDiscoveryBundleToOpenApi({
729 documents: [729 documents: [
packages/plugins/openapi/src/providers/google/openapi-ownership-migration.test.ts+1−1
@@ -180,7 +180,7 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 179 unmodified lines
180 pluginId: "openapi",180 pluginId: "openapi",
181 config: {181 config: {
182 specHash: "stripehash",182 specHash: "stripehash",
183 sourceUrl: "https://stripe.example/openapi.json",183 specUrl: "https://stripe.example/openapi.json",
184 },184 },
185 }),185 }),
186 );186 );
packages/plugins/openapi/src/providers/google/presets.ts+1−1
@@ -33,7 +33,7 @@ export const GOOGLE_PHOTOS_ICON = · 32 unmodified lines
33export const googleOpenApiBundlePreset: GooglePreset = {33export const googleOpenApiBundlePreset: GooglePreset = {
34 id: GOOGLE_BUNDLE_PRESET_ID,34 id: GOOGLE_BUNDLE_PRESET_ID,
35 name: "Google",35 name: "Google",
36 summary: "Bundle Gmail, Calendar, Drive, Docs, and other Google APIs into one source.",36 summary: "Bundle Gmail, Calendar, Drive, Docs, and other Google APIs into one integration.",
37 icon: GOOGLE_G,37 icon: GOOGLE_G,
38 featured: true,38 featured: true,
39};39};
packages/plugins/openapi/src/providers/google/spec-format-adapter.ts+1−1
@@ -75,7 +75,7 @@ export const googleDiscoveryAdapter: SpecFormatAdapter = { · 74 unmodified lines
75 );75 );
76 return {76 return {
77 specText: conversion.specText,77 specText: conversion.specText,
78 sourceUrl: documents[0]?.discoveryUrl,78 specUrl: documents[0]?.discoveryUrl,
79 baseUrl: conversion.baseUrl,79 baseUrl: conversion.baseUrl,
80 authenticationTemplate: conversion.authenticationTemplate,80 authenticationTemplate: conversion.authenticationTemplate,
81 document,81 document,
packages/plugins/openapi/src/providers/microsoft/graph.ts+2−2
@@ -86,7 +86,7 @@ export type MicrosoftGraphIntegrationConfig = OpenApiIntegrationConfig & { · 85 unmodified lines
86 86
87const MicrosoftGraphIntegrationConfigSchema = Schema.Struct({87const MicrosoftGraphIntegrationConfigSchema = Schema.Struct({
88 specHash: Schema.optional(Schema.String),88 specHash: Schema.optional(Schema.String),
89 sourceUrl: Schema.optional(Schema.String),89 specUrl: Schema.optional(Schema.String),
90 baseUrl: Schema.optional(Schema.String),90 baseUrl: Schema.optional(Schema.String),
91 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),91 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
92 queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),92 queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -298,7 +298,7 @@ const validateSelectionUrls = ( · 205 unmodified lines
298 const specUrl = normalizeMicrosoftGraphSpecUrl(selection.specUrl, policy);298 const specUrl = normalizeMicrosoftGraphSpecUrl(selection.specUrl, policy);
299 if (!specUrl) {299 if (!specUrl) {
300 return yield* new OpenApiParseError({300 return yield* new OpenApiParseError({
301 message: "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI source",301 message: "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI spec",
302 });302 });
303 }303 }
304 const baseUrl = normalizeMicrosoftGraphBaseUrl(selection.baseUrl, policy);304 const baseUrl = normalizeMicrosoftGraphBaseUrl(selection.baseUrl, policy);
packages/plugins/openapi/src/providers/microsoft/presets.ts+1−1
@@ -59,7 +59,7 @@ export const MICROSOFT_GRAPH_PRESET_ID = "microsoft"; · 58 unmodified lines
59export const microsoftGraphPreset: MicrosoftGraphPreset = {59export const microsoftGraphPreset: MicrosoftGraphPreset = {
60 id: MICROSOFT_GRAPH_PRESET_ID,60 id: MICROSOFT_GRAPH_PRESET_ID,
61 name: "Microsoft Graph",61 name: "Microsoft Graph",
62 summary: "Bundle Microsoft 365 workloads into one Graph source and one OAuth consent.",62 summary: "Bundle Microsoft 365 workloads into one Graph integration and one OAuth consent.",
63 icon: MICROSOFT_ICON,63 icon: MICROSOFT_ICON,
64 featured: true,64 featured: true,
65};65};
packages/plugins/openapi/src/providers/microsoft/spec-format-adapter.ts+1−1
@@ -28,7 +28,7 @@ export const microsoftGraphAdapter: SpecFormatAdapter = { · 27 unmodified lines
28 ).pipe(28 ).pipe(
29 Effect.map((graphSpec) => ({29 Effect.map((graphSpec) => ({
30 specText: graphSpec.specText,30 specText: graphSpec.specText,
31 sourceUrl: graphSpec.specUrl,31 specUrl: graphSpec.specUrl,
32 baseUrl: graphSpec.baseUrl,32 baseUrl: graphSpec.baseUrl,
33 authenticationTemplate: graphSpec.authenticationTemplate,33 authenticationTemplate: graphSpec.authenticationTemplate,
34 // Stream the full Graph source straight to persisted bindings. This is34 // Stream the full Graph source straight to persisted bindings. This is
packages/plugins/openapi/src/react/AddOpenApiIntegration.test.tsrenamed from packages/plugins/openapi/src/react/AddOpenApiSource.test.ts+1−1
@@ -5,7 +5,7 @@ import { · 4 unmodified lines
5 AddOpenApiHealthCheckSection,5 AddOpenApiHealthCheckSection,
6 baseUrlFromSpecInput,6 baseUrlFromSpecInput,
7 openApiPreviewFailureMessage,7 openApiPreviewFailureMessage,
8} from "./AddOpenApiSource";8} from "./AddOpenApiIntegration";
9 9
10const visibleText = (node: React.ReactNode): string => {10const visibleText = (node: React.ReactNode): string => {
11 if (node === null || node === undefined || typeof node === "boolean") return "";11 if (node === null || node === undefined || typeof node === "boolean") return "";
packages/plugins/openapi/src/react/AddOpenApiIntegration.tsxrenamed from packages/plugins/openapi/src/react/AddOpenApiSource.tsx+16−10
@@ -44,7 +44,7 @@ import { · 43 unmodified lines
44 openApiWireAuthInput,44 openApiWireAuthInput,
45} from "./auth-method-config";45} from "./auth-method-config";
46import { addOpenApiSpec, previewOpenApiSpec } from "./atoms";46import { addOpenApiSpec, previewOpenApiSpec } from "./atoms";
47import { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields";47import { OpenApiIntegrationDetailsFields } from "./OpenApiIntegrationDetailsFields";
48import type { SpecPreviewSummary } from "../sdk/preview";48import type { SpecPreviewSummary } from "../sdk/preview";
49import { type Authentication } from "../sdk/types";49import { type Authentication } from "../sdk/types";
50import { resolveServerUrl } from "../sdk/openapi-utils";50import { resolveServerUrl } from "../sdk/openapi-utils";
@@ -142,7 +142,7 @@ export function AddOpenApiHealthCheckSection(props: { · 91 unmodified lines
142// flow no longer creates a connection.142// flow no longer creates a connection.
143// ---------------------------------------------------------------------------143// ---------------------------------------------------------------------------
144 144
145export default function AddOpenApiSource(props: {145export default function AddOpenApiIntegration(props: {
146 onComplete: (slug?: string) => void;146 onComplete: (slug?: string) => void;
147 onCancel: () => void;147 onCancel: () => void;
148 initialUrl?: string;148 initialUrl?: string;
@@ -222,13 +222,15 @@ export default function AddOpenApiSource(props: { · 73 unmodified lines
222 )?.icon ?? null;222 )?.icon ?? null;
223 223
224 const resolvedBaseUrl = baseUrl.trim();224 const resolvedBaseUrl = baseUrl.trim();
225 const resolvedSourceId =225 const resolvedIntegrationId =
226 initialPreset?.defaultSlug ||226 initialPreset?.defaultSlug ||
227 slugifyNamespace(identity.namespace) ||227 slugifyNamespace(identity.namespace) ||
228 (preview ? Option.getOrElse(preview.title, () => "openapi") : "openapi");228 (preview ? Option.getOrElse(preview.title, () => "openapi") : "openapi");
229 const resolvedDisplayName =229 const resolvedDisplayName =
230 identity.name.trim() ||230 identity.name.trim() ||
231 (preview ? Option.getOrElse(preview.title, () => resolvedSourceId) : resolvedSourceId);231 (preview
232 ? Option.getOrElse(preview.title, () => resolvedIntegrationId)
233 : resolvedIntegrationId);
232 const resolvedDescription =234 const resolvedDescription =
233 descriptionDraft ?? (preview ? Option.getOrElse(preview.description, () => "") : "");235 descriptionDraft ?? (preview ? Option.getOrElse(preview.description, () => "") : "");
234 236
@@ -324,7 +326,7 @@ export default function AddOpenApiSource(props: { · 89 unmodified lines
324 // Pre-empt the API's `IntegrationAlreadyExistsError`: adding an integration326 // Pre-empt the API's `IntegrationAlreadyExistsError`: adding an integration
325 // whose slug already exists clobbers the existing one's connections/policies,327 // whose slug already exists clobbers the existing one's connections/policies,
326 // so the API blocks it. Surface that here from the tenant-scoped catalog list.328 // so the API blocks it. Surface that here from the tenant-scoped catalog list.
327 const slugAlreadyExists = useSlugAlreadyExists(resolvedSourceId);329 const slugAlreadyExists = useSlugAlreadyExists(resolvedIntegrationId);
328 330
329 // The base URL is optional when the spec declares servers (resolved per call);331 // The base URL is optional when the spec declares servers (resolved per call);
330 // required only when it doesn't. A drafted health check must have its required332 // required only when it doesn't. A drafted health check must have its required
@@ -364,7 +366,7 @@ export default function AddOpenApiSource(props: { · 33 unmodified lines
364 const exit = await doAdd({366 const exit = await doAdd({
365 payload: {367 payload: {
366 spec: specInputForAdd(specUrl),368 spec: specInputForAdd(specUrl),
367 slug: resolvedSourceId,369 slug: resolvedIntegrationId,
368 name: resolvedDisplayName,370 name: resolvedDisplayName,
369 ...(resolvedDescription.trim().length > 0371 ...(resolvedDescription.trim().length > 0
370 ? { description: resolvedDescription.trim() }372 ? { description: resolvedDescription.trim() }
@@ -383,14 +385,16 @@ export default function AddOpenApiSource(props: { · 12 unmodified lines
383 reactivityKeys: integrationWriteKeys,385 reactivityKeys: integrationWriteKeys,
384 });386 });
385 if (Exit.isFailure(exit)) {387 if (Exit.isFailure(exit)) {
386 setAddError(addIntegrationErrorMessage(exit, resolvedSourceId, "Failed to add integration"));388 setAddError(
389 addIntegrationErrorMessage(exit, resolvedIntegrationId, "Failed to add integration"),
390 );
387 return null;391 return null;
388 }392 }
389 return exit.value.slug;393 return exit.value.slug;
390 }, [394 }, [
391 specUrl,395 specUrl,
392 doAdd,396 doAdd,
393 resolvedSourceId,397 resolvedIntegrationId,
394 resolvedDisplayName,398 resolvedDisplayName,
395 resolvedDescription,399 resolvedDescription,
396 resolvedBaseUrl,400 resolvedBaseUrl,
@@ -499,7 +503,7 @@ export default function AddOpenApiSource(props: { · 102 unmodified lines
499 ) : null}503 ) : null}
500 504
501 {preview ? (505 {preview ? (
502 <OpenApiSourceDetailsFields506 <OpenApiIntegrationDetailsFields
503 title={Option.getOrElse(preview.title, () => "API")}507 title={Option.getOrElse(preview.title, () => "API")}
504 subtitle={`${Option.getOrElse(preview.version, () => "")}${508 subtitle={`${Option.getOrElse(preview.version, () => "")}${
505 Option.isSome(preview.version) ? " · " : ""509 Option.isSome(preview.version) ? " · " : ""
@@ -558,7 +562,9 @@ export default function AddOpenApiSource(props: { · 52 unmodified lines
558 />562 />
559 ) : null}563 ) : null}
560 564
561 {preview && slugAlreadyExists && !adding && <SlugCollisionAlert slug={resolvedSourceId} />}565 {preview && slugAlreadyExists && !adding && (
566 <SlugCollisionAlert slug={resolvedIntegrationId} />
567 )}
562 568
563 {addError && <FormErrorAlert message={addError} />}569 {addError && <FormErrorAlert message={addError} />}
564 570
packages/plugins/openapi/src/react/OpenApiAccountsPanel.tsx+3−3
@@ -51,12 +51,12 @@ const NO_AUTH_METHOD: AuthMethod = { · 50 unmodified lines
51// ---------------------------------------------------------------------------51// ---------------------------------------------------------------------------
52 52
53export default function OpenApiAccountsPanel(props: {53export default function OpenApiAccountsPanel(props: {
54 readonly sourceId: string;54 readonly integrationId: string;
55 readonly integrationName: string;55 readonly integrationName: string;
56 readonly accountHandoff?: IntegrationAccountHandoff | null;56 readonly accountHandoff?: IntegrationAccountHandoff | null;
57}) {57}) {
58 const { sourceId, integrationName, accountHandoff } = props;58 const { integrationId, integrationName, accountHandoff } = props;
59 const slug = IntegrationSlug.make(sourceId);59 const slug = IntegrationSlug.make(integrationId);
60 const configResult = useAtomValue(openApiConfigAtom(slug));60 const configResult = useAtomValue(openApiConfigAtom(slug));
61 const doConfigure = useAtomSet(openapiConfigure, { mode: "promiseExit" });61 const doConfigure = useAtomSet(openapiConfigure, { mode: "promiseExit" });
62 62
packages/plugins/openapi/src/react/OpenApiIntegrationDetailsFields.tsxrenamed from packages/plugins/openapi/src/react/OpenApiSourceDetailsFields.tsx+1−1
@@ -23,7 +23,7 @@ import { · 22 unmodified lines
23 * URL; pasted document content gets a multi-line editor instead. */23 * URL; pasted document content gets a multi-line editor instead. */
24const isUrlInput = (value: string): boolean => URL.canParse(value.trim());24const isUrlInput = (value: string): boolean => URL.canParse(value.trim());
25 25
26export function OpenApiSourceDetailsFields(props: {26export function OpenApiIntegrationDetailsFields(props: {
27 readonly title: string;27 readonly title: string;
28 readonly subtitle?: string;28 readonly subtitle?: string;
29 readonly identity: IntegrationIdentity;29 readonly identity: IntegrationIdentity;
packages/plugins/openapi/src/react/UpdateSpecSection.tsx+4−4
@@ -42,7 +42,7 @@ const outcomeSummary = (outcome: UpdateOutcome): string => { · 41 unmodified lines
42};42};
43 43
44export default function UpdateSpecSection(props: EditSheetSectionProps) {44export default function UpdateSpecSection(props: EditSheetSectionProps) {
45 const slug = IntegrationSlug.make(props.sourceId);45 const slug = IntegrationSlug.make(props.integrationId);
46 const configResult = useAtomValue(openApiConfigAtom(slug));46 const configResult = useAtomValue(openApiConfigAtom(slug));
47 const doUpdate = useAtomSet(updateOpenApiSpec, { mode: "promiseExit" });47 const doUpdate = useAtomSet(updateOpenApiSpec, { mode: "promiseExit" });
48 const [refetchStaged, setRefetchStaged] = useState(false);48 const [refetchStaged, setRefetchStaged] = useState(false);
@@ -52,7 +52,7 @@ export default function UpdateSpecSection(props: EditSheetSectionProps) { · 3 unmodified lines
52 const config =52 const config =
53 AsyncResult.isSuccess(configResult) && configResult.value ? configResult.value : null;53 AsyncResult.isSuccess(configResult) && configResult.value ? configResult.value : null;
54 54
55 const sourceUrl = config?.sourceUrl;55 const specUrl = config?.specUrl;
56 56
57 // The staged apply, rebuilt whenever the staged inputs change. Reported to57 // The staged apply, rebuilt whenever the staged inputs change. Reported to
58 // the sheet through a ref-stable callback so Save can run it.58 // the sheet through a ref-stable callback so Save can run it.
@@ -95,7 +95,7 @@ export default function UpdateSpecSection(props: EditSheetSectionProps) { · 36 unmodified lines
95 </p>95 </p>
96 </div>96 </div>
97 97
98 {sourceUrl ? (98 {specUrl ? (
99 <div className="space-y-2">99 <div className="space-y-2">
100 <Label className="flex items-start gap-2 text-sm font-normal">100 <Label className="flex items-start gap-2 text-sm font-normal">
101 <Checkbox101 <Checkbox
@@ -107,7 +107,7 @@ export default function UpdateSpecSection(props: EditSheetSectionProps) { · 5 unmodified lines
107 <span className="space-y-0.5">107 <span className="space-y-0.5">
108 <span className="block">Re-fetch the spec on save</span>108 <span className="block">Re-fetch the spec on save</span>
109 <span className="block break-all font-mono text-xs text-muted-foreground">109 <span className="block break-all font-mono text-xs text-muted-foreground">
110 {sourceUrl}110 {specUrl}
111 </span>111 </span>
112 </span>112 </span>
113 </Label>113 </Label>
packages/plugins/openapi/src/react/index.ts+2−2
@@ -1,6 +1,6 @@
1export { openApiIntegrationPlugin } from "./source-plugin";1export { openApiIntegrationPlugin } from "./integration-plugin";
2export { OpenApiClient } from "./client";2export { OpenApiClient } from "./client";
3export { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields";3export { OpenApiIntegrationDetailsFields } from "./OpenApiIntegrationDetailsFields";
4export {4export {
5 authenticationFromEditorValue,5 authenticationFromEditorValue,
6 authMethodsFromConfig,6 authMethodsFromConfig,
packages/plugins/openapi/src/react/integration-plugin.tsrenamed from packages/plugins/openapi/src/react/source-plugin.ts+1−1
@@ -2,7 +2,7 @@ import { lazy } from "react"; · 1 unmodified line
2import type { IntegrationPlugin, IntegrationPreset } from "@executor-js/sdk/client";2import type { IntegrationPlugin, IntegrationPreset } from "@executor-js/sdk/client";
3import { openApiPresets } from "../sdk/presets";3import { openApiPresets } from "../sdk/presets";
4 4
5const importAdd = () => import("./AddOpenApiSource");5const importAdd = () => import("./AddOpenApiIntegration");
6const importEditSheet = () => import("./UpdateSpecSection");6const importEditSheet = () => import("./UpdateSpecSection");
7const importAccounts = () => import("./OpenApiAccountsPanel");7const importAccounts = () => import("./OpenApiAccountsPanel");
8 8
packages/plugins/openapi/src/react/plugin-client.tsx+2−2
@@ -3,7 +3,7 @@ · 2 unmodified lines
3//3//
4// Aggregates the openapi plugin's frontend contributions into a single4// Aggregates the openapi plugin's frontend contributions into a single
5// declarative spec. The host's Vite plugin reads this via5// declarative spec. The host's Vite plugin reads this via
6// `virtual:executor/plugins-client`, so the host's sources page derives6// `virtual:executor/plugins-client`, so the host's integrations page derives
7// the openapi entry from here without a direct `*/react` import.7// the openapi entry from here without a direct `*/react` import.
8//8//
9// The richer add/edit/summary components still live in `./react`; this9// The richer add/edit/summary components still live in `./react`; this
@@ -12,7 +12,7 @@ · 2 unmodified lines
12 12
13import { defineClientPlugin } from "@executor-js/sdk/client";13import { defineClientPlugin } from "@executor-js/sdk/client";
14 14
15import { createOpenApiIntegrationPlugin, type OpenApiClientConfig } from "./source-plugin";15import { createOpenApiIntegrationPlugin, type OpenApiClientConfig } from "./integration-plugin";
16 16
17export default function createOpenApiClientPlugin(config?: OpenApiClientConfig) {17export default function createOpenApiClientPlugin(config?: OpenApiClientConfig) {
18 return defineClientPlugin({18 return defineClientPlugin({
packages/plugins/openapi/src/sdk/backing.ts+1−1
@@ -141,7 +141,7 @@ const openApiAuthToolFailure = (failure: { · 140 unmodified lines
141 authToolFailure({141 authToolFailure({
142 code: failure.code as Parameters<typeof authToolFailure>[0]["code"],142 code: failure.code as Parameters<typeof authToolFailure>[0]["code"],
143 message: failure.message,143 message: failure.message,
144 source: { id: failure.integration, scope: failure.owner },144 integration: { id: failure.integration, scope: failure.owner },
145 credential: {145 credential: {
146 kind: failure.credentialKind,146 kind: failure.credentialKind,
147 ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),147 ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),
packages/plugins/openapi/src/sdk/config.ts+1−1
@@ -44,7 +44,7 @@ export const OpenApiIntegrationConfigSchema = Schema.Struct({ · 43 unmodified lines
44 * blob (`spec/<hash>` in the plugin blob store). */44 * blob (`spec/<hash>` in the plugin blob store). */
45 specHash: Schema.optional(Schema.String),45 specHash: Schema.optional(Schema.String),
46 /** Origin URL the spec was fetched from, when known. Enables refresh. */46 /** Origin URL the spec was fetched from, when known. Enables refresh. */
47 sourceUrl: Schema.optional(Schema.String),47 specUrl: Schema.optional(Schema.String),
48 /** Optional base URL override. */48 /** Optional base URL override. */
49 baseUrl: Schema.optional(Schema.String),49 baseUrl: Schema.optional(Schema.String),
50 /** Static headers applied to every request (no secret material). */50 /** Static headers applied to every request (no secret material). */
packages/plugins/openapi/src/sdk/configure.test.ts+2−2
@@ -31,7 +31,7 @@ import { openApiPlugin } from "./plugin"; · 30 unmodified lines
31import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types";31import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types";
32import { variable } from "@executor-js/sdk/http-auth";32import { variable } from "@executor-js/sdk/http-auth";
33import {33import {
34 makeOpenApiHttpApiTestSourceConfig,34 makeOpenApiHttpApiTestIntegrationConfig,
35 serveOpenApiHttpApiTestServer,35 serveOpenApiHttpApiTestServer,
36 unwrapInvocation,36 unwrapInvocation,
37} from "../testing";37} from "../testing";
@@ -66,7 +66,7 @@ const servePluginTestApi = () => · 28 unmodified lines
66 serveOpenApiHttpApiTestServer({ api: TestApi, handlersLayer: EchoGroupLive });66 serveOpenApiHttpApiTestServer({ api: TestApi, handlersLayer: EchoGroupLive });
67 67
68const specText = () => {68const specText = () => {
69 const spec = makeOpenApiHttpApiTestSourceConfig(TestApi, {}).spec;69 const spec = makeOpenApiHttpApiTestIntegrationConfig(TestApi, {}).spec;
70 if (spec.kind === "blob") return spec.value;70 if (spec.kind === "blob") return spec.value;
71 return spec.url;71 return spec.url;
72};72};
packages/plugins/openapi/src/sdk/describe-auth-methods.test.ts+3−3
@@ -106,20 +106,20 @@ describe("describeOpenApiAuthMethods", () => { · 105 unmodified lines
106 ...recordWith([]),106 ...recordWith([]),
107 config: {107 config: {
108 spec: "{}",108 spec: "{}",
109 sourceUrl: "https://api.example.com/openapi.json",109 specUrl: "https://api.example.com/openapi.json",
110 baseUrl: "https://api.example.com",110 baseUrl: "https://api.example.com",
111 } as IntegrationConfig,111 } as IntegrationConfig,
112 }),112 }),
113 ).toEqual({ url: "https://api.example.com" });113 ).toEqual({ url: "https://api.example.com" });
114 });114 });
115 115
116 it("falls back to sourceUrl for display metadata", () => {116 it("falls back to specUrl for display metadata", () => {
117 expect(117 expect(
118 describeOpenApiIntegrationDisplay({118 describeOpenApiIntegrationDisplay({
119 ...recordWith([]),119 ...recordWith([]),
120 config: {120 config: {
121 spec: "{}",121 spec: "{}",
122 sourceUrl: "https://api.example.com/openapi.json",122 specUrl: "https://api.example.com/openapi.json",
123 } as IntegrationConfig,123 } as IntegrationConfig,
124 }),124 }),
125 ).toEqual({ url: "https://api.example.com/openapi.json" });125 ).toEqual({ url: "https://api.example.com/openapi.json" });
packages/plugins/openapi/src/sdk/invoke.ts+6−6
@@ -839,7 +839,7 @@ export const buildRequest = Effect.fn("OpenApi.buildRequest")(function* ( · 838 unmodified lines
839 operation: OperationBinding,839 operation: OperationBinding,
840 args: Record<string, unknown>,840 args: Record<string, unknown>,
841 resolvedHeaders: Record<string, string>,841 resolvedHeaders: Record<string, string>,
842 sourceQueryParams: Record<string, string> = {},842 integrationQueryParams: Record<string, string> = {},
843) {843) {
844 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);844 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);
845 845
@@ -851,7 +851,7 @@ export const buildRequest = Effect.fn("OpenApi.buildRequest")(function* ( · 5 unmodified lines
851 // can override it; the upstream still gets a User-Agent if nothing else sets one.851 // can override it; the upstream still gets a User-Agent if nothing else sets one.
852 request = HttpClientRequest.setHeader(request, "User-Agent", DEFAULT_USER_AGENT);852 request = HttpClientRequest.setHeader(request, "User-Agent", DEFAULT_USER_AGENT);
853 853
854 for (const [name, value] of Object.entries(sourceQueryParams)) {854 for (const [name, value] of Object.entries(integrationQueryParams)) {
855 request = HttpClientRequest.setUrlParam(request, name, value);855 request = HttpClientRequest.setUrlParam(request, name, value);
856 }856 }
857 857
@@ -998,7 +998,7 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 140 unmodified lines
998 operation: OperationBinding,998 operation: OperationBinding,
999 args: Record<string, unknown>,999 args: Record<string, unknown>,
1000 resolvedHeaders: Record<string, string>,1000 resolvedHeaders: Record<string, string>,
1001 sourceQueryParams: Record<string, string> = {},1001 integrationQueryParams: Record<string, string> = {},
1002 options: InvokeOptions = {},1002 options: InvokeOptions = {},
1003) {1003) {
1004 const client = yield* HttpClient.HttpClient;1004 const client = yield* HttpClient.HttpClient;
@@ -1011,7 +1011,7 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 6 unmodified lines
1011 "plugin.openapi.headers.resolved_count": Object.keys(resolvedHeaders).length,1011 "plugin.openapi.headers.resolved_count": Object.keys(resolvedHeaders).length,
1012 });1012 });
1013 1013
1014 const request = yield* buildRequest(operation, args, resolvedHeaders, sourceQueryParams);1014 const request = yield* buildRequest(operation, args, resolvedHeaders, integrationQueryParams);
1015 1015
1016 const responseHeadersTimeoutMs = options.responseHeadersTimeoutMs ?? RESPONSE_HEADERS_TIMEOUT_MS;1016 const responseHeadersTimeoutMs = options.responseHeadersTimeoutMs ?? RESPONSE_HEADERS_TIMEOUT_MS;
1017 const runFork = Effect.runForkWith(yield* Effect.context<never>());1017 const runFork = Effect.runForkWith(yield* Effect.context<never>());
@@ -1161,7 +1161,7 @@ export const invokeWithLayer = ( · 143 unmodified lines
1161 args: Record<string, unknown>,1161 args: Record<string, unknown>,
1162 baseUrl: string,1162 baseUrl: string,
1163 resolvedHeaders: Record<string, string>,1163 resolvedHeaders: Record<string, string>,
1164 sourceQueryParams: Record<string, string>,1164 integrationQueryParams: Record<string, string>,
1165 httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,1165 httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,
1166 options: InvokeOptions = {},1166 options: InvokeOptions = {},
1167) => {1167) => {
@@ -1176,7 +1176,7 @@ export const invokeWithLayer = ( · 8 unmodified lines
1176 ).pipe(Layer.provide(httpClientLayer))1176 ).pipe(Layer.provide(httpClientLayer))
1177 : httpClientLayer;1177 : httpClientLayer;
1178 1178
1179 return invoke(operation, args, resolvedHeaders, sourceQueryParams, options).pipe(1179 return invoke(operation, args, resolvedHeaders, integrationQueryParams, options).pipe(
1180 Effect.provide(clientWithBaseUrl),1180 Effect.provide(clientWithBaseUrl),
1181 // `invoke` annotates http.status_code on ITS span (`OpenApi.invoke`,1181 // `invoke` annotates http.status_code on ITS span (`OpenApi.invoke`,
1182 // via Effect.fn) — annotateCurrentSpan inside it never reaches this1182 // via Effect.fn) — annotateCurrentSpan inside it never reaches this
packages/plugins/openapi/src/sdk/plugin.test.ts+2−2
@@ -41,7 +41,7 @@ import { openApiPlugin } from "./plugin"; · 40 unmodified lines
41import { type AuthenticationInput } from "./types";41import { type AuthenticationInput } from "./types";
42import {42import {
43 addOpenApiTestConnection,43 addOpenApiTestConnection,
44 makeOpenApiHttpApiTestSourceConfig,44 makeOpenApiHttpApiTestIntegrationConfig,
45 serveMutableOpenApiSpecTestServer,45 serveMutableOpenApiSpecTestServer,
46 serveOpenApiHttpApiTestServer,46 serveOpenApiHttpApiTestServer,
47 unwrapInvocation,47 unwrapInvocation,
@@ -104,7 +104,7 @@ const ItemsGroup = HttpApiGroup.make("items") · 56 unmodified lines
104const TestApi = HttpApi.make("testApi").add(ItemsGroup);104const TestApi = HttpApi.make("testApi").add(ItemsGroup);
105 105
106const testApiSpecText = () => {106const testApiSpecText = () => {
107 const spec = makeOpenApiHttpApiTestSourceConfig(TestApi, {}).spec;107 const spec = makeOpenApiHttpApiTestIntegrationConfig(TestApi, {}).spec;
108 if (spec.kind === "blob") return spec.value;108 if (spec.kind === "blob") return spec.value;
109 return spec.url;109 return spec.url;
110};110};
packages/plugins/openapi/src/sdk/plugin.ts+26−26
@@ -122,7 +122,7 @@ export interface UpdateSpecResult { · 121 unmodified lines
122 122
123export interface OpenApiUpdateSpecInput {123export interface OpenApiUpdateSpecInput {
124 /** New spec source. Omit to re-fetch from the integration's stored124 /** New spec source. Omit to re-fetch from the integration's stored
125 * `sourceUrl`. */125 * `specUrl`. */
126 readonly spec?: OpenApiSpecInput;126 readonly spec?: OpenApiSpecInput;
127}127}
128 128
@@ -273,7 +273,7 @@ const AuthenticationSchema = Schema.Union([ · 144 unmodified lines
273 ApiKeyAuthTemplate,273 ApiKeyAuthTemplate,
274]);274]);
275 275
276const AddSourceInputSchema = Schema.Struct({276const AddIntegrationInputSchema = Schema.Struct({
277 spec: OpenApiSpecInputSchema,277 spec: OpenApiSpecInputSchema,
278 slug: Schema.optional(Schema.String),278 slug: Schema.optional(Schema.String),
279 name: Schema.optional(Schema.String),279 name: Schema.optional(Schema.String),
@@ -287,7 +287,7 @@ const AddSourceInputSchema = Schema.Struct({ · 7 unmodified lines
287 authenticationTemplate: Schema.optional(Schema.Array(AuthenticationSchema)),287 authenticationTemplate: Schema.optional(Schema.Array(AuthenticationSchema)),
288});288});
289 289
290const AddSourceOutputSchema = Schema.Struct({290const AddIntegrationOutputSchema = Schema.Struct({
291 slug: Schema.String,291 slug: Schema.String,
292 toolCount: Schema.Number,292 toolCount: Schema.Number,
293});293});
@@ -298,11 +298,11 @@ const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1( · 4 unmodified lines
298const PreviewSpecOutputStandardSchema = Schema.toStandardSchemaV1(298const PreviewSpecOutputStandardSchema = Schema.toStandardSchemaV1(
299 Schema.toStandardJSONSchemaV1(StaticPreviewSpecOutputSchema),299 Schema.toStandardJSONSchemaV1(StaticPreviewSpecOutputSchema),
300);300);
301const AddSourceInputStandardSchema = Schema.toStandardSchemaV1(301const AddIntegrationInputStandardSchema = Schema.toStandardSchemaV1(
302 Schema.toStandardJSONSchemaV1(AddSourceInputSchema),302 Schema.toStandardJSONSchemaV1(AddIntegrationInputSchema),
303);303);
304const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1(304const AddIntegrationOutputStandardSchema = Schema.toStandardSchemaV1(
305 Schema.toStandardJSONSchemaV1(AddSourceOutputSchema),305 Schema.toStandardJSONSchemaV1(AddIntegrationOutputSchema),
306);306);
307 307
308const openApiToolFailure = (code: string, message: string, details?: unknown) =>308const openApiToolFailure = (code: string, message: string, details?: unknown) =>
@@ -378,7 +378,7 @@ const staticPreviewOutput = (preview: SpecPreview): StaticPreviewSpecOutput => ( · 69 unmodified lines
378 })),378 })),
379});379});
380 380
381const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined =>381const specInputToSpecUrl = (spec: OpenApiSpecInput): string | undefined =>
382 spec.kind === "url" ? spec.url : undefined;382 spec.kind === "url" ? spec.url : undefined;
383 383
384const OAUTH_DISCOVERED_SCHEME_NAME = "DiscoveredOAuth2";384const OAUTH_DISCOVERED_SCHEME_NAME = "DiscoveredOAuth2";
@@ -418,7 +418,7 @@ const addProbeCandidate = (candidates: string[], value: string | undefined): voi · 33 unmodified lines
418 418
419const oauthProbeCandidates = (419const oauthProbeCandidates = (
420 preview: SpecPreview,420 preview: SpecPreview,
421 sourceUrl: string | undefined,421 specUrl: string | undefined,
422 baseUrl: string | undefined,422 baseUrl: string | undefined,
423): readonly string[] => {423): readonly string[] => {
424 const candidates: string[] = [];424 const candidates: string[] = [];
@@ -429,7 +429,7 @@ const oauthProbeCandidates = ( · 4 unmodified lines
429 resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {}),429 resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {}),
430 );430 );
431 }431 }
432 addProbeCandidate(candidates, sourceUrl);432 addProbeCandidate(candidates, specUrl);
433 return candidates;433 return candidates;
434};434};
435 435
@@ -574,7 +574,7 @@ export const describeOpenApiIntegrationDisplay = ( · 138 unmodified lines
574): { readonly url?: string; readonly family?: string } => {574): { readonly url?: string; readonly family?: string } => {
575 const config = decodeOpenApiIntegrationConfig(record.config);575 const config = decodeOpenApiIntegrationConfig(record.config);
576 return {576 return {
577 url: config?.baseUrl ?? config?.sourceUrl,577 url: config?.baseUrl ?? config?.specUrl,
578 ...(config?.family ? { family: config.family } : {}),578 ...(config?.family ? { family: config.family } : {}),
579 };579 };
580};580};
@@ -624,7 +624,7 @@ export const openApiPlugin = definePlugin< · 43 unmodified lines
624 const specText = yield* resolveSpecText(config.spec.url).pipe(624 const specText = yield* resolveSpecText(config.spec.url).pipe(
625 Effect.provide(httpClientLayer),625 Effect.provide(httpClientLayer),
626 );626 );
627 return { specText, sourceUrl: config.spec.url };627 return { specText, specUrl: config.spec.url };
628 }628 }
629 return { specText: config.spec.value };629 return { specText: config.spec.value };
630 });630 });
@@ -654,13 +654,13 @@ export const openApiPlugin = definePlugin< · 23 unmodified lines
654 const enrichPreviewWithDiscoveredOAuth = (input: {654 const enrichPreviewWithDiscoveredOAuth = (input: {
655 readonly specText: string;655 readonly specText: string;
656 readonly preview: SpecPreview;656 readonly preview: SpecPreview;
657 readonly sourceUrl?: string;657 readonly specUrl?: string;
658 readonly baseUrl?: string;658 readonly baseUrl?: string;
659 }): Effect.Effect<SpecPreview, OpenApiParseError | OpenApiExtractionError> =>659 }): Effect.Effect<SpecPreview, OpenApiParseError | OpenApiExtractionError> =>
660 Effect.gen(function* () {660 Effect.gen(function* () {
661 if (input.preview.oauth2Presets.length > 0) return input.preview;661 if (input.preview.oauth2Presets.length > 0) return input.preview;
662 662
663 const candidates = oauthProbeCandidates(input.preview, input.sourceUrl, input.baseUrl);663 const candidates = oauthProbeCandidates(input.preview, input.specUrl, input.baseUrl);
664 if (candidates.length === 0) return input.preview;664 if (candidates.length === 0) return input.preview;
665 665
666 for (const candidate of candidates) {666 for (const candidate of candidates) {
@@ -741,7 +741,7 @@ export const openApiPlugin = definePlugin< · 74 unmodified lines
741 enrichPreviewWithDiscoveredOAuth({741 enrichPreviewWithDiscoveredOAuth({
742 specText: resolved.specText,742 specText: resolved.specText,
743 preview: rawPreview,743 preview: rawPreview,
744 sourceUrl: resolved.sourceUrl ?? specInputToSourceUrl(config.spec),744 specUrl: resolved.specUrl ?? specInputToSpecUrl(config.spec),
745 baseUrl: explicitBaseUrl,745 baseUrl: explicitBaseUrl,
746 }),746 }),
747 ),747 ),
@@ -772,8 +772,8 @@ export const openApiPlugin = definePlugin< · 24 unmodified lines
772 const integrationConfig: OpenApiIntegrationConfig = {772 const integrationConfig: OpenApiIntegrationConfig = {
773 ...(resolved.config ?? {}),773 ...(resolved.config ?? {}),
774 specHash,774 specHash,
775 ...((resolved.sourceUrl ?? specInputToSourceUrl(config.spec)) !== undefined775 ...((resolved.specUrl ?? specInputToSpecUrl(config.spec)) !== undefined
776 ? { sourceUrl: resolved.sourceUrl ?? specInputToSourceUrl(config.spec) }776 ? { specUrl: resolved.specUrl ?? specInputToSpecUrl(config.spec) }
777 : {}),777 : {}),
778 // baseUrl is an optional override only. The host is otherwise778 // baseUrl is an optional override only. The host is otherwise
779 // resolved per call from the operation's `servers` (extracted from779 // resolved per call from the operation's `servers` (extracted from
@@ -822,7 +822,7 @@ export const openApiPlugin = definePlugin< · 42 unmodified lines
822 resolvedSlug,822 resolvedSlug,
823 config: integrationConfig satisfies OpenApiIntegrationConfig as IntegrationConfig,823 config: integrationConfig satisfies OpenApiIntegrationConfig as IntegrationConfig,
824 canRemove: true,824 canRemove: true,
825 canRefresh: integrationConfig.sourceUrl != null,825 canRefresh: integrationConfig.specUrl != null,
826 });826 });
827 if (config.healthCheck) {827 if (config.healthCheck) {
828 yield* ctx.core.integrations.setHealthCheck(slug, config.healthCheck);828 yield* ctx.core.integrations.setHealthCheck(slug, config.healthCheck);
@@ -871,7 +871,7 @@ export const openApiPlugin = definePlugin< · 42 unmodified lines
871 // where the spec originally came from. A pasted-blob integration has871 // where the spec originally came from. A pasted-blob integration has
872 // no origin, so updating it requires a new input.872 // no origin, so updating it requires a new input.
873 const specInput: OpenApiSpecInput | null =873 const specInput: OpenApiSpecInput | null =
874 input?.spec ?? (current.sourceUrl ? { kind: "url", url: current.sourceUrl } : null);874 input?.spec ?? (current.specUrl ? { kind: "url", url: current.specUrl } : null);
875 if (specInput === null) {875 if (specInput === null) {
876 return yield* new OpenApiParseError({876 return yield* new OpenApiParseError({
877 message:877 message:
@@ -911,8 +911,8 @@ export const openApiPlugin = definePlugin< · 33 unmodified lines
911 const nextConfig: OpenApiIntegrationConfig = {911 const nextConfig: OpenApiIntegrationConfig = {
912 ...current,912 ...current,
913 specHash,913 specHash,
914 ...((resolved.sourceUrl ?? specInputToSourceUrl(specInput)) !== undefined914 ...((resolved.specUrl ?? specInputToSpecUrl(specInput)) !== undefined
915 ? { sourceUrl: resolved.sourceUrl ?? specInputToSourceUrl(specInput) }915 ? { specUrl: resolved.specUrl ?? specInputToSpecUrl(specInput) }
916 : {}),916 : {}),
917 };917 };
918 918
@@ -992,7 +992,7 @@ export const openApiPlugin = definePlugin< · 73 unmodified lines
992 return yield* enrichPreviewWithDiscoveredOAuth({992 return yield* enrichPreviewWithDiscoveredOAuth({
993 specText: resolved.specText,993 specText: resolved.specText,
994 preview,994 preview,
995 sourceUrl: resolved.sourceUrl ?? (spec.kind === "url" ? spec.url : undefined),995 specUrl: resolved.specUrl ?? (spec.kind === "url" ? spec.url : undefined),
996 });996 });
997 }),997 }),
998 998
@@ -1071,7 +1071,7 @@ export const openApiPlugin = definePlugin< · 72 unmodified lines
1071 };1071 };
1072 },1072 },
1073 1073
1074 staticSources: (self: OpenApiPluginExtension) => [1074 staticIntegrations: (self: OpenApiPluginExtension) => [
1075 {1075 {
1076 id: "openapi",1076 id: "openapi",
1077 kind: "executor",1077 kind: "executor",
@@ -1104,9 +1104,9 @@ export const openApiPlugin = definePlugin< · 26 unmodified lines
1104 requiresApproval: true,1104 requiresApproval: true,
1105 approvalDescription: "Add an OpenAPI integration",1105 approvalDescription: "Add an OpenAPI integration",
1106 },1106 },
1107 inputSchema: AddSourceInputStandardSchema,1107 inputSchema: AddIntegrationInputStandardSchema,
1108 outputSchema: AddSourceOutputStandardSchema,1108 outputSchema: AddIntegrationOutputStandardSchema,
1109 execute: (input: typeof AddSourceInputSchema.Type) =>1109 execute: (input: typeof AddIntegrationInputSchema.Type) =>
1110 self1110 self
1111 .addSpec({1111 .addSpec({
1112 spec: input.spec,1112 spec: input.spec,
packages/plugins/openapi/src/sdk/preview.ts+2−2
@@ -146,7 +146,7 @@ export const OAuth2Preset = Schema.Struct({ · 145 unmodified lines
146export type OAuth2Preset = typeof OAuth2Preset.Type;146export type OAuth2Preset = typeof OAuth2Preset.Type;
147 147
148// ---------------------------------------------------------------------------148// ---------------------------------------------------------------------------
149// Preview operation — lightweight shape for the add-source UI list149// Preview operation — lightweight shape for the add-integration UI list
150// ---------------------------------------------------------------------------150// ---------------------------------------------------------------------------
151 151
152export const PreviewOperation = Schema.Struct({152export const PreviewOperation = Schema.Struct({
@@ -171,7 +171,7 @@ export const SpecPreview = Schema.Struct({ · 18 unmodified lines
171 /** Reuses ServerInfo from extraction */171 /** Reuses ServerInfo from extraction */
172 servers: Schema.Array(ServerInfo),172 servers: Schema.Array(ServerInfo),
173 operationCount: Schema.Number,173 operationCount: Schema.Number,
174 /** Lightweight operation list for the add-source UI */174 /** Lightweight operation list for the add-integration UI */
175 operations: Schema.Array(PreviewOperation),175 operations: Schema.Array(PreviewOperation),
176 tags: Schema.Array(Schema.String),176 tags: Schema.Array(Schema.String),
177 securitySchemes: Schema.Array(SecurityScheme),177 securitySchemes: Schema.Array(SecurityScheme),
packages/plugins/openapi/src/sdk/real-specs.test.ts+1−1
@@ -1,6 +1,6 @@
1// Parse / extract / preview coverage against a big real-world spec.1// Parse / extract / preview coverage against a big real-world spec.
2// DB-touching behaviour (addSpec, removeSpec, tool registration) moved2// DB-touching behaviour (addSpec, removeSpec, tool registration) moved
3// to apps/cloud/src/services/sources-api.node.test.ts — those run3// to the apps/cloud integrations API node tests — those run
4// through the real Drizzle/FumaDB path so storage regressions4// through the real Drizzle/FumaDB path so storage regressions
5// (e.g. a per-row createMany fallback) surface automatically instead5// (e.g. a per-row createMany fallback) surface automatically instead
6// of needing a dedicated budget assertion.6// of needing a dedicated budget assertion.
packages/plugins/openapi/src/sdk/response-headers-timeout.test.ts+2−2
@@ -13,7 +13,7 @@ import { · 12 unmodified lines
13 ToolAddress,13 ToolAddress,
14} from "@executor-js/sdk";14} from "@executor-js/sdk";
15import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";15import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
16import { makeOpenApiHttpApiTestSourceConfig, unwrapInvocation } from "../testing";16import { makeOpenApiHttpApiTestIntegrationConfig, unwrapInvocation } from "../testing";
17 17
18import { invokeWithLayer } from "./invoke";18import { invokeWithLayer } from "./invoke";
19import { openApiPlugin } from "./plugin";19import { openApiPlugin } from "./plugin";
@@ -46,7 +46,7 @@ const buildExecutor = (baseUrl: string) => · 26 unmodified lines
46 Effect.gen(function* () {46 Effect.gen(function* () {
47 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));47 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));
48 yield* executor.openapi.addSpec(48 yield* executor.openapi.addSpec(
49 makeOpenApiHttpApiTestSourceConfig(TimeoutApi, { slug: "headers_timeout", baseUrl }),49 makeOpenApiHttpApiTestIntegrationConfig(TimeoutApi, { slug: "headers_timeout", baseUrl }),
50 );50 );
51 yield* executor.connections.create({51 yield* executor.connections.create({
52 owner: "org",52 owner: "org",
packages/plugins/openapi/src/sdk/spec-blob.test.ts+2−2
@@ -34,7 +34,7 @@ import type { OpenapiStore } from "./store"; · 33 unmodified lines
34import type { AuthenticationInput } from "./types";34import type { AuthenticationInput } from "./types";
35import { defsBlobKey, specBlobKey } from "./store";35import { defsBlobKey, specBlobKey } from "./store";
36import {36import {
37 makeOpenApiHttpApiTestSourceConfig,37 makeOpenApiHttpApiTestIntegrationConfig,
38 serveOpenApiHttpApiTestServer,38 serveOpenApiHttpApiTestServer,
39 unwrapInvocation,39 unwrapInvocation,
40} from "../testing";40} from "../testing";
@@ -61,7 +61,7 @@ const EchoGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => · 20 unmodified lines
61);61);
62 62
63const specText = () => {63const specText = () => {
64 const spec = makeOpenApiHttpApiTestSourceConfig(TestApi, {}).spec;64 const spec = makeOpenApiHttpApiTestIntegrationConfig(TestApi, {}).spec;
65 if (spec.kind === "blob") return spec.value;65 if (spec.kind === "blob") return spec.value;
66 return spec.url;66 return spec.url;
67};67};
packages/plugins/openapi/src/sdk/spec-format.ts+1−1
@@ -26,7 +26,7 @@ export interface DerivedIdentity { · 25 unmodified lines
26 26
27export interface ConvertedSpec {27export interface ConvertedSpec {
28 readonly specText: string;28 readonly specText: string;
29 readonly sourceUrl?: string;29 readonly specUrl?: string;
30 readonly baseUrl?: string;30 readonly baseUrl?: string;
31 readonly authenticationTemplate?: readonly Authentication[];31 readonly authenticationTemplate?: readonly Authentication[];
32 readonly config?: Partial<OpenApiIntegrationConfig> & Readonly<Record<string, unknown>>;32 readonly config?: Partial<OpenApiIntegrationConfig> & Readonly<Record<string, unknown>>;
packages/plugins/openapi/src/sdk/streaming-response.test.ts+2−2
@@ -13,7 +13,7 @@ import { · 12 unmodified lines
13 ToolAddress,13 ToolAddress,
14} from "@executor-js/sdk";14} from "@executor-js/sdk";
15import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";15import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
16import { makeOpenApiHttpApiTestSourceConfig, unwrapInvocation } from "../testing";16import { makeOpenApiHttpApiTestIntegrationConfig, unwrapInvocation } from "../testing";
17 17
18import { collectStreamingBody, STREAM_MAX_BYTES } from "./invoke";18import { collectStreamingBody, STREAM_MAX_BYTES } from "./invoke";
19import { openApiPlugin } from "./plugin";19import { openApiPlugin } from "./plugin";
@@ -37,7 +37,7 @@ const buildExecutor = (baseUrl: string) => · 17 unmodified lines
37 Effect.gen(function* () {37 Effect.gen(function* () {
38 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));38 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));
39 yield* executor.openapi.addSpec(39 yield* executor.openapi.addSpec(
40 makeOpenApiHttpApiTestSourceConfig(StreamingApi, { slug: "streaming", baseUrl }),40 makeOpenApiHttpApiTestIntegrationConfig(StreamingApi, { slug: "streaming", baseUrl }),
41 );41 );
42 yield* executor.connections.create({42 yield* executor.connections.create({
43 owner: "org",43 owner: "org",
packages/plugins/openapi/src/sdk/upstream-failures.test.ts+2−2
@@ -35,7 +35,7 @@ import { · 34 unmodified lines
35import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";35import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
36import {36import {
37 addOpenApiTestConnection,37 addOpenApiTestConnection,
38 makeOpenApiHttpApiTestSourceConfig,38 makeOpenApiHttpApiTestIntegrationConfig,
39 type OpenApiTestServerShape,39 type OpenApiTestServerShape,
40 serveOpenApiHttpApiTestServer,40 serveOpenApiHttpApiTestServer,
41 unwrapInvocation,41 unwrapInvocation,
@@ -118,7 +118,7 @@ const buildExecutor = (baseUrl: string) => · 76 unmodified lines
118 Effect.gen(function* () {118 Effect.gen(function* () {
119 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));119 const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));
120 yield* executor.openapi.addSpec(120 yield* executor.openapi.addSpec(
121 makeOpenApiHttpApiTestSourceConfig(FailureApi, { slug: "f", baseUrl }),121 makeOpenApiHttpApiTestIntegrationConfig(FailureApi, { slug: "f", baseUrl }),
122 );122 );
123 yield* executor.connections.create({123 yield* executor.connections.create({
124 owner: "org",124 owner: "org",
packages/plugins/openapi/src/testing/index.ts+20−20
@@ -92,12 +92,12 @@ export interface OpenApiEchoTestServerShape extends OpenApiTestServerShape { · 91 unmodified lines
92 readonly clearRequests: Effect.Effect<void>;92 readonly clearRequests: Effect.Effect<void>;
93}93}
94 94
95export type OpenApiTestSourceOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & {95export type OpenApiTestIntegrationOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & {
96 readonly baseUrl?: string | null;96 readonly baseUrl?: string | null;
97 readonly slug?: string;97 readonly slug?: string;
98};98};
99 99
100export type OpenApiHttpApiTestSourceOptions = Omit<100export type OpenApiHttpApiTestIntegrationOptions = Omit<
101 OpenApiSpecConfig,101 OpenApiSpecConfig,
102 "spec" | "slug" | "baseUrl"102 "spec" | "slug" | "baseUrl"
103> & {103> & {
@@ -113,12 +113,12 @@ type OpenApiHttpApiAddSpecHeadersInput = { · 9 unmodified lines
113};113};
114 114
115export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit<115export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit<
116 OpenApiHttpApiTestSourceOptions,116 OpenApiHttpApiTestIntegrationOptions,
117 "headers" | "queryParams"117 "headers" | "queryParams"
118> &118> &
119 OpenApiHttpApiAddSpecHeadersInput;119 OpenApiHttpApiAddSpecHeadersInput;
120 120
121export type OpenApiTestSourceExecutor = {121export type OpenApiTestIntegrationExecutor = {
122 readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;122 readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;
123};123};
124 124
@@ -160,11 +160,11 @@ export interface OpenApiTestConnection { · 35 unmodified lines
160export const addOpenApiTestConnection = (160export const addOpenApiTestConnection = (
161 executor: OpenApiTestConnectionExecutor,161 executor: OpenApiTestConnectionExecutor,
162 server: OpenApiTestServerShape,162 server: OpenApiTestServerShape,
163 sourceOptions: OpenApiTestSourceOptions,163 integrationOptions: OpenApiTestIntegrationOptions,
164 connectionOptions: OpenApiTestConnectionOptions = {},164 connectionOptions: OpenApiTestConnectionOptions = {},
165): Effect.Effect<OpenApiTestConnection, unknown> =>165): Effect.Effect<OpenApiTestConnection, unknown> =>
166 Effect.gen(function* () {166 Effect.gen(function* () {
167 const config = makeOpenApiTestSourceConfig(server, sourceOptions);167 const config = makeOpenApiTestIntegrationConfig(server, integrationOptions);
168 const result = yield* executor.openapi.addSpec(config);168 const result = yield* executor.openapi.addSpec(config);
169 const slug = String(result.slug);169 const slug = String(result.slug);
170 const owner: Owner = connectionOptions.owner ?? "org";170 const owner: Owner = connectionOptions.owner ?? "org";
@@ -209,9 +209,9 @@ export const makeOpenApiTestSpecJson = ( · 38 unmodified lines
209 return JSON.stringify(OpenApi.fromApi(annotated));209 return JSON.stringify(OpenApi.fromApi(annotated));
210};210};
211 211
212export const makeOpenApiTestSourceConfig = (212export const makeOpenApiTestIntegrationConfig = (
213 server: OpenApiTestServerShape,213 server: OpenApiTestServerShape,
214 options: OpenApiTestSourceOptions,214 options: OpenApiTestIntegrationOptions,
215): OpenApiSpecConfig => {215): OpenApiSpecConfig => {
216 const { baseUrl, slug, ...rest } = options;216 const { baseUrl, slug, ...rest } = options;
217 return {217 return {
@@ -222,15 +222,15 @@ export const makeOpenApiTestSourceConfig = ( · 4 unmodified lines
222 } satisfies OpenApiSpecConfig;222 } satisfies OpenApiSpecConfig;
223};223};
224 224
225export const addOpenApiTestSource = (225export const addOpenApiTestIntegration = (
226 executor: OpenApiTestSourceExecutor,226 executor: OpenApiTestIntegrationExecutor,
227 server: OpenApiTestServerShape,227 server: OpenApiTestServerShape,
228 options: OpenApiTestSourceOptions,228 options: OpenApiTestIntegrationOptions,
229) => executor.openapi.addSpec(makeOpenApiTestSourceConfig(server, options));229) => executor.openapi.addSpec(makeOpenApiTestIntegrationConfig(server, options));
230 230
231export const makeOpenApiHttpApiTestSourceConfig = (231export const makeOpenApiHttpApiTestIntegrationConfig = (
232 api: HttpApi.Any,232 api: HttpApi.Any,
233 options: OpenApiHttpApiTestSourceOptions,233 options: OpenApiHttpApiTestIntegrationOptions,
234): OpenApiSpecConfig => {234): OpenApiSpecConfig => {
235 const { baseUrl, specBaseUrl, transformSpec, slug, ...config } = options;235 const { baseUrl, specBaseUrl, transformSpec, slug, ...config } = options;
236 return {236 return {
@@ -244,18 +244,18 @@ export const makeOpenApiHttpApiTestSourceConfig = ( · 7 unmodified lines
244 } satisfies OpenApiSpecConfig;244 } satisfies OpenApiSpecConfig;
245};245};
246 246
247export const addOpenApiHttpApiTestSource = (247export const addOpenApiHttpApiTestIntegration = (
248 executor: OpenApiTestSourceExecutor,248 executor: OpenApiTestIntegrationExecutor,
249 api: HttpApi.Any,249 api: HttpApi.Any,
250 options: OpenApiHttpApiTestSourceOptions,250 options: OpenApiHttpApiTestIntegrationOptions,
251) => executor.openapi.addSpec(makeOpenApiHttpApiTestSourceConfig(api, options));251) => executor.openapi.addSpec(makeOpenApiHttpApiTestIntegrationConfig(api, options));
252 252
253export const makeOpenApiHttpApiTestAddSpecPayload = (253export const makeOpenApiHttpApiTestAddSpecPayload = (
254 api: HttpApi.Any,254 api: HttpApi.Any,
255 options: OpenApiHttpApiTestAddSpecPayloadOptions,255 options: OpenApiHttpApiTestAddSpecPayloadOptions,
256) => {256) => {
257 const { headers, queryParams, ...sourceOptions } = options;257 const { headers, queryParams, ...integrationOptions } = options;
258 const config = makeOpenApiHttpApiTestSourceConfig(api, sourceOptions);258 const config = makeOpenApiHttpApiTestIntegrationConfig(api, integrationOptions);
259 return {259 return {
260 spec: config.spec,260 spec: config.spec,
261 slug: config.slug,261 slug: config.slug,
packages/plugins/provider-service-split/src/planner.test.ts+5−5
@@ -313,7 +313,7 @@ describe("provider service split migration planner", () => { · 312 unmodified lines
313 313
314 expect(planned?.config).toEqual({314 expect(planned?.config).toEqual({
315 specHash: "mono-hash",315 specHash: "mono-hash",
316 sourceUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",316 specUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
317 specFormat: "google-discovery",317 specFormat: "google-discovery",
318 family: "google",318 family: "google",
319 authenticationTemplate: [319 authenticationTemplate: [
@@ -380,13 +380,13 @@ describe("provider service split migration planner", () => { · 60 unmodified lines
380 const planned = plan.orgs[0]?.integrations[0];380 const planned = plan.orgs[0]?.integrations[0];
381 381
382 expect(planned?.config).toMatchObject({382 expect(planned?.config).toMatchObject({
383 sourceUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",383 specUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
384 specFormat: "google-discovery",384 specFormat: "google-discovery",
385 family: "google",385 family: "google",
386 });386 });
387 expect(387 expect((planned?.config as { readonly specUrl?: string } | undefined)?.specUrl).not.toContain(
388 (planned?.config as { readonly sourceUrl?: string } | undefined)?.sourceUrl,388 "oauth2",
389 ).not.toContain("oauth2");389 );
390 expect(planned?.servingState.operationToolNames).toEqual(["calendar.events.list"]);390 expect(planned?.servingState.operationToolNames).toEqual(["calendar.events.list"]);
391 });391 });
392 392
packages/plugins/provider-service-split/src/planner.ts+4−4
@@ -159,7 +159,7 @@ export interface ServiceTarget { · 158 unmodified lines
159 readonly slug: string;159 readonly slug: string;
160 readonly name: string;160 readonly name: string;
161 readonly description: string;161 readonly description: string;
162 readonly sourceUrl?: string;162 readonly specUrl?: string;
163 readonly specFormat: "google-discovery" | "microsoft-graph";163 readonly specFormat: "google-discovery" | "microsoft-graph";
164 readonly authenticationTemplate?: readonly unknown[];164 readonly authenticationTemplate?: readonly unknown[];
165 readonly healthCheck?: unknown;165 readonly healthCheck?: unknown;
@@ -480,7 +480,7 @@ const serviceTargetForPreset = ( · 314 unmodified lines
480 slug: preset.defaultSlug ?? preset.id,480 slug: preset.defaultSlug ?? preset.id,
481 name: preset.name,481 name: preset.name,
482 description: preset.summary,482 description: preset.summary,
483 ...(preset.url ? { sourceUrl: preset.url } : {}),483 ...(preset.url ? { specUrl: preset.url } : {}),
484 specFormat: "google-discovery",484 specFormat: "google-discovery",
485 ...(preset.authTemplate ? { authenticationTemplate: preset.authTemplate } : {}),485 ...(preset.authTemplate ? { authenticationTemplate: preset.authTemplate } : {}),
486 ...(preset.healthCheck ? { healthCheck: preset.healthCheck } : {}),486 ...(preset.healthCheck ? { healthCheck: preset.healthCheck } : {}),
@@ -495,7 +495,7 @@ const serviceTargetForPreset = ( · 8 unmodified lines
495 slug: preset.defaultSlug ?? preset.id,495 slug: preset.defaultSlug ?? preset.id,
496 name: preset.name,496 name: preset.name,
497 description: preset.summary,497 description: preset.summary,
498 ...(preset.url ? { sourceUrl: preset.url } : {}),498 ...(preset.url ? { specUrl: preset.url } : {}),
499 specFormat: "microsoft-graph",499 specFormat: "microsoft-graph",
500 ...(preset.authTemplate ? { authenticationTemplate: preset.authTemplate } : {}),500 ...(preset.authTemplate ? { authenticationTemplate: preset.authTemplate } : {}),
501 ...(preset.healthCheck ? { healthCheck: preset.healthCheck } : {}),501 ...(preset.healthCheck ? { healthCheck: preset.healthCheck } : {}),
@@ -634,7 +634,7 @@ const configForService = (source: IntegrationRow, target: ServiceTarget): unknow · 132 unmodified lines
634 typeof config.specHash === "string" && config.specHash.length > 0 ? config.specHash : undefined;634 typeof config.specHash === "string" && config.specHash.length > 0 ? config.specHash : undefined;
635 return {635 return {
636 ...(specHash ? { specHash } : {}),636 ...(specHash ? { specHash } : {}),
637 ...(target.sourceUrl ? { sourceUrl: target.sourceUrl } : {}),637 ...(target.specUrl ? { specUrl: target.specUrl } : {}),
638 specFormat: target.specFormat,638 specFormat: target.specFormat,
639 family: target.family,639 family: target.family,
640 ...(target.authenticationTemplate640 ...(target.authenticationTemplate
packages/plugins/provider-service-split/src/sqlite.test.ts+1−1
@@ -207,7 +207,7 @@ describe("providerServiceSplitDataMigration", () => { · 206 unmodified lines
207 expect(integrations.rows[0]?.plugin_id).toBe("openapi");207 expect(integrations.rows[0]?.plugin_id).toBe("openapi");
208 expect(parseJson(String(integrations.rows[0]?.config))).toMatchObject({208 expect(parseJson(String(integrations.rows[0]?.config))).toMatchObject({
209 specHash: "mono-hash",209 specHash: "mono-hash",
210 sourceUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",210 specUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
211 specFormat: "google-discovery",211 specFormat: "google-discovery",
212 family: "google",212 family: "google",
213 });213 });
packages/plugins/toolkits/src/page.tsx+11−11
@@ -198,7 +198,7 @@ type ToolkitConnectionGroup = { · 197 unmodified lines
198 198
199type IntegrationMeta = {199type IntegrationMeta = {
200 readonly name: string;200 readonly name: string;
201 readonly sourceId: string;201 readonly integrationId: string;
202 readonly icon?: string | null;202 readonly icon?: string | null;
203 readonly url?: string;203 readonly url?: string;
204};204};
@@ -327,17 +327,17 @@ const integrationMetaFor = ( · 122 unmodified lines
327 const fallbackName = group.integration327 const fallbackName = group.integration
328 .replace(/[_-]/g, " ")328 .replace(/[_-]/g, " ")
329 .replace(/\b\w/g, (character) => character.toUpperCase());329 .replace(/\b\w/g, (character) => character.toUpperCase());
330 const source = {330 const integrationSummary = {
331 id: group.integration,331 id: group.integration,
332 kind: integration?.kind ?? group.integration,332 kind: integration?.kind ?? group.integration,
333 name: integration?.name ?? fallbackName,333 name: integration?.name ?? fallbackName,
334 url: integration?.displayUrl,334 url: integration?.displayUrl,
335 };335 };
336 return {336 return {
337 name: source.name,337 name: integrationSummary.name,
338 sourceId: group.integration,338 integrationId: group.integration,
339 icon: integrationPresetIconUrl(source, integrationPlugins),339 icon: integrationPresetIconUrl(integrationSummary, integrationPlugins),
340 ...(source.url ? { url: source.url } : {}),340 ...(integrationSummary.url ? { url: integrationSummary.url } : {}),
341 };341 };
342};342};
343 343
@@ -346,7 +346,7 @@ type ConfiguredConnectionView = { · 2 unmodified lines
346 readonly title: string;346 readonly title: string;
347 readonly subtitle: string;347 readonly subtitle: string;
348 readonly pattern: string;348 readonly pattern: string;
349 readonly sourceId: string;349 readonly integrationId: string;
350 readonly icon?: string | null;350 readonly icon?: string | null;
351 readonly url?: string;351 readonly url?: string;
352};352};
@@ -368,7 +368,7 @@ const configuredConnectionViews = ( · 15 unmodified lines
368 title: connection.pattern,368 title: connection.pattern,
369 subtitle: "Configured pattern",369 subtitle: "Configured pattern",
370 pattern: connection.pattern,370 pattern: connection.pattern,
371 sourceId: connection.pattern.split(".")[0] ?? "toolkit",371 integrationId: connection.pattern.split(".")[0] ?? "toolkit",
372 };372 };
373 }373 }
374 const meta = integrationMetaFor(group, integrations, integrationPlugins);374 const meta = integrationMetaFor(group, integrations, integrationPlugins);
@@ -377,7 +377,7 @@ const configuredConnectionViews = ( · 2 unmodified lines
377 title: connectionDisplayTitle(group, meta),377 title: connectionDisplayTitle(group, meta),
378 subtitle: connectionDisplaySubtitleForHost(group, meta, showOwnerLabels),378 subtitle: connectionDisplaySubtitleForHost(group, meta, showOwnerLabels),
379 pattern: connection.pattern,379 pattern: connection.pattern,
380 sourceId: meta.sourceId,380 integrationId: meta.integrationId,
381 icon: meta.icon,381 icon: meta.icon,
382 ...(meta.url ? { url: meta.url } : {}),382 ...(meta.url ? { url: meta.url } : {}),
383 };383 };
@@ -437,7 +437,7 @@ function ToolkitConnectionIconStack(props: { connections: readonly ConfiguredCon · 53 unmodified lines
437 >437 >
438 <IntegrationFavicon438 <IntegrationFavicon
439 icon={connection.icon}439 icon={connection.icon}
440 sourceId={connection.sourceId}440 integrationId={connection.integrationId}
441 url={connection.url}441 url={connection.url}
442 size={14}442 size={14}
443 />443 />
@@ -887,7 +887,7 @@ function AddConnectionDialog(props: { · 443 unmodified lines
887 <span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/30">887 <span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/30">
888 <IntegrationFavicon888 <IntegrationFavicon
889 icon={meta.icon}889 icon={meta.icon}
890 sourceId={meta.sourceId}890 integrationId={meta.integrationId}
891 url={meta.url}891 url={meta.url}
892 size={18}892 size={18}
893 />893 />
packages/react/src/api/error-reporting.test.ts+6−6
@@ -18,14 +18,14 @@ describe("frontend error reporting", () => { · 17 unmodified lines
18 });18 });
19 19
20 it("extracts stable messages from Effect exits", () => {20 it("extracts stable messages from Effect exits", () => {
21 const exit = Exit.fail({ message: "Could not update source" });21 const exit = Exit.fail({ message: "Could not update integration" });
22 22
23 expect(messageFromExit(exit, "Fallback")).toBe("Could not update source");23 expect(messageFromExit(exit, "Fallback")).toBe("Could not update integration");
24 expect(messageFromExit(Exit.fail({ reason: "unknown" }), "Fallback")).toBe("Fallback");24 expect(messageFromExit(Exit.fail({ reason: "unknown" }), "Fallback")).toBe("Fallback");
25 });25 });
26 26
27 it("reports failed exits with the provided context", () => {27 it("reports failed exits with the provided context", () => {
28 const exit = Exit.fail({ message: "Could not update source" });28 const exit = Exit.fail({ message: "Could not update integration" });
29 const calls: Array<{ error: unknown; context: FrontendErrorContext }> = [];29 const calls: Array<{ error: unknown; context: FrontendErrorContext }> = [];
30 30
31 reportExitFailure(31 reportExitFailure(
@@ -34,15 +34,15 @@ describe("frontend error reporting", () => { · 2 unmodified lines
34 },34 },
35 exit,35 exit,
36 {36 {
37 surface: "sources",37 surface: "integrations",
38 action: "update",38 action: "update",
39 message: "Could not update source",39 message: "Could not update integration",
40 },40 },
41 );41 );
42 42
43 expect(calls).toHaveLength(1);43 expect(calls).toHaveLength(1);
44 expect(Cause.isCause(calls[0]!.error)).toBe(true);44 expect(Cause.isCause(calls[0]!.error)).toBe(true);
45 expect(calls[0]!.context.surface).toBe("sources");45 expect(calls[0]!.context.surface).toBe("integrations");
46 expect(calls[0]!.context.action).toBe("update");46 expect(calls[0]!.context.action).toBe("update");
47 });47 });
48});48});
packages/react/src/components/accounts-section.tsx+1−1
@@ -397,7 +397,7 @@ export function AccountsSection(props: { · 396 unmodified lines
397 // added widened the consent).397 // added widened the consent).
398 //398 //
399 // Spec-derived oauth scopes are the full per-operation catalog union (e.g. an399 // Spec-derived oauth scopes are the full per-operation catalog union (e.g. an
400 // OpenAPI source like PostHog declares hundreds of scopes). Those are requested400 // OpenAPI integration like PostHog declares hundreds of scopes). Those are requested
401 // broadly but not individually required: a provider that narrows the grant to401 // broadly but not individually required: a provider that narrows the grant to
402 // the user's actual access is healthy, not in need of reconnect. So only treat402 // the user's actual access is healthy, not in need of reconnect. So only treat
403 // CUSTOM (user-configured) scopes as required here; never the spec catalog.403 // CUSTOM (user-configured) scopes as required here; never the spec catalog.
packages/react/src/components/command-palette.tsx+7−7
@@ -23,8 +23,8 @@ import { · 22 unmodified lines
23// CommandPalette — global ⌘K navigator.23// CommandPalette — global ⌘K navigator.
24//24//
25// Order of entries:25// Order of entries:
26// 1. Connected sources (priority, shown first)26// 1. Connected integrations (priority, shown first)
27// 2. Add <Plugin> actions for each available source plugin27// 2. Add <Plugin> actions for each available integration plugin
28// 3. Popular integrations (plugin presets)28// 3. Popular integrations (plugin presets)
29// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
30 30
@@ -46,7 +46,7 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool · 15 unmodified lines
46 return () => document.removeEventListener("keydown", onKeyDown);46 return () => document.removeEventListener("keydown", onKeyDown);
47 }, [onOpenChange, open]);47 }, [onOpenChange, open]);
48 48
49 const connectedSources = useMemo(49 const connectedIntegrations = useMemo(
50 () =>50 () =>
51 AsyncResult.match(integrationsResult, {51 AsyncResult.match(integrationsResult, {
52 onInitial: () => [] as Array<{ id: string; name: string; kind: string; url?: string }>,52 onInitial: () => [] as Array<{ id: string; name: string; kind: string; url?: string }>,
@@ -140,9 +140,9 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool · 87 unmodified lines
140 <CommandList>140 <CommandList>
141 <CommandEmpty>No results found.</CommandEmpty>141 <CommandEmpty>No results found.</CommandEmpty>
142 142
143 {connectedSources.length > 0 && (143 {connectedIntegrations.length > 0 && (
144 <CommandGroup heading="Connected">144 <CommandGroup heading="Connected">
145 {connectedSources.map(145 {connectedIntegrations.map(
146 (s: {146 (s: {
147 readonly id: string;147 readonly id: string;
148 readonly name: string;148 readonly name: string;
@@ -150,7 +150,7 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool · 1 unmodified line
150 readonly url?: string;150 readonly url?: string;
151 }) => (151 }) => (
152 <CommandItem152 <CommandItem
153 key={`source-${s.id}`}153 key={`integration-${s.id}`}
154 value={`connected ${s.name} ${s.id} ${s.kind}`}154 value={`connected ${s.name} ${s.id} ${s.kind}`}
155 onSelect={() => goToIntegration(s.id)}155 onSelect={() => goToIntegration(s.id)}
156 >156 >
@@ -166,7 +166,7 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool · 9 unmodified lines
166 </CommandGroup>166 </CommandGroup>
167 )}167 )}
168 168
169 {connectedSources.length > 0 && integrationPlugins.length > 0 && <CommandSeparator />}169 {connectedIntegrations.length > 0 && integrationPlugins.length > 0 && <CommandSeparator />}
170 170
171 {integrationPlugins.length > 0 && (171 {integrationPlugins.length > 0 && (
172 <CommandGroup heading="Add integration">172 <CommandGroup heading="Add integration">
packages/react/src/components/integration-favicon.test.tsx+12−10
@@ -43,34 +43,36 @@ describe("IntegrationFavicon", () => { · 42 unmodified lines
43 );43 );
44 });44 });
45 45
46 it("uses the Executor favicon for the built-in executor source", () => {46 it("uses the Executor favicon for the built-in executor integration", () => {
47 expect(integrationLocalIconUrl("executor")).toBe("/favicon-32.png");47 expect(integrationLocalIconUrl("executor")).toBe("/favicon-32.png");
48 expect(integrationLocalIconUrl("openapi")).toBeNull();48 expect(integrationLocalIconUrl("openapi")).toBeNull();
49 });49 });
50 50
51 it("resolves the Executor sidebar icon only when the source id is threaded (cloud/self-host repro)", () => {51 it("resolves the Executor sidebar icon only when the integration id is threaded (cloud/self-host repro)", () => {
52 // Reconstruct the props the multiplayer shell derives for the built-in52 // Reconstruct the props the multiplayer shell derives for the built-in
53 // executor source (EXECUTOR_SOURCE: kind "built-in", name "Executor", no53 // executor integration (EXECUTOR_INTEGRATION: kind "built-in", name "Executor", no
54 // displayUrl). The sidebar's IntegrationList builds icon/url exactly this way.54 // displayUrl). The sidebar's IntegrationList builds icon/url exactly this way.
55 const slug = "executor";55 const slug = "executor";
56 const name = "Executor";56 const name = "Executor";
57 const icon = integrationPresetIconUrl({ id: slug, kind: "built-in", name, url: undefined }, []);57 const icon = integrationPresetIconUrl({ id: slug, kind: "built-in", name, url: undefined }, []);
58 const url = integrationInferredUrl({ id: slug, name }) ?? undefined;58 const url = integrationInferredUrl({ id: slug, name }) ?? undefined;
59 59
60 // The built-in source matches no preset and has no inferable host, so the60 // The built-in integration matches no preset and has no inferable host, so the
61 // sourceId branch is the only thing that can resolve its icon.61 // integrationId branch is the only thing that can resolve its icon.
62 expect(icon).toBeNull();62 expect(icon).toBeNull();
63 expect(url).toBeUndefined();63 expect(url).toBeUndefined();
64 64
65 // Bug: cloud/self-host dropped sourceId, so the cascade fell through to null65 // Bug: cloud/self-host dropped integrationId, so the cascade fell through to null
66 // and rendered the neutral BoxIcon placeholder.66 // and rendered the neutral BoxIcon placeholder.
67 expect(integrationFaviconSrc({ icon, url, size: 16 })).toBeNull();67 expect(integrationFaviconSrc({ icon, url, size: 16 })).toBeNull();
68 68
69 // Fix: threading sourceId resolves the bundled Executor favicon.69 // Fix: threading integrationId resolves the bundled Executor favicon.
70 expect(integrationFaviconSrc({ icon, sourceId: slug, url, size: 16 })).toBe("/favicon-32.png");70 expect(integrationFaviconSrc({ icon, integrationId: slug, url, size: 16 })).toBe(
71 "/favicon-32.png",
72 );
71 });73 });
72 74
73 it("finds preset icons from a source URL", () => {75 it("finds preset icons from an integration URL", () => {
74 expect(76 expect(
75 integrationPresetIconUrl(77 integrationPresetIconUrl(
76 {78 {
@@ -128,7 +130,7 @@ describe("IntegrationFavicon", () => { · 51 unmodified lines
128 ).toBe("https://example.com/google.svg");130 ).toBe("https://example.com/google.svg");
129 });131 });
130 132
131 it("finds preset icons from a source id when the URL is missing", () => {133 it("finds preset icons from an integration id when the URL is missing", () => {
132 expect(134 expect(
133 integrationPresetIconUrl(135 integrationPresetIconUrl(
134 {136 {
packages/react/src/components/integration-favicon.tsx+29−29
@@ -32,8 +32,8 @@ export function integrationFaviconFallbackUrl( · 31 unmodified lines
32 return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`;32 return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`;
33}33}
34 34
35export function integrationLocalIconUrl(sourceId: string | undefined): string | null {35export function integrationLocalIconUrl(integrationId: string | undefined): string | null {
36 if (sourceId !== "executor") return null;36 if (integrationId !== "executor") return null;
37 return "/favicon-32.png";37 return "/favicon-32.png";
38}38}
39 39
@@ -125,12 +125,12 @@ const tokenVariants = (value: string | undefined): readonly string[] => { · 85 unmodified lines
125 return [...tokens];125 return [...tokens];
126};126};
127 127
128export function integrationInferredUrl(source: {128export function integrationInferredUrl(integration: {
129 readonly id: string;129 readonly id: string;
130 readonly name?: string;130 readonly name?: string;
131}): string | null {131}): string | null {
132 const hostPattern = /^[a-z0-9][a-z0-9.-]*\.(?:app|co|com|dev|io|net|org)(?:\/.*)?$/i;132 const hostPattern = /^[a-z0-9][a-z0-9.-]*\.(?:app|co|com|dev|io|net|org)(?:\/.*)?$/i;
133 const hostLike = [source.name, source.id.replaceAll("_", ".")]133 const hostLike = [integration.name, integration.id.replaceAll("_", ".")]
134 .filter((value): value is string => value != null && value.length > 0)134 .filter((value): value is string => value != null && value.length > 0)
135 .find((value) => hostPattern.test(value.trim()));135 .find((value) => hostPattern.test(value.trim()));
136 if (hostLike) return `https://${hostLike.trim().replace(/^https?:\/\//i, "")}`;136 if (hostLike) return `https://${hostLike.trim().replace(/^https?:\/\//i, "")}`;
@@ -138,15 +138,15 @@ export function integrationInferredUrl(source: { · 1 unmodified line
138 return null;138 return null;
139}139}
140 140
141const tokenMatches = (sourceValue: string, presetValue: string): boolean =>141const tokenMatches = (integrationValue: string, presetValue: string): boolean =>
142 presetValue.length > 0 &&142 presetValue.length > 0 &&
143 sourceValue.length > 0 &&143 integrationValue.length > 0 &&
144 (sourceValue === presetValue ||144 (integrationValue === presetValue ||
145 sourceValue.includes(presetValue) ||145 integrationValue.includes(presetValue) ||
146 presetValue.includes(sourceValue));146 presetValue.includes(integrationValue));
147 147
148export function integrationPresetIconUrl(148export function integrationPresetIconUrl(
149 source: {149 integration: {
150 readonly id: string;150 readonly id: string;
151 readonly kind: string;151 readonly kind: string;
152 readonly name?: string;152 readonly name?: string;
@@ -154,25 +154,25 @@ export function integrationPresetIconUrl( · 1 unmodified line
154 },154 },
155 integrationPlugins: readonly IntegrationPlugin[],155 integrationPlugins: readonly IntegrationPlugin[],
156): string | null {156): string | null {
157 const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind;157 const pluginKey = KIND_TO_PLUGIN_KEY[integration.kind] ?? integration.kind;
158 const plugin = integrationPlugins.find((p) => p.key === pluginKey);158 const plugin = integrationPlugins.find((p) => p.key === pluginKey);
159 const presets = plugin?.presets ?? [];159 const presets = plugin?.presets ?? [];
160 const exactSlugIcon = presets.find((p) => p.defaultSlug === source.id)?.icon;160 const exactSlugIcon = presets.find((p) => p.defaultSlug === integration.id)?.icon;
161 if (exactSlugIcon) return exactSlugIcon;161 if (exactSlugIcon) return exactSlugIcon;
162 162
163 const sourceUrl = normalizeUrl(source.url);163 const integrationUrl = normalizeUrl(integration.url);
164 const sourceGoogleService = googleApiServiceFromUrl(source.url);164 const integrationGoogleService = googleApiServiceFromUrl(integration.url);
165 const sourceTokens = [...tokenVariants(source.id), ...tokenVariants(source.name)];165 const integrationTokens = [...tokenVariants(integration.id), ...tokenVariants(integration.name)];
166 166
167 const preset = presets.find((p) => {167 const preset = presets.find((p) => {
168 const presetUrl = normalizeUrl(p.url);168 const presetUrl = normalizeUrl(p.url);
169 const presetGoogleService = googleApiServiceFromUrl(p.url);169 const presetGoogleService = googleApiServiceFromUrl(p.url);
170 const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];170 const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];
171 return (171 return (
172 (sourceUrl !== null && presetUrl === sourceUrl) ||172 (integrationUrl !== null && presetUrl === integrationUrl) ||
173 (sourceGoogleService !== null && presetGoogleService === sourceGoogleService) ||173 (integrationGoogleService !== null && presetGoogleService === integrationGoogleService) ||
174 sourceTokens.some((sourceToken) =>174 integrationTokens.some((integrationToken) =>
175 presetTokens.some((presetToken) => tokenMatches(sourceToken, presetToken)),175 presetTokens.some((presetToken) => tokenMatches(integrationToken, presetToken)),
176 )176 )
177 );177 );
178 });178 });
@@ -181,14 +181,14 @@ export function integrationPresetIconUrl( · 2 unmodified lines
181}181}
182 182
183// Resolution cascade for the rendered favicon: first non-null, non-failed of an183// Resolution cascade for the rendered favicon: first non-null, non-failed of an
184// explicit preset icon, the bundled local icon for a known source id, the184// explicit preset icon, the bundled local icon for a known integration id, the
185// integrations.sh logo proxy derived from the source URL, then the Google185// integrations.sh logo proxy derived from the integration URL, then the Google
186// favicon service as a last resort. The built-in executor source has no preset186// favicon service as a last resort. The built-in executor integration has no preset
187// icon and no URL, so it resolves ONLY through the sourceId branch: callers187// icon and no URL, so it resolves ONLY through the integrationId branch: callers
188// that drop sourceId fall through to the neutral BoxIcon placeholder.188// that drop integrationId fall through to the neutral BoxIcon placeholder.
189export function integrationFaviconSrc(args: {189export function integrationFaviconSrc(args: {
190 icon?: string | null;190 icon?: string | null;
191 sourceId?: string;191 integrationId?: string;
192 url?: string;192 url?: string;
193 size: number;193 size: number;
194 failedSrcs?: readonly string[];194 failedSrcs?: readonly string[];
@@ -197,7 +197,7 @@ export function integrationFaviconSrc(args: { · 2 unmodified lines
197 return (197 return (
198 [198 [
199 args.icon ?? null,199 args.icon ?? null,
200 integrationLocalIconUrl(args.sourceId),200 integrationLocalIconUrl(args.integrationId),
201 integrationFaviconUrl(args.url, args.size),201 integrationFaviconUrl(args.url, args.size),
202 integrationFaviconFallbackUrl(args.url, args.size),202 integrationFaviconFallbackUrl(args.url, args.size),
203 ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null203 ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null
@@ -206,17 +206,17 @@ export function integrationFaviconSrc(args: { · 2 unmodified lines
206 206
207export function IntegrationFavicon({207export function IntegrationFavicon({
208 icon,208 icon,
209 sourceId,209 integrationId,
210 url,210 url,
211 size = 16,211 size = 16,
212}: {212}: {
213 icon?: string | null;213 icon?: string | null;
214 sourceId?: string;214 integrationId?: string;
215 url?: string;215 url?: string;
216 size?: number;216 size?: number;
217}) {217}) {
218 const [failedSrcs, setFailedSrcs] = useState<readonly string[]>([]);218 const [failedSrcs, setFailedSrcs] = useState<readonly string[]>([]);
219 const src = integrationFaviconSrc({ icon, sourceId, url, size, failedSrcs });219 const src = integrationFaviconSrc({ icon, integrationId, url, size, failedSrcs });
220 220
221 if (!src) {221 if (!src) {
222 return (222 return (
packages/react/src/components/integration-icon-with-account.tsx+2−2
@@ -2,7 +2,7 @@ import { IntegrationFavicon } from "./integration-favicon"; · 1 unmodified line
2 2
3export function IntegrationIconWithAccount(props: {3export function IntegrationIconWithAccount(props: {
4 readonly icon?: string | null;4 readonly icon?: string | null;
5 readonly sourceId: string;5 readonly integrationId: string;
6 readonly url?: string;6 readonly url?: string;
7 readonly size?: "sm" | "md";7 readonly size?: "sm" | "md";
8}) {8}) {
@@ -11,7 +11,7 @@ export function IntegrationIconWithAccount(props: { · 2 unmodified lines
11 <span className={props.size === "sm" ? "relative size-4 shrink-0" : "relative size-8 shrink-0"}>11 <span className={props.size === "sm" ? "relative size-4 shrink-0" : "relative size-8 shrink-0"}>
12 <IntegrationFavicon12 <IntegrationFavicon
13 icon={props.icon}13 icon={props.icon}
14 sourceId={props.sourceId}14 integrationId={props.integrationId}
15 url={props.url}15 url={props.url}
16 size={iconSize}16 size={iconSize}
17 />17 />
packages/react/src/components/metadata-edit-sheet.tsx+3−2
@@ -232,7 +232,8 @@ export function IntegrationEditSheet(props: { · 231 unmodified lines
232 <SheetTitle>Edit integration</SheetTitle>232 <SheetTitle>Edit integration</SheetTitle>
233 <SheetDescription>233 <SheetDescription>
234 The name is what people see; the description is agent-visible context — agents read it234 The name is what people see; the description is agent-visible context — agents read it
235 when browsing sources and as fallback context on connections without one of their own.235 when browsing integrations and as fallback context on connections without one of their
236 own.
236 </SheetDescription>237 </SheetDescription>
237 </SheetHeader>238 </SheetHeader>
238 239
@@ -275,7 +276,7 @@ export function IntegrationEditSheet(props: { · 36 unmodified lines
275 276
276 {props.pluginSection ? (277 {props.pluginSection ? (
277 <Suspense fallback={null}>278 <Suspense fallback={null}>
278 <props.pluginSection sourceId={slug} onPendingChange={handlePendingChange} />279 <props.pluginSection integrationId={slug} onPendingChange={handlePendingChange} />
279 </Suspense>280 </Suspense>
280 ) : null}281 ) : null}
281 </div>282 </div>
packages/react/src/lib/integration-add.tsx+1−1
@@ -1,5 +1,5 @@
1// ---------------------------------------------------------------------------1// ---------------------------------------------------------------------------
2// Integration add-flow helpers — the plugin-agnostic pieces every Add-source2// Integration add-flow helpers — the plugin-agnostic pieces every Add-integration
3// form repeats: decoding a mutation Exit into a user-facing message, the3// form repeats: decoding a mutation Exit into a user-facing message, the
4// IntegrationAlreadyExistsError guard, the live slug-collision check against4// IntegrationAlreadyExistsError guard, the live slug-collision check against
5// the catalog, and the inline error/collision alerts.5// the catalog, and the inline error/collision alerts.
packages/react/src/multiplayer/shell.tsx+1−1
@@ -230,7 +230,7 @@ function IntegrationList(props: { pathname: string; onNavigate?: () => void }) { · 229 unmodified lines
230 { id: slug, kind: integration.kind, name, url: integration.displayUrl },230 { id: slug, kind: integration.kind, name, url: integration.displayUrl },
231 integrationPlugins,231 integrationPlugins,
232 )}232 )}
233 sourceId={slug}233 integrationId={slug}
234 url={234 url={
235 integration.displayUrl ??235 integration.displayUrl ??
236 integrationInferredUrl({ id: slug, name }) ??236 integrationInferredUrl({ id: slug, name }) ??
packages/react/src/pages/integration-detail.tsx+1−1
@@ -433,7 +433,7 @@ export function IntegrationDetailPage(props: { namespace: string }) { · 432 unmodified lines
433 {editPlugin?.accounts ? (433 {editPlugin?.accounts ? (
434 <Suspense fallback={<AccountsSkeleton />}>434 <Suspense fallback={<AccountsSkeleton />}>
435 <editPlugin.accounts435 <editPlugin.accounts
436 sourceId={namespace}436 integrationId={namespace}
437 integrationName={integrationData?.name || namespace}437 integrationName={integrationData?.name || namespace}
438 accountHandoff={accountHandoff}438 accountHandoff={accountHandoff}
439 />439 />
packages/react/src/pages/integrations.tsx+2−2
@@ -458,7 +458,7 @@ function IntegrationGrid(props: { integrations: readonly Integration[] }) { · 457 unmodified lines
458 { id: slug, kind: integration.kind, name, url: integration.displayUrl },458 { id: slug, kind: integration.kind, name, url: integration.displayUrl },
459 integrationPlugins,459 integrationPlugins,
460 )}460 )}
461 sourceId={slug}461 integrationId={slug}
462 url={integration.displayUrl ?? integrationInferredUrl({ id: slug, name }) ?? undefined}462 url={integration.displayUrl ?? integrationInferredUrl({ id: slug, name }) ?? undefined}
463 />463 />
464 <CardStackEntryContent>464 <CardStackEntryContent>
@@ -468,7 +468,7 @@ function IntegrationGrid(props: { integrations: readonly Integration[] }) { · 3 unmodified lines
468 <CardStackEntryActions>468 <CardStackEntryActions>
469 {SummaryComponent && (469 {SummaryComponent && (
470 <Suspense fallback={null}>470 <Suspense fallback={null}>
471 <SummaryComponent sourceId={slug} />471 <SummaryComponent integrationId={slug} />
472 </Suspense>472 </Suspense>
473 )}473 )}
474 <IntegrationHealthSummary integration={integration.slug} />474 <IntegrationHealthSummary integration={integration.slug} />
packages/react/src/plugins/integration-identity.tsx+7−7
@@ -40,10 +40,10 @@ export function pascalCaseDomainLabel(label: string): string | null { · 39 unmodified lines
40 .join("");40 .join("");
41}41}
42 42
43export function integrationDisplayNameFromUrl(url: string, sourceKind: string): string | null {43export function integrationDisplayNameFromUrl(url: string, integrationKind: string): string | null {
44 const label = domainLabelFromUrl(url);44 const label = domainLabelFromUrl(url);
45 const displayLabel = label ? pascalCaseDomainLabel(label) : null;45 const displayLabel = label ? pascalCaseDomainLabel(label) : null;
46 return displayLabel ? `${displayLabel} ${sourceKind}` : null;46 return displayLabel ? `${displayLabel} ${integrationKind}` : null;
47}47}
48 48
49// Package runners whose own name is never the integration name: the meaningful49// Package runners whose own name is never the integration name: the meaningful
@@ -129,11 +129,11 @@ export function humanizeStdioToken(token: string): string | null { · 79 unmodified lines
129export function integrationDisplayNameFromStdio(129export function integrationDisplayNameFromStdio(
130 command: string,130 command: string,
131 args: readonly string[],131 args: readonly string[],
132 sourceKind: string,132 integrationKind: string,
133): string | null {133): string | null {
134 const token = stdioPackageToken(command, args);134 const token = stdioPackageToken(command, args);
135 const label = token ? humanizeStdioToken(token) : null;135 const label = token ? humanizeStdioToken(token) : null;
136 return label ? `${label} ${sourceKind}` : null;136 return label ? `${label} ${integrationKind}` : null;
137}137}
138 138
139// ---------------------------------------------------------------------------139// ---------------------------------------------------------------------------
@@ -208,7 +208,7 @@ export interface IntegrationIdentityFieldsProps { · 68 unmodified lines
208 readonly namespaceHint?: string;208 readonly namespaceHint?: string;
209 /**209 /**
210 * When true, the namespace field is rendered disabled — useful on Edit210 * When true, the namespace field is rendered disabled — useful on Edit
211 * forms, where the namespace is the source's identity and changing it211 * forms, where the namespace is the integration's identity and changing it
212 * would require a delete + recreate flow.212 * would require a delete + recreate flow.
213 */213 */
214 readonly namespaceReadOnly?: boolean;214 readonly namespaceReadOnly?: boolean;
@@ -225,7 +225,7 @@ export function IntegrationIdentityFields({ · 10 unmodified lines
225 const effectiveNamespaceHint =225 const effectiveNamespaceHint =
226 namespaceHint ??226 namespaceHint ??
227 (namespaceReadOnly227 (namespaceReadOnly
228 ? "The namespace is part of the source's identity and cannot be changed."228 ? "The namespace is part of the integration's identity and cannot be changed."
229 : undefined);229 : undefined);
230 230
231 return (231 return (
@@ -255,7 +255,7 @@ export function IntegrationIdentityFieldRows({ · 23 unmodified lines
255 const effectiveNamespaceHint =255 const effectiveNamespaceHint =
256 namespaceHint ??256 namespaceHint ??
257 (namespaceReadOnly257 (namespaceReadOnly
258 ? "The namespace is part of the source's identity and cannot be changed."258 ? "The namespace is part of the integration's identity and cannot be changed."
259 : undefined);259 : undefined);
260 260
261 return (261 return (
packages/react/src/plugins/oauth-reconnect.test.ts+1−1
@@ -117,7 +117,7 @@ describe("missingScopes (Part 2 informational subset warning)", () => { · 116 unmodified lines
117 117
118describe("reconsentRequiredScopes", () => {118describe("reconsentRequiredScopes", () => {
119 it("treats spec-derived oauth scopes as NOT required (opportunistic catalog)", () => {119 it("treats spec-derived oauth scopes as NOT required (opportunistic catalog)", () => {
120 // An OpenAPI source (e.g. PostHog) declares the full per-operation scope120 // An OpenAPI integration (e.g. PostHog) declares the full per-operation scope
121 // catalog. A narrower grant is healthy and must not nag for reconnect.121 // catalog. A narrower grant is healthy and must not nag for reconnect.
122 expect(122 expect(
123 reconsentRequiredScopes({123 reconsentRequiredScopes({
packages/react/src/plugins/oauth-reconnect.ts+1−1
@@ -113,7 +113,7 @@ export function connectionNeedsReconsent( · 112 unmodified lines
113 * granted, given the integration's oauth auth method.113 * granted, given the integration's oauth auth method.
114 *114 *
115 * Spec-derived oauth scopes are the full per-operation catalog union (an OpenAPI115 * Spec-derived oauth scopes are the full per-operation catalog union (an OpenAPI
116 * source like PostHog declares hundreds): requested broadly to unlock as many116 * integration like PostHog declares hundreds): requested broadly to unlock as many
117 * tools as possible, but none individually required. A provider that narrows the117 * tools as possible, but none individually required. A provider that narrows the
118 * grant to the user's actual access is healthy, so the spec catalog must NOT118 * grant to the user's actual access is healthy, so the spec catalog must NOT
119 * drive a reconnect prompt. Custom (user-configured) scopes are intentional and119 * drive a reconnect prompt. Custom (user-configured) scopes are intentional and
scripts/agent-config-smoke.ts+5−5
@@ -268,10 +268,10 @@ const runSmoke = async (oauth: OAuthTestServerShape, graph: GraphqlTestServerSha · 267 unmodified lines
268 `OAuth connection was not listed: ${JSON.stringify(connections)}`,268 `OAuth connection was not listed: ${JSON.stringify(connections)}`,
269 );269 );
270 270
271 console.log("[agent-config-smoke] add OAuth-backed GraphQL source through approval flow");271 console.log("[agent-config-smoke] add OAuth-backed GraphQL integration through approval flow");
272 const addSource = await approvePausedCall(272 const addIntegration = await approvePausedCall(
273 ctx,273 ctx,
274 await callTool(ctx, ["executor", "graphql", "addSource"], {274 await callTool(ctx, ["executor", "graphql", "addIntegration"], {
275 scope: ctx.scopeDir,275 scope: ctx.scopeDir,
276 endpoint: graph.endpoint,276 endpoint: graph.endpoint,
277 name: "Agent Smoke GraphQL",277 name: "Agent Smoke GraphQL",
@@ -297,8 +297,8 @@ const runSmoke = async (oauth: OAuthTestServerShape, graph: GraphqlTestServerSha · 19 unmodified lines
297 },297 },
298 }),298 }),
299 );299 );
300 const added = parseJsonOutput<{ readonly ok: boolean }>(addSource);300 const added = parseJsonOutput<{ readonly ok: boolean }>(addIntegration);
301 assert(added.ok === true, `GraphQL addSource failed: ${addSource.text}`);301 assert(added.ok === true, `GraphQL addIntegration failed: ${addIntegration.text}`);
302 302
303 console.log("[agent-config-smoke] invoke configured OAuth-backed tool");303 console.log("[agent-config-smoke] invoke configured OAuth-backed tool");
304 const invoked = parseJsonOutput<{ readonly ok: boolean; readonly data?: unknown }>(304 const invoked = parseJsonOutput<{ readonly ok: boolean; readonly data?: unknown }>(
tests/presets-reachable.test.ts+1−1
@@ -176,7 +176,7 @@ describe("public preset URLs are detected by the correct plugin", () => { · 175 unmodified lines
176 () =>176 () =>
177 Effect.gen(function* () {177 Effect.gen(function* () {
178 const executor = yield* makeExecutor();178 const executor = yield* makeExecutor();
179 const results = yield* executor.sources.detect(preset.url);179 const results = yield* executor.integrations.detect(preset.url);
180 180
181 expect(181 expect(
182 results.length,182 results.length,
tests/tools-cli.test.ts+5−5
@@ -6,7 +6,7 @@ import { · 5 unmodified lines
6 buildToolPath,6 buildToolPath,
7 filterToolPathChildren,7 filterToolPathChildren,
8 buildInvokeToolCode,8 buildInvokeToolCode,
9 buildListSourcesCode,9 buildListIntegrationsCode,
10 buildSearchToolsCode,10 buildSearchToolsCode,
11 extractPausedInteraction,11 extractPausedInteraction,
12 extractExecutionId,12 extractExecutionId,
@@ -54,19 +54,19 @@ describe("CLI tooling helpers", () => { · 41 unmodified lines
54 expect(() => buildToolPath(["github", "issues", "create now"])).toThrow();54 expect(() => buildToolPath(["github", "issues", "create now"])).toThrow();
55 });55 });
56 56
57 it("builds search and sources code snippets", () => {57 it("builds search and integrations code snippets", () => {
58 const searchCode = buildSearchToolsCode({58 const searchCode = buildSearchToolsCode({
59 query: "google calendar events",59 query: "google calendar events",
60 namespace: "google",60 namespace: "google",
61 limit: 5,61 limit: 5,
62 });62 });
63 const sourcesCode = buildListSourcesCode({ query: "google", limit: 20 });63 const integrationsCode = buildListIntegrationsCode({ query: "google", limit: 20 });
64 64
65 expect(searchCode).toBe(65 expect(searchCode).toBe(
66 'return await tools.search({"query":"google calendar events","limit":5,"namespace":"google"});',66 'return await tools.search({"query":"google calendar events","limit":5,"namespace":"google"});',
67 );67 );
68 expect(sourcesCode).toBe(68 expect(integrationsCode).toBe(
69 'return await tools.executor.sources.list({"limit":20,"query":"google"});',69 'return await tools.executor.integrations.list({"limit":20,"query":"google"});',
70 );70 );
71 });71 });
72 72
vision.md+25−25
@@ -14,7 +14,7 @@ it safely. · 13 unmodified lines
14- It is **not AI-specific.** Agents are a primary consumer, but the layer is a general way to14- It is **not AI-specific.** Agents are a primary consumer, but the layer is a general way to
15 represent and interop between sources of capability.15 represent and interop between sources of capability.
16- It is **not code-mode-specific.** Code mode is one way to call tools; it isn't the point.16- It is **not code-mode-specific.** Code mode is one way to call tools; it isn't the point.
17- It is a category of **source** and a way to **interop between them** through one set of17- It is a category of **integration** and a way to **interop between them** through one set of
18 concepts.18 concepts.
19 19
20The bet: as work shifts toward agents acting through software, every company needs one layer20The bet: as work shifts toward agents acting through software, every company needs one layer
@@ -30,23 +30,23 @@ multiplayer must be powerful; neither carries the other's weight. · 9 unmodified lines
30 30
31- **Tool** — an id, an optional input schema, and an optional output schema (JSON Schema today;31- **Tool** — an id, an optional input schema, and an optional output schema (JSON Schema today;
32 may grow richer types). The unit of capability. Addressed as32 may grow richer types). The unit of capability. Addressed as
33 `<source>.<scope>.<connection>.<tool>`.33 `<integration>.<scope>.<connection>.<tool>`.
34- **Source** — contains tools. A source is produced by a plugin from some config (an OpenAPI34- **Integration** — contains tools. An integration is produced by a plugin from some config (an OpenAPI
35 spec, a GraphQL endpoint, an MCP server, a CLI's install/run). The core never sees the raw35 spec, a GraphQL endpoint, an MCP server, a CLI's install/run). The core never sees the raw
36 config — only a normalized manifest.36 config — only a normalized manifest.
37- **Connection** — a credential, born wired, identified by `(scope, source, name)`. The `name`37- **Connection** — a credential, born wired, identified by `(scope, integration, name)`. The `name`
38 is required and load-bearing (the account: `work`, `personal`, `prod`). One source holds many38 is required and load-bearing (the account: `work`, `personal`, `prod`). One integration holds many
39 connections.39 connections.
40- **Secret** — never lives in Executor and never reaches the agent. A connection holds a40- **Secret** — never lives in Executor and never reaches the agent. A connection holds a
41 `SecretRef` (a pointer — `op://`, `keychain://`, `env://`, `vault://`); a provider resolves it41 `SecretRef` (a pointer — `op://`, `keychain://`, `env://`, `vault://`); a provider resolves it
42 at call time, in trusted space, behind a proxy.42 at call time, in trusted space, behind a proxy.
43- **Scope** — placement and identity. An ordered, merged set (see _Scopes_). Every record is43- **Scope** — placement and identity. An ordered, merged set (see _Scopes_). Every record is
44 placed in a scope.44 placed in a scope.
45- **Policy** — gates execution (allow / require-approval / block). Attached to sources and45- **Policy** — gates execution (allow / require-approval / block). Attached to integrations and
46 connections (see _Policies_).46 connections (see _Policies_).
47- **Plugin** — registers sources, tools, providers, storage, surfaces. The one open extension47- **Plugin** — registers integrations, tools, providers, storage, surfaces. The one open extension
48 seam (see _The model_).48 seam (see _The model_).
49- **Manager / Invoker** — the core runtime roles: a manager owns a source's tools and config; an49- **Manager / Invoker** — the core runtime roles: a manager owns an integration's tools and config; an
50 invoker executes a tool through the right connection and proxy.50 invoker executes a tool through the right connection and proxy.
51 51
52## The model — core, first-party, one seam, surfaces52## The model — core, first-party, one seam, surfaces
@@ -63,12 +63,12 @@ generic plugin registry: toolkits, the MCP host, generative UI, workflows, stora · 10 unmodified lines
63audit/runs, the internal-apps catalog. They're separable but they don't go through an abstract63audit/runs, the internal-apps catalog. They're separable but they don't go through an abstract
64plugin contract, because nobody outside you needs to add a new _kind_ of them.64plugin contract, because nobody outside you needs to add a new _kind_ of them.
65 65
66**One open plugin seam: sources.** There are thousands of APIs; you'll never enumerate them; you66**One open plugin seam: integrations.** There are thousands of APIs; you'll never enumerate them; you
67want others to add them. That is where an open contract (`resolveTools` / `invokeTool`) earns its67want others to add them. That is where an open contract (`resolveTools` / `invokeTool`) earns its
68complexity. Secret **providers** are the one plausible second seam, for the same reason.68complexity. Secret **providers** are the one plausible second seam, for the same reason.
69 69
70> The discipline: do not make "everything a plugin." Keep first-party capabilities first-party,70> The discipline: do not make "everything a plugin." Keep first-party capabilities first-party,
71> and keep exactly one open seam (sources, + maybe providers). Reversible — extract a new seam71> and keep exactly one open seam (integrations, + maybe providers). Reversible — extract a new seam
72> only when several first-party capabilities demand the same one.72> only when several first-party capabilities demand the same one.
73 73
74**Surfaces / hosts** serve the executor over a wire: MCP, HTTP, CLI, stdio, triggers, the web74**Surfaces / hosts** serve the executor over a wire: MCP, HTTP, CLI, stdio, triggers, the web
@@ -128,11 +128,11 @@ is to _attach_ a record to a scope. · 53 unmodified lines
128## Connections, secrets & auth128## Connections, secrets & auth
129 129
130- **Connection = a credential, born wired** (secret + account fused; no slot/binding split),130- **Connection = a credential, born wired** (secret + account fused; no slot/binding split),
131 identified by `(scope, source, name)` with `name` required.131 identified by `(scope, integration, name)` with `name` required.
132- **Multiple accounts per source per scope** (distinct names). Per-member auth (each connects132- **Multiple accounts per integration per scope** (distinct names). Per-member auth (each connects
133 their own) and team-level shared auth (one credential placed in an outer scope) are both just133 their own) and team-level shared auth (one credential placed in an outer scope) are both just
134 placement.134 placement.
135- **Auth template vs credential.** A source declares _how_ it authenticates (OAuth / bearer /135- **Auth template vs credential.** An integration declares _how_ it authenticates (OAuth / bearer /
136 API key — a discriminated set); each connection fills that template.136 API key — a discriminated set); each connection fills that template.
137- **Secrets never reach the agent.** Resolved by a provider at call time, in trusted space,137- **Secrets never reach the agent.** Resolved by a provider at call time, in trusted space,
138 behind a **tool-proxy** — credentials never enter agent or sandbox code. **No escape hatch:** a138 behind a **tool-proxy** — credentials never enter agent or sandbox code. **No escape hatch:** a
@@ -141,7 +141,7 @@ is to _attach_ a record to a scope. · 2 unmodified lines
141 separately. **Credential lifecycle** (refresh, rotation, revocation, expiry) handled below the141 separately. **Credential lifecycle** (refresh, rotation, revocation, expiry) handled below the
142 agent.142 agent.
143 143
144## Sources & source kinds144## Integrations & integration kinds
145 145
146Each kind is added through the **one open seam** (`resolveTools` + `invokeTool`): **OpenAPI**,146Each kind is added through the **one open seam** (`resolveTools` + `invokeTool`): **OpenAPI**,
147**GraphQL**, **MCP servers**, **CLI** (install + run, no spec), **direct API / raw-fetch**, and147**GraphQL**, **MCP servers**, **CLI** (install + run, no spec), **direct API / raw-fetch**, and
@@ -150,25 +150,25 @@ reasons over a **normalized manifest** (schemas, side-effect class, auth templat · 2 unmodified lines
150required capabilities, an LLM-authored description). **Spec auto-refresh** tracks upstream drift150required capabilities, an LLM-authored description). **Spec auto-refresh** tracks upstream drift
151and **must never auto-expand authority** — new operations appear _ungranted until reviewed_.151and **must never auto-expand authority** — new operations appear _ungranted until reviewed_.
152 152
153An **integrations registry** lets people discover and add sources.153An **integrations registry** lets people discover and add integrations.
154 154
155## Policies155## Policies
156 156
157Policies gate **execution** — `approve` / `require_approval` / `block` — and are distinct from157Policies gate **execution** — `approve` / `require_approval` / `block` — and are distinct from
158toolkits (which gate visibility). Resolution is _most-restrictive-wins_; **plugin defaults** let158toolkits (which gate visibility). Resolution is _most-restrictive-wins_; **plugin defaults** let
159a source ship a tool pre-marked `require_approval` (destructive detection), with `approve` as the159an integration ship a tool pre-marked `require_approval` (destructive detection), with `approve` as the
160override when an import wrongly flags a safe action.160override when an import wrongly flags a safe action.
161 161
162How people actually use them today (real usage): glob patterns over the address, where _every_162How people actually use them today (real usage): glob patterns over the address, where _every_
163rule targets a **source** and wildcards the scope/connection (`stripe_api.*.*.disputes.*`,163rule targets an **integration** and wildcards the scope/connection (`stripe_api.*.*.disputes.*`,
164`pscale.*.*.execute_write_query`) — i.e. faking structural targeting because there's nowhere to164`pscale.*.*.execute_write_query`) — i.e. faking structural targeting because there's nowhere to
165attach it. The direction:165attach it. The direction:
166 166
167- **Kill the global glob list. Attach policy to the source and connection records** — the two167- **Kill the global glob list. Attach policy to the integration and connection records** — the two
168 structural levels. **Connection overrides source** (innermost-wins), with **scope authority168 structural levels. **Connection overrides integration** (innermost-wins), with **scope authority
169 layered on top** (outer/org mandatory, can't be weakened inward).169 layered on top** (outer/org mandatory, can't be weakened inward).
170- Fixing a mis-flagged tool becomes `approve` _on that source/connection_, directly.170- Fixing a mis-flagged tool becomes `approve` _on that integration/connection_, directly.
171- **Toolkits carry no policy** — they're pure curation and inherit whatever source/connection171- **Toolkits carry no policy** — they're pure curation and inherit whatever integration/connection
172 policies apply to the tools they expose. This dissolves the old global-vs-toolkit merge172 policies apply to the tools they expose. This dissolves the old global-vs-toolkit merge
173 conflict.173 conflict.
174 174
@@ -230,7 +230,7 @@ You build once; surfaces project. · 55 unmodified lines
230 draw on, that **surfaces in tool search**, and can **run tools inline** to cut round-trips.230 draw on, that **surfaces in tool search**, and can **run tools inline** to cut round-trips.
231 Served off MCP and from a `.well-known`. File-backed (see _Authoring model_).231 Served off MCP and from a `.well-known`. File-backed (see _Authoring model_).
232- **Internal apps catalog & custom UI snippets.** Store and share custom UI and internal apps.232- **Internal apps catalog & custom UI snippets.** Store and share custom UI and internal apps.
233- **Configure Executor via Executor** — managing sources, connections, scopes, policies,233- **Configure Executor via Executor** — managing integrations, connections, scopes, policies,
234 toolkits is itself done through tools.234 toolkits is itself done through tools.
235 235
236## Authoring model — two paths236## Authoring model — two paths
@@ -309,7 +309,7 @@ service install` (daemon), `executor web` (web app) — one instance, delivered · 72 unmodified lines
309The hosted product runs on a **Cloudflare substrate** and lets users **write plugins and load309The hosted product runs on a **Cloudflare substrate** and lets users **write plugins and load
310them dynamically, sandboxed** — each in a Worker isolate with a **declared capability manifest**310them dynamically, sandboxed** — each in a Worker isolate with a **declared capability manifest**
311(a plugin asking for specific scopes can do exactly that and nothing else). User plugins ride the311(a plugin asking for specific scopes can do exactly that and nothing else). User plugins ride the
312**source seam** and the capability membrane, opened **last and curated**. Local == remote via312**integration seam** and the capability membrane, opened **last and curated**. Local == remote via
313**substrate substitution**: a local V8 isolate stands in for the dynamic worker, Git for313**substrate substitution**: a local V8 isolate stands in for the dynamic worker, Git for
314Cloudflare Artifacts, local SQLite for hosted storage.314Cloudflare Artifacts, local SQLite for hosted storage.
315 315
@@ -329,7 +329,7 @@ Cloudflare Artifacts, local SQLite for hosted storage. · 13 unmodified lines
329Built on **Effect**. The same SDK is in-memory _and_ client/server; SDK-only users need no329Built on **Effect**. The same SDK is in-memory _and_ client/server; SDK-only users need no
330server; serving is a standard Web `Request → Response` handler. The discipline that keeps breadth330server; serving is a standard Web `Request → Response` handler. The discipline that keeps breadth
331cheap is the **seam discipline**: the platform owns generic concerns (auth, caching, retries,331cheap is the **seam discipline**: the platform owns generic concerns (auth, caching, retries,
332schema, rendering, transport, the Run); a plugin owns only the source-specific resolve/invoke.332schema, rendering, transport, the Run); a plugin owns only the integration-specific resolve/invoke.
333 333
334## Principles334## Principles
335 335
@@ -356,7 +356,7 @@ schema, rendering, transport, the Run); a plugin owns only the source-specific r · 20 unmodified lines
356 nearest-to-substrate first: MCP → storage → workflows → generative UI → user plugins (curated,356 nearest-to-substrate first: MCP → storage → workflows → generative UI → user plugins (curated,
357 last). Three substrate invariants keep composition cheap: one typed invocation primitive;357 last). Three substrate invariants keep composition cheap: one typed invocation primitive;
358 authorization inherited from the substrate, never re-implemented; a hard seam between358 authorization inherited from the substrate, never re-implemented; a hard seam between
359 substrate-generic and source-specific code.359 substrate-generic and integration-specific code.
360- **Vision mode vs build mode.** This document is vision mode — generativity ("and then you'd360- **Vision mode vs build mode.** This document is vision mode — generativity ("and then you'd
361 want X") is the point. In **build mode, every "and then you'd want X" is a YAGNI to defer**, not361 want X") is the point. In **build mode, every "and then you'd want X" is a YAGNI to defer**, not
362 a dependency to satisfy. Build the smallest thing useful on its own; let the next be _pulled_ by362 a dependency to satisfy. Build the smallest thing useful on its own; let the next be _pulled_ by