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.
Unified · Split
| @@ -65,7 +65,7 @@ a given Git URL into `.reference` and pull latest before using it. · 64 unmodified lines | |||
| 65 | 65 | ||
| 66 | ## Package Roles | 66 | ## 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-method | 69 | policies, fixtures. `@executor-js/sdk/http-auth` carries the shared auth-method |
| 70 | vocabulary the HTTP protocol plugins compose (core never imports it, keeping it | 70 | vocabulary the HTTP protocol plugins compose (core never imports it, keeping it |
| 71 | carrier-agnostic). | 71 | carrier-agnostic). |
| @@ -21,9 +21,9 @@ set its policy once, and every agent shares it over MCP. Your integrations, · 20 unmodified lines | |||
| 21 | auth, and policies live in one place instead of being scattered across each | 21 | auth, and policies live in one place instead of being scattered across each |
| 22 | client. | 22 | client. |
| 23 | 23 | ||
| 24 | - **Any source.** First-party support for MCP servers, OpenAPI, GraphQL, and | 24 | - **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 an | 25 | 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 same | 27 | - **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, or | 29 | - **Governed by policy.** Each tool is allowed, gated behind approval, or |
| @@ -100,12 +100,12 @@ appear. · 70 unmodified lines | |||
| 100 | 100 | ||
| 101 | ## Add an integration | 101 | ## Add an integration |
| 102 | 102 | ||
| 103 | From the web UI, click **Add Source**, paste an OpenAPI, GraphQL, or MCP URL, | 103 | From the web UI, click **Add Integration**, paste an OpenAPI, GraphQL, or MCP URL, |
| 104 | and Executor detects the type, indexes the tools, and handles auth. Or add one | 104 | and Executor detects the type, indexes the tools, and handles auth. Or add one |
| 105 | from the CLI: | 105 | from the CLI: |
| 106 | 106 | ||
| 107 | ```bash | 107 | ```bash |
| 108 | executor call executor openapi addSource '{ | 108 | executor 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 | ||
| 115 | Use `baseUrl` when the OpenAPI document has relative `servers` entries (for | 115 | Use `baseUrl` when the OpenAPI document has relative `servers` entries (for |
| 116 | example `"/api/v3"`). Confirm it is live with `executor tools sources`. | 116 | example `"/api/v3"`). Confirm it is live with `executor tools integrations`. |
| 117 | 117 | ||
| 118 | ## Using tools | 118 | ## Using tools |
| 119 | 119 | ||
| @@ -169,7 +169,7 @@ executor call <path...> --help # browse namespaces/resources/methods · 49 unmodified lines | |||
| 169 | executor call <path...> --help --match "<text>" --limit <n> # narrow huge namespaces | 169 | executor call <path...> --help --match "<text>" --limit <n> # narrow huge namespaces |
| 170 | executor resume --execution-id <id> # resume paused execution | 170 | executor resume --execution-id <id> # resume paused execution |
| 171 | executor tools search "<query>" # search tools by intent | 171 | executor tools search "<query>" # search tools by intent |
| 172 | executor tools sources # list configured sources + tool counts | 172 | executor tools integrations # list configured integrations + tool counts |
| 173 | executor tools describe <path> # show tool TypeScript/JSON schema | 173 | executor tools describe <path> # show tool TypeScript/JSON schema |
| 174 | ``` | 174 | ``` |
| 175 | 175 | ||
| @@ -190,7 +190,7 @@ apps/ · 14 unmodified lines | |||
| 190 | packages/ | 190 | packages/ |
| 191 | core/ contracts, plugin wiring, scopes, policies, SDK, API, CLI core | 191 | 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 UI | 196 | react/ shared React UI |
| @@ -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 | ||
| 2055 | const toolsSourcesCommand = Command.make( | 2055 | const 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 | ||
| 2082 | const toolsDescribeCommand = Command.make( | 2082 | const 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 | ||
| 2104 | const toolsCommand = Command.make("tools").pipe( | 2104 | const 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 | ||
| 2109 | const profileConnectionInput = (input: { | 2113 | const profileConnectionInput = (input: { |
| @@ -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 | ||
| 411 | export const buildListSourcesCode = (input: { query?: string; limit: number }): string => { | 411 | export 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 | ||
| 421 | export const buildDescribeToolCode = (toolPath: string): string => | 421 | export const buildDescribeToolCode = (toolPath: string): string => |
| @@ -2,7 +2,7 @@ · 1 unmodified line | |||
| 2 | // merge of cloud + executor schemas. If ANY call site builds its own | 2 | // merge of cloud + executor schemas. If ANY call site builds its own |
| 3 | // `{ schema }` without spreading both, `db._.fullSchema` comes back | 3 | // `{ schema }` without spreading both, `db._.fullSchema` comes back |
| 4 | // missing tables and the drizzle adapter throws | 4 | // 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` built | 7 | // We hit this in prod (MCP endpoint) when `mcp-session.ts` built |
| 8 | // `combinedSchema = { ...cloudSchema }` and forgot to spread the executor | 8 | // `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). If | 59 | // 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)) { |
| @@ -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 schema | 25 | // Exported so every drizzle() call in the cloud app shares one schema |
| 26 | // object. Historically `mcp-session.ts` built its own and forgot to spread | 26 | // object. Historically `mcp-session.ts` built its own and forgot to spread |
| 27 | // `executorSchema`, producing runtime "unknown model source" errors that | 27 | // `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. |
| 29 | export const combinedSchema = { ...cloudSchema, ...executorSchema }; | 29 | export const combinedSchema = { ...cloudSchema, ...executorSchema }; |
| 30 | 30 | ||
| @@ -52,7 +52,7 @@ const EMPTY_INPUT_SCHEMA = Schema.toStandardSchemaV1( · 51 unmodified lines | |||
| 52 | const elicitingTestPlugin = definePlugin(() => ({ | 52 | const 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", |
| @@ -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 | <a | 20 | <a |
| 19 | href={loginHref} | 21 | href={loginHref} |
| @@ -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 renders | 68 | // 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-run | 69 | // (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 SSR | 70 | // 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 one | 71 | // 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 heap | 72 | // without freeing it, so a handful of reloads exhausts the worker's heap |
| @@ -4,7 +4,7 @@ description: "An integration is something Executor connects to, defined by an MC · 3 unmodified lines | |||
| 4 | --- | 4 | --- |
| 5 | 5 | ||
| 6 | An **integration** is something Executor can connect to. Each integration is defined by | 6 | An **integration** is something Executor can connect to. Each integration is defined by |
| 7 | a source that describes the available tools: | 7 | a spec that describes the available tools: |
| 8 | 8 | ||
| 9 | - an **MCP server** | 9 | - an **MCP server** |
| 10 | - an **OpenAPI** specification | 10 | - an **OpenAPI** specification |
| @@ -74,12 +74,12 @@ the CLI: · 73 unmodified lines | |||
| 74 | 74 | ||
| 75 | ## Add an integration | 75 | ## Add an integration |
| 76 | 76 | ||
| 77 | From the web UI, click **Add Source** and paste an OpenAPI, GraphQL, or MCP URL. | 77 | From the web UI, click **Add Integration** and paste an OpenAPI, GraphQL, or MCP URL. |
| 78 | Executor detects the type, indexes the tools, and handles auth. Or add one from the | 78 | Executor detects the type, indexes the tools, and handles auth. Or add one from the |
| 79 | CLI: | 79 | CLI: |
| 80 | 80 | ||
| 81 | ```bash | 81 | ```bash |
| 82 | executor call executor openapi addSource '{ | 82 | executor 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 | |||
| 91 | Confirm it's live: | 91 | Confirm it's live: |
| 92 | 92 | ||
| 93 | ```bash | 93 | ```bash |
| 94 | executor tools sources # lists configured integrations + tool counts | 94 | executor tools integrations # lists configured integrations + tool counts |
| 95 | ``` | 95 | ``` |
| 96 | 96 | ||
| 97 | ## Call tools | 97 | ## Call tools |
| @@ -63,7 +63,7 @@ afterAll(() => dispose()); · 62 unmodified lines | |||
| 63 | 63 | ||
| 64 | const BASE = "http://localhost:4788"; | 64 | const BASE = "http://localhost:4788"; |
| 65 | 65 | ||
| 66 | const addOrgSource = async (organizationId: string): Promise<void> => { | 66 | const addOrgIntegration = async (organizationId: string): Promise<void> => { |
| 67 | // Register the integration and attach an org-owned connection, on its own | 67 | // 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 to | 68 | // connection to the shared DB file. WAL makes the committed rows visible to |
| 69 | // the server. Org-owned connections (and their per-connection tools) are | 69 | // 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( |
File renamed without changes.
| @@ -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 | ||
| 74 | const MissingAuthSourceApi = HttpApi.make("localAuthFailureSource").add(PingGroup); | 74 | const MissingAuthIntegrationApi = HttpApi.make("localAuthFailureIntegration").add(PingGroup); |
| 75 | 75 | ||
| 76 | const TestApi = addGroup(OpenApiGroup); | 76 | const TestApi = addGroup(OpenApiGroup); |
| 77 | type TestApiShape = | 77 | type 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 | { |
| @@ -44,7 +44,7 @@ const EmptyInputSchema = Schema.toStandardSchemaV1( · 43 unmodified lines | |||
| 44 | const approvalPlugin = definePlugin(() => ({ | 44 | const 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", |
| @@ -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 can | 77 | // 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"); |
| @@ -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, email | 159 | 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> |
| @@ -351,9 +351,9 @@ Do not use a uniform icon pack (Tabler, Lucide-as-decoration, Heroicons) or · 350 unmodified lines | |||
| 351 | monogram-in-rounded-square placeholders: both read as generic, generated UI. Identity comes from | 351 | monogram-in-rounded-square placeholders: both read as generic, generated UI. Identity comes from |
| 352 | authentic specifics or from nothing at all. | 352 | authentic 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`, rendered | 355 | `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 only | 356 | 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 | |||
| 376 | Copy is part of the design; keep it precise, technical, and free of filler. | 376 | Copy 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`), never | 379 | - 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 is | 381 | - Errors are what happened plus what to do next: `Couldn't reach the integration. Check the server is |
| 382 | running, then retry.` | 382 | running, then retry.` |
| 383 | - Toasts name the specific thing that changed, drop the trailing period, never say | 383 | - 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 across | 385 | - Empty states point to the first action: `No integrations yet. Add one to start sharing tools across |
| 386 | your agents.` | 386 | your 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, |
| @@ -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` |
| @@ -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 | ||
| @@ -6,7 +6,7 @@ · 5 unmodified lines | |||
| 6 | // URL, confirms the code, and clicks "Authorize device" on the WorkOS | 6 | // URL, confirms the code, and clicks "Authorize device" on the WorkOS |
| 7 | // emulator's verification page, exactly the human hop, the way the MCP | 7 | // 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 WorkOS | 9 | // 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. |
| 11 | import { spawn } from "node:child_process"; | 11 | import { spawn } from "node:child_process"; |
| 12 | import { readFileSync } from "node:fs"; | 12 | import { readFileSync } from "node:fs"; |
| @@ -83,12 +83,12 @@ scenario( · 70 unmodified lines | |||
| 83 | 83 | ||
| 84 | // The terminal journey, recorded to terminal.cast. `&&` means a clean | 84 | // The terminal journey, recorded to terminal.cast. `&&` means a clean |
| 85 | // exit only happens if every step, including the authenticated /api call | 85 | // 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], |
File renamed without changes.
File renamed without changes.
| @@ -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()); |
| @@ -1,5 +1,5 @@ | |||
| 1 | // The toolkit-connect performance bug, reproduced with the REAL opencode | 1 | // 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 runs | 3 | // 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 session | 4 | // 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 a | 5 | // 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 — large | 41 | // 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 connect | 42 | // 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); |
| @@ -7,12 +7,12 @@ · 6 unmodified lines | |||
| 7 | // the policy engine over the WHOLE catalog to decide tool visibility. A | 7 | // 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 catalog | 8 | // 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 dozen | 9 | // 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 the | 10 | // 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 plus | 13 | // 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 over | 14 | // 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 to | 15 | // 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 pays | 16 | // 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-walk | 17 | // 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 in | 18 | // 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,200 | 144 | // 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 completes | 145 | // 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 as | 146 | // 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); |
| @@ -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( |
| @@ -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) — proves | 38 | // 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. |
| @@ -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 and | 89 | // The console renders past the auth gate: the Integrations page and |
| 90 | // its built-in Executor source row load (proves auth + data, not | 90 | // 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 }); |
| @@ -4,7 +4,7 @@ · 3 unmodified lines | |||
| 4 | // each rendered from its own credential input; the merge-append configureAuth | 4 | // 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-method | 6 | // declaring a method on a server that advertises none; GraphQL's multi-method |
| 7 | // add — and the EXTREME case end-to-end: a source declaring | 7 | // 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 against | 9 | // 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 | ||
| 194 | scenario( | 194 | scenario( |
| 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, every | 242 | // 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 | ||
| 250 | scenario( | 250 | scenario( |
| 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* () { |
| @@ -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 : [], |
| @@ -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", |
| @@ -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", |
| @@ -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 artifact | 4 | // 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 | ||
| 24 | scenario( | 24 | scenario( |
| 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 submit | 57 | // The reported ghost was the bar painting doubled when the submit |
| 58 | // button changed width on click. The single-node counts (above and | 58 | // button changed width on click. The single-node counts (above and |
File renamed without changes.
| @@ -1,5 +1,5 @@ | |||
| 1 | // Cross-target (browser): the UI side of per-call server selection. When a | 1 | // 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 turns | 2 | // 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 an | 3 | // 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 session | 4 | // optional override — the host is otherwise resolved per tool call. The session |
| 5 | // video + per-step screenshots are the artifact; this scenario skips on targets | 5 | // 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 | }); |
| @@ -5,7 +5,7 @@ · 4 unmodified lines | |||
| 5 | // by running the policy engine over the WHOLE catalog on connect (the | 5 | // by running the policy engine over the WHOLE catalog on connect (the |
| 6 | // getDescription -> connections.list -> toolsList path), not just the toolkit's | 6 | // 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 total | 7 | // 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 to | 8 | // 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 SAME | 11 | // `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 | ||
| 68 | export interface SeedSource { | 68 | export 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 | ||
| 75 | export interface SeedPlan { | 75 | export 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 | ||
| 83 | export interface SeedOptions { | 83 | export 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 + 10 | 93 | * 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, where | 94 | * 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 | */ |
| 98 | export const planLargeCatalog = (options: SeedOptions = {}): SeedPlan => { | 98 | export 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 per | 153 | * 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 tool | 154 | * 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 cost | 155 | * 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( |
| @@ -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 | "", |
| @@ -4,7 +4,7 @@ · 3 unmodified lines | |||
| 4 | // discovers Better Auth's device endpoints via /api/auth/cli-login, prints the | 4 | // 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): Playwright | 5 | // 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 clicks | 6 | // 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`; a | 7 | // "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 a | 8 | // 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. |
| 10 | import { readFileSync } from "node:fs"; | 10 | import { 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 the | 115 | // 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. |
| @@ -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 | <Link | 208 | <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 | |||
| 268 | export function Shell() { | 268 | export 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"> |
| @@ -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 works | 19 | * If omitted, no plugin UIs get bundled — the renderer still works |
| 20 | * for built-in pages but Sources / Connections / etc. plugin pages | 20 | * 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; |
| @@ -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 through | 61 | * …). Extracted via inference so adding a core group flows through |
| 62 | * without touching this helper. */ | 62 | * without touching this helper. */ |
| 63 | type CoreGroups = | 63 | type CoreGroups = |
| @@ -7,8 +7,8 @@ import { join } from "node:path"; · 6 unmodified lines | |||
| 7 | import { ExecutorFileConfig } from "./schema"; | 7 | import { ExecutorFileConfig } from "./schema"; |
| 8 | import { ConfigParseError, loadConfig } from "./load"; | 8 | import { ConfigParseError, loadConfig } from "./load"; |
| 9 | import { | 9 | import { |
| 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 | ||
| 28 | describe("ExecutorFileConfig schema", () => { | 28 | describe("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 comment | 107 | // 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 | ||
| 142 | describe("write operations", () => { | 142 | describe("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 config | 200 | // 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 spec | 236 | // 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 2 | 244 | // 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", |
| @@ -1,11 +1,11 @@ | |||
| 1 | export { | 1 | export { |
| 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 | |||
| 17 | export type { LoadPluginsFromJsoncOptions } from "./load-plugins"; | 17 | export type { LoadPluginsFromJsoncOptions } from "./load-plugins"; |
| 18 | 18 | ||
| 19 | export { | 19 | export { |
| 20 | addSourceToConfig, | 20 | addIntegrationToConfig, |
| 21 | removeSourceFromConfig, | 21 | removeIntegrationFromConfig, |
| 22 | writeConfig, | 22 | writeConfig, |
| 23 | addSecretToConfig, | 23 | addSecretToConfig, |
| 24 | removeSecretFromConfig, | 24 | removeSecretFromConfig, |
| @@ -23,26 +23,26 @@ export type ConfigHeaderValue = typeof ConfigHeaderValue.Type; · 22 unmodified lines | |||
| 23 | const ConfigHeaders = Schema.Record(Schema.String, ConfigHeaderValue); | 23 | const 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 | ||
| 29 | export const OpenApiSourceConfig = Schema.Struct({ | 29 | export 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 | }); |
| 36 | export type OpenApiSourceConfig = typeof OpenApiSourceConfig.Type; | 36 | export type OpenApiIntegrationConfig = typeof OpenApiIntegrationConfig.Type; |
| 37 | 37 | ||
| 38 | export const GraphqlSourceConfig = Schema.Struct({ | 38 | export 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 | }); |
| 45 | export type GraphqlSourceConfig = typeof GraphqlSourceConfig.Type; | 45 | export type GraphqlIntegrationConfig = typeof GraphqlIntegrationConfig.Type; |
| 46 | 46 | ||
| 47 | const StringMap = Schema.Record(Schema.String, Schema.String); | 47 | const StringMap = Schema.Record(Schema.String, Schema.String); |
| 48 | 48 | ||
| @@ -66,7 +66,7 @@ export const McpAuthConfig = Schema.Union([ · 17 unmodified lines | |||
| 66 | ]); | 66 | ]); |
| 67 | export type McpAuthConfig = typeof McpAuthConfig.Type; | 67 | export type McpAuthConfig = typeof McpAuthConfig.Type; |
| 68 | 68 | ||
| 69 | export const McpRemoteSourceConfig = Schema.Struct({ | 69 | export 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 | }); |
| 80 | export type McpRemoteSourceConfig = typeof McpRemoteSourceConfig.Type; | 80 | export type McpRemoteIntegrationConfig = typeof McpRemoteIntegrationConfig.Type; |
| 81 | 81 | ||
| 82 | export const McpStdioSourceConfig = Schema.Struct({ | 82 | export 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 | }); |
| 92 | export type McpStdioSourceConfig = typeof McpStdioSourceConfig.Type; | 92 | export type McpStdioIntegrationConfig = typeof McpStdioIntegrationConfig.Type; |
| 93 | 93 | ||
| 94 | export const SourceConfig = Schema.Union([ | 94 | export const IntegrationConfig = Schema.Union([ |
| 95 | OpenApiSourceConfig, | 95 | OpenApiIntegrationConfig, |
| 96 | GraphqlSourceConfig, | 96 | GraphqlIntegrationConfig, |
| 97 | McpRemoteSourceConfig, | 97 | McpRemoteIntegrationConfig, |
| 98 | McpStdioSourceConfig, | 98 | McpStdioIntegrationConfig, |
| 99 | ]); | 99 | ]); |
| 100 | export type SourceConfig = typeof SourceConfig.Type; | 100 | export type IntegrationConfig = typeof IntegrationConfig.Type; |
| 101 | 101 | ||
| 102 | // --------------------------------------------------------------------------- | 102 | // --------------------------------------------------------------------------- |
| 103 | // Secret metadata | 103 | // 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 | }); |
| 142 | export type ExecutorFileConfig = typeof ExecutorFileConfig.Type; | 142 | export type ExecutorFileConfig = typeof ExecutorFileConfig.Type; |
| @@ -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 DB | 4 | // Plugins (openapi, graphql, mcp) call `sink.upsertIntegration` after their DB |
| 5 | // writes so the committable file stays in sync with runtime state. Errors | 5 | // 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 DB | 6 | // 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) will | 7 | // mutation, and the next successful mutation (or a boot-time sync) will |
| @@ -15,8 +15,8 @@ import { Effect } from "effect"; · 7 unmodified lines | |||
| 15 | import type { Layer } from "effect"; | 15 | import type { Layer } from "effect"; |
| 16 | import type { FileSystem } from "effect"; | 16 | import type { FileSystem } from "effect"; |
| 17 | 17 | ||
| 18 | import { SECRET_REF_PREFIX, type ConfigHeaderValue, type SourceConfig } from "./schema"; | 18 | import { SECRET_REF_PREFIX, type ConfigHeaderValue, type IntegrationConfig } from "./schema"; |
| 19 | import { addSourceToConfig, removeSourceFromConfig } from "./write"; | 19 | import { addIntegrationToConfig, removeIntegrationFromConfig } from "./write"; |
| 20 | 20 | ||
| 21 | // Translate a plugin-side header value (`{ secretId, prefix? }` for secret | 21 | // 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 | ||
| 40 | export interface ConfigFileSink { | 40 | export 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 | ||
| 45 | export interface ConfigFileSinkOptions { | 45 | export 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 | ), |
| @@ -2,7 +2,7 @@ import { Effect } from "effect"; · 1 unmodified line | |||
| 2 | import { FileSystem } from "effect"; | 2 | import { FileSystem } from "effect"; |
| 3 | import type { PlatformError } from "effect/PlatformError"; | 3 | import type { PlatformError } from "effect/PlatformError"; |
| 4 | import * as jsonc from "jsonc-parser"; | 4 | import * as jsonc from "jsonc-parser"; |
| 5 | import type { SourceConfig, ExecutorFileConfig } from "./schema"; | 5 | import type { IntegrationConfig, ExecutorFileConfig } from "./schema"; |
| 6 | 6 | ||
| 7 | export class ConfigWriteError { | 7 | export 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 | ||
| 21 | const DEFAULT_CONFIG = `{ | 21 | const 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 | */ |
| 42 | export const addSourceToConfig = ( | 42 | export 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 exists | 50 | // 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 duplicates | 60 | // 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 removals | 73 | // 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 | */ |
| 91 | export const removeSourceFromConfig = ( | 91 | export 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 removal | 108 | // 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); |
| @@ -31,7 +31,7 @@ const succeedingExecutor: CodeExecutor<FakeRuntimeError> = { · 30 unmodified lines | |||
| 31 | const emptyPlugin = definePlugin(() => ({ | 31 | const 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 | ||
| 37 | const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const })); | 37 | const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const })); |
| @@ -15,7 +15,7 @@ import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor- · 14 unmodified lines | |||
| 15 | import { | 15 | import { |
| 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, |
| @@ -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, |
| @@ -34,7 +34,7 @@ class FakeOpenApiInvocationError extends Data.TaggedError("OpenApiInvocationErro · 33 unmodified lines | |||
| 34 | const leakyPlugin = definePlugin(() => ({ | 34 | const 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", |
| @@ -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, ""), |
| @@ -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 like | 246 | * Extract the integration namespace from a tool path. v2 addresses look like |
| 247 | * `<integration>.<owner>.<connection>.<tool>`; static fqids look like | 247 | * `<integration>.<owner>.<connection>.<tool>`; static fqids look like |
| 248 | * `<source>.<op>`. We take the first segment as a cheap, non-lookup namespace | 248 | * `<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 | */ |
| 251 | const extractNamespace = (path: string): string => { | 251 | const 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 | ||
| 367 | export type ExecutorSourceListItem = { | 367 | export 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 by | 392 | * Page of results from a list-style discovery tool. Shared by |
| 393 | * `tools.search` and `tools.executor.sources.list` so the model sees one | 393 | * `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: the | 677 | /** What `tools.executor.integrations.list()` calls inside the sandbox. v2: the |
| 678 | * "sources" are the integration catalog; tool counts come from the | 678 | * integrations are the integration catalog; tool counts come from the |
| 679 | * per-connection tool list. */ | 679 | * per-connection tool list. */ |
| 680 | export const listExecutorSources = Effect.fn("executor.sources.list")(function* ( | 680 | export 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 the | 733 | // The integration's catalog description — user-editable context the |
| 734 | // agent can use to pick a source. Omitted when it just repeats the | 734 | // 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 | }); |
| @@ -1,6 +1,6 @@ | |||
| 1 | # @executor-js/sdk | 1 | # @executor-js/sdk |
| 2 | 2 | ||
| 3 | A TypeScript SDK for building executors that wire together tool sources, secrets, and policies across MCP, OpenAPI, GraphQL, and custom plugins. | 3 | A TypeScript SDK for building executors that wire together integrations, secrets, and policies across MCP, OpenAPI, GraphQL, and custom plugins. |
| 4 | 4 | ||
| 5 | The default surface is `Promise`-based — plugins are built on [Effect](https://effect.website/) under the hood, but consumers never have to touch it. | 5 | The 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 | ||
| 133 | These plugin packages are published from the monorepo: | 133 | These 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 tools | 136 | - [`@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 tools | 137 | - [`@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 store | 138 | - [`@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 | |||
| 161 | const refs = await executor.secrets.list(); | 161 | const refs = await executor.secrets.list(); |
| 162 | ``` | 162 | ``` |
| 163 | 163 | ||
| 164 | Plugins 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. | 164 | Plugins 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 | ## Status | 166 | ## Status |
| 167 | 167 | ||
| @@ -10,7 +10,7 @@ export type AuthToolFailureCode = · 9 unmodified lines | |||
| 10 | export type AuthToolFailureInput = { | 10 | export 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 to | 34 | // 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 via | 35 | // "bind" to an integration afterward. Manually-entered credentials are created via |
| 36 | // the connection handoff (the user enters the value in the web UI, which | 36 | // 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 by | 37 | // 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 the | 38 | // 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), |
| @@ -1,6 +1,6 @@ | |||
| 1 | // --------------------------------------------------------------------------- | 1 | // --------------------------------------------------------------------------- |
| 2 | // BlobStore — the seam for large, opaque, write-once data. Blobs are stored | 2 | // BlobStore — the seam for large, opaque, write-once data. Blobs are stored |
| 3 | // in FumaDB with their own lifecycle and namespacing, separate from source | 3 | // 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 the | 6 | // Plugins see a `PluginBlobStore` that's already namespaced to the |
| @@ -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 | ||
| @@ -177,7 +177,7 @@ export type EditSheetApplyResult = · 176 unmodified lines | |||
| 177 | | { readonly ok: false }; | 177 | | { readonly ok: false }; |
| 178 | 178 | ||
| 179 | export interface EditSheetSectionProps { | 179 | export 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 — plugin | 199 | /** 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 implement | 218 | * 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 expose | 250 | /** 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 provider | 252 | * 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; |
| @@ -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", |
| @@ -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 | ||
| 1243 | interface StaticTools { | 1243 | interface 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 | ||
| 1256 | const EXECUTOR_SOURCE_ID = "executor"; | 1256 | const EXECUTOR_INTEGRATION_ID = "executor"; |
| 1257 | const EXECUTOR_SOURCE: StaticSourceDecl = { | 1257 | const 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 from | 1821 | // A scoped toolkit must not advertise providers it grants no tools from |
| 1822 | // (mirrors `connectionsList`). Static sources are system namespaces, not | 1822 | // (mirrors `connectionsList`). Static integrations are system namespaces, not |
| 1823 | // user providers, so they stay; DB-backed integrations are filtered to | 1823 | // 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 row | 1847 | 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 tools | 2141 | // Keep the existing catalog — replacing it would wipe working tools |
| 2140 | // over a transient outage — and stamp the sync time anyway so a down | 2142 | // 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-attempts | 2143 | // 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 = mountUnderExecutor | 3887 | 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, |
| @@ -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, |
| @@ -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" }); |
| @@ -608,7 +608,7 @@ export interface V1OpenApiSourceConfig extends V1OpenApiAuthConfig { · 607 unmodified lines | |||
| 608 | 608 | ||
| 609 | export interface V2OpenApiIntegrationConfig { | 609 | export 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 !== undefined | 635 | ...(v1.googleDiscoveryUrls !== undefined |
| 636 | ? { googleDiscoveryUrls: v1.googleDiscoveryUrls } | 636 | ? { googleDiscoveryUrls: v1.googleDiscoveryUrls } |
| @@ -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, and | 147 | // 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 | ||
| 153 | export interface PluginCtx<TStore = unknown> { | 153 | export 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 not | 293 | /** The integration could not be (fully) enumerated: unreachable server, auth not |
| 294 | * ready, listing aborted. The result is non-authoritative, so the executor | 294 | * ready, listing aborted. The result is non-authoritative, so the executor |
| 295 | * keeps the connection's existing persisted catalog instead of replacing it | 295 | * keeps the connection's existing persisted catalog instead of replacing it |
| 296 | * (a transient outage must not wipe working tools). Omit / `false` when the | 296 | * (a transient outage must not wipe working tools). Omit / `false` when the |
| 297 | * listing is authoritative, including a genuine "this source has zero | 297 | * 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 it | 300 | /** 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 | ||
| 360 | export interface StaticToolHandlerInput<TStore = unknown> { | 360 | export interface StaticToolHandlerInput<TStore = unknown> { |
| @@ -433,7 +433,7 @@ export const tool = < · 72 unmodified lines | |||
| 433 | ), | 433 | ), |
| 434 | }); | 434 | }); |
| 435 | 435 | ||
| 436 | export interface StaticSourceDecl<TStore = unknown> { | 436 | export 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 the | 576 | /** Build the plugin's extension API — becomes `executor[plugin.id]` and the |
| 577 | * `self` passed to `staticSources`. Field order matters: `extension` MUST | 577 | * `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; |
| @@ -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, |
| @@ -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 model | 10 | // Not a goal: authoring plugins in Promise style. The plugin model |
| 11 | // (storage, schema, staticSources, Effect ctx) is Effect-only. Bring | 11 | // (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 | ||
| @@ -15,7 +15,7 @@ import { Effect, Schema } from "effect"; · 14 unmodified lines | |||
| 15 | const echoPlugin = definePlugin(() => ({ | 15 | const 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, |
| @@ -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: namespace | 68 | // 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. |
| @@ -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 | }); |
| @@ -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'); |
| @@ -4,7 +4,7 @@ import type { JsonSchema } from "effect/JsonSchema"; · 3 unmodified lines | |||
| 4 | export const ToolRegistration = Schema.Struct({ | 4 | export 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 | |||
| 21 | export interface LiveToolRegistration { | 21 | export 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; |
| @@ -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), |
| @@ -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 = authToken | 200 | 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 your | 362 | 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 bearer | 363 | 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-functional | 433 | // 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 | }, |
| @@ -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", |
| @@ -22,7 +22,7 @@ const executor = await createExecutor({ · 21 unmodified lines | |||
| 22 | }); | 22 | }); |
| 23 | 23 | ||
| 24 | // Public endpoint — no auth | 24 | // Public endpoint — no auth |
| 25 | await executor.graphql.addSource({ | 25 | await 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 | ||
| 58 | await executor.graphql.addSource({ | 58 | await 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", |
| @@ -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 | ||
| 26 | import { createGraphqlIntegrationOptimistic } from "./atoms"; | 26 | import { createGraphqlIntegrationOptimistic } from "./atoms"; |
| 27 | import { GraphqlSourceFields } from "./GraphqlSourceFields"; | 27 | import { GraphqlIntegrationFields } from "./GraphqlIntegrationFields"; |
| 28 | import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config"; | 28 | import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config"; |
| 29 | import type { GraphqlAuthMethodInput } from "../sdk/types"; | 29 | import 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). |
| 39 | const NO_SEEDS: readonly AuthMethodSeed[] = []; | 39 | const NO_SEEDS: readonly AuthMethodSeed[] = []; |
| 40 | 40 | ||
| 41 | export default function AddGraphqlSource(props: { | 41 | export 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 | <GraphqlSourceFields | 135 | <GraphqlIntegrationFields |
| 136 | endpoint={endpoint} | 136 | endpoint={endpoint} |
| 137 | onEndpointChange={setEndpoint} | 137 | onEndpointChange={setEndpoint} |
| 138 | identity={identity} | 138 | identity={identity} |
| @@ -36,12 +36,12 @@ import type { GraphqlAuthMethod } from "../sdk/types"; · 35 unmodified lines | |||
| 36 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
| 37 | 37 | ||
| 38 | export default function GraphqlAccountsPanel(props: { | 38 | export 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 | ||
| @@ -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 | ||
| 13 | export function GraphqlSourceFields(props: { | 13 | export 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; |
| @@ -1,3 +1,3 @@ | |||
| 1 | export { graphqlIntegrationPlugin } from "./source-plugin"; | 1 | export { graphqlIntegrationPlugin } from "./integration-plugin"; |
| 2 | export { GraphqlClient } from "./client"; | 2 | export { GraphqlClient } from "./client"; |
| 3 | export { createGraphqlIntegration, graphqlIntegrationConfigAtom } from "./atoms"; | 3 | export { createGraphqlIntegration, graphqlIntegrationConfigAtom } from "./atoms"; |
| @@ -2,7 +2,7 @@ import { lazy } from "react"; · 1 unmodified line | |||
| 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; | 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; |
| 3 | import { graphqlPresets } from "../sdk/presets"; | 3 | import { graphqlPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddGraphqlSource"); | 5 | const importAdd = () => import("./AddGraphqlIntegration"); |
| 6 | const importAccounts = () => import("./GraphqlAccountsPanel"); | 6 | const importAccounts = () => import("./GraphqlAccountsPanel"); |
| 7 | 7 | ||
| 8 | // No `editSheet`: GraphQL has no plugin-specific configuration beyond auth | 8 | // No `editSheet`: GraphQL has no plugin-specific configuration beyond auth |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { defineClientPlugin } from "@executor-js/sdk/client"; | 1 | import { defineClientPlugin } from "@executor-js/sdk/client"; |
| 2 | 2 | ||
| 3 | import { graphqlIntegrationPlugin } from "./source-plugin"; | 3 | import { graphqlIntegrationPlugin } from "./integration-plugin"; |
| 4 | 4 | ||
| 5 | export default defineClientPlugin({ | 5 | export default defineClientPlugin({ |
| 6 | id: "graphql" as const, | 6 | id: "graphql" as const, |
| @@ -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 a | 644 | /** Register the integration in the catalog. Registering an integration is a |
| 645 | * catalog statement ("we use this GraphQL endpoint now") and MUST NOT make a | 645 | * catalog statement ("we use this GraphQL endpoint now") and MUST NOT make a |
| 646 | * network call or require auth — exactly like MCP defers discovery. Live | 646 | * network call or require auth — exactly like MCP defers discovery. Live |
| 647 | * introspection (and the operation bindings it yields) is deferred to | 647 | * 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, |
| @@ -1,6 +1,6 @@ | |||
| 1 | # @executor-js/plugin-mcp | 1 | # @executor-js/plugin-mcp |
| 2 | 2 | ||
| 3 | Register [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. | 3 | Register [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 | ## Install | 5 | ## Install |
| 6 | 6 | ||
| @@ -18,7 +18,7 @@ import { mcpPlugin } from "@executor-js/plugin-mcp"; · 11 unmodified lines | |||
| 18 | 18 | ||
| 19 | const executor = await createExecutor({ | 19 | const 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 | |||
| 26 | const scope = executor.scopes[0]!.id; | 26 | const scope = executor.scopes[0]!.id; |
| 27 | 27 | ||
| 28 | // Remote MCP server | 28 | // Remote MCP server |
| 29 | await executor.mcp.addSource({ | 29 | await 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) |
| 37 | await executor.mcp.addSource({ | 37 | await executor.mcp.addIntegration({ |
| 38 | scope, | 38 | scope, |
| 39 | transport: "stdio", | 39 | transport: "stdio", |
| 40 | name: "My Server", | 40 | name: "My Server", |
| @@ -36,7 +36,7 @@ import { · 35 unmodified lines | |||
| 36 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; | 36 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 37 | import type { McpAuthMethodInput } from "../sdk/types"; | 37 | import type { McpAuthMethodInput } from "../sdk/types"; |
| 38 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; | 38 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; |
| 39 | import { McpRemoteSourceFields } from "./McpRemoteSourceFields"; | 39 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; |
| 40 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; | 40 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; |
| 41 | import { mcpPresets, type McpPreset } from "../sdk/presets"; | 41 | import { mcpPresets, type McpPreset } from "../sdk/presets"; |
| 42 | 42 | ||
| @@ -161,7 +161,7 @@ function reducer(state: State, action: Action): State { · 118 unmodified lines | |||
| 161 | // Component | 161 | // Component |
| 162 | // --------------------------------------------------------------------------- | 162 | // --------------------------------------------------------------------------- |
| 163 | 163 | ||
| 164 | export default function AddMcpSource(props: { | 164 | export 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 | <McpRemoteSourceFields | 421 | <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} |
| @@ -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 updated | 189 | 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 | ||
| 210 | export default function EditMcpSource({ sourceId, onPendingChange }: EditSheetSectionProps) { | 210 | export 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 | ||
| @@ -36,12 +36,12 @@ import type { McpAuthMethod } from "../sdk/types"; · 35 unmodified lines | |||
| 36 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
| 37 | 37 | ||
| 38 | export default function McpAccountsPanel(props: { | 38 | export 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 | ||
| @@ -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 | ||
| 25 | export type McpRemoteSourcePreview = { | 25 | export 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 | ||
| 34 | export function McpRemoteSourceFields(props: { | 34 | export 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; |
| @@ -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 | ||
| 31 | export default function McpSignInButton(props: { sourceId: string; owner?: Owner }) { | 31 | export 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); |
| @@ -1,5 +1,5 @@ | |||
| 1 | export { mcpIntegrationPlugin, createMcpIntegrationPlugin } from "./source-plugin"; | 1 | export { mcpIntegrationPlugin, createMcpIntegrationPlugin } from "./integration-plugin"; |
| 2 | export type { McpIntegrationPluginOptions } from "./source-plugin"; | 2 | export type { McpIntegrationPluginOptions } from "./integration-plugin"; |
| 3 | export { McpClient } from "./client"; | 3 | export { McpClient } from "./client"; |
| 4 | export { | 4 | export { |
| 5 | probeMcpEndpoint, | 5 | probeMcpEndpoint, |
| @@ -2,11 +2,11 @@ import { lazy, type ComponentProps, type ComponentType } from "react"; · 1 unmodified line | |||
| 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; | 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; |
| 3 | import { mcpPresets } from "../sdk/presets"; | 3 | import { mcpPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddMcpSource"); | 5 | const importAdd = () => import("./AddMcpIntegration"); |
| 6 | const importEditSheet = () => import("./EditMcpSource"); | 6 | const importEditSheet = () => import("./EditMcpIntegration"); |
| 7 | const importAccounts = () => import("./McpAccountsPanel"); | 7 | const importAccounts = () => import("./McpAccountsPanel"); |
| 8 | 8 | ||
| 9 | const LazyAddMcpSource = lazy(importAdd); | 9 | const LazyAddMcpIntegration = lazy(importAdd); |
| 10 | const LazyEditMcpSheet = lazy(importEditSheet); | 10 | const LazyEditMcpSheet = lazy(importEditSheet); |
| 11 | const LazyMcpAccountsPanel = lazy(importAccounts); | 11 | const LazyMcpAccountsPanel = lazy(importAccounts); |
| 12 | 12 | ||
| @@ -14,7 +14,7 @@ type AddProps = ComponentProps<IntegrationPlugin["add"]>; · 1 unmodified line | |||
| 14 | 14 | ||
| 15 | export interface McpIntegrationPluginOptions { | 15 | export 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 deployment | 19 | * Off by default — stdio is a high-risk transport on any server deployment |
| 20 | * (see `dangerouslyAllowStdioMCP` on the server-side plugin). Only enable in | 20 | * (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 = allowStdio | 35 | const presets = allowStdio |
| @@ -12,12 +12,12 @@ · 11 unmodified lines | |||
| 12 | 12 | ||
| 13 | import { defineClientPlugin } from "@executor-js/sdk/client"; | 13 | import { defineClientPlugin } from "@executor-js/sdk/client"; |
| 14 | 14 | ||
| 15 | import { createMcpIntegrationPlugin } from "./source-plugin"; | 15 | import { createMcpIntegrationPlugin } from "./integration-plugin"; |
| 16 | 16 | ||
| 17 | export interface McpClientConfig { | 17 | export interface McpClientConfig { |
| 18 | /** | 18 | /** |
| 19 | * Mirrors `dangerouslyAllowStdioMCP` on the server-side plugin. When | 19 | * 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; |
| @@ -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 | }, |
| @@ -299,7 +299,7 @@ const mcpInvocationAuthFailure = (input: { · 298 unmodified lines | |||
| 299 | input.status === 403 | 299 | 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", |
| @@ -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 result | 151 | // Run the full probe path (the function the React UI calls). The result |
| 152 | // surfaces `requiresOAuth` / `supportsDynamicRegistration`, which is what | 152 | // surfaces `requiresOAuth` / `supportsDynamicRegistration`, which is what |
| 153 | // drives the OAuth popup vs. credentials-editor branches in | 153 | // 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-required | 155 | // for API-key MCPs (Cubic) it should fail with the auth-required |
| 156 | // message; for unauth public servers (Hugging Face, etc.) it should | 156 | // 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. |
| @@ -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", |
| @@ -35,7 +35,7 @@ const result = await executor.tools.invoke("petstore.listPets", {}); · 34 unmodified lines | |||
| 35 | 35 | ||
| 36 | ## Secret-backed auth headers | 36 | ## Secret-backed auth headers |
| 37 | 37 | ||
| 38 | Wire API keys or bearer tokens through the executor's secret store — never hard-code them in source configs: | 38 | Wire API keys or bearer tokens through the executor's secret store — never hard-code them in integration configs: |
| 39 | 39 | ||
| 40 | ```ts | 40 | ```ts |
| 41 | import { createExecutor } from "@executor-js/sdk"; | 41 | import { createExecutor } from "@executor-js/sdk"; |
| @@ -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 blob | 134 | // 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). |
| 136 | const OpenApiConfigView = Schema.Struct({ | 136 | const 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)), |
| @@ -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 config | 97 | 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, |
| @@ -5,7 +5,7 @@ import type { Authentication } from "../../sdk/types"; · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | export const GoogleIntegrationConfigSchema = Schema.Struct({ | 6 | export 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)), |
| @@ -723,7 +723,7 @@ it.effect( · 722 unmodified lines | |||
| 723 | }), | 723 | }), |
| 724 | ); | 724 | ); |
| 725 | 725 | ||
| 726 | it.effect("bundles Google Discovery documents into one Google OpenAPI source", () => | 726 | it.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: [ |
| @@ -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 | ); |
| @@ -33,7 +33,7 @@ export const GOOGLE_PHOTOS_ICON = · 32 unmodified lines | |||
| 33 | export const googleOpenApiBundlePreset: GooglePreset = { | 33 | export 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 | }; |
| @@ -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, |
| @@ -86,7 +86,7 @@ export type MicrosoftGraphIntegrationConfig = OpenApiIntegrationConfig & { · 85 unmodified lines | |||
| 86 | 86 | ||
| 87 | const MicrosoftGraphIntegrationConfigSchema = Schema.Struct({ | 87 | const 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); |
| @@ -59,7 +59,7 @@ export const MICROSOFT_GRAPH_PRESET_ID = "microsoft"; · 58 unmodified lines | |||
| 59 | export const microsoftGraphPreset: MicrosoftGraphPreset = { | 59 | export 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 | }; |
| @@ -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 is | 34 | // Stream the full Graph source straight to persisted bindings. This is |
| @@ -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 | ||
| 10 | const visibleText = (node: React.ReactNode): string => { | 10 | const 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 ""; |
| @@ -44,7 +44,7 @@ import { · 43 unmodified lines | |||
| 44 | openApiWireAuthInput, | 44 | openApiWireAuthInput, |
| 45 | } from "./auth-method-config"; | 45 | } from "./auth-method-config"; |
| 46 | import { addOpenApiSpec, previewOpenApiSpec } from "./atoms"; | 46 | import { addOpenApiSpec, previewOpenApiSpec } from "./atoms"; |
| 47 | import { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields"; | 47 | import { OpenApiIntegrationDetailsFields } from "./OpenApiIntegrationDetailsFields"; |
| 48 | import type { SpecPreviewSummary } from "../sdk/preview"; | 48 | import type { SpecPreviewSummary } from "../sdk/preview"; |
| 49 | import { type Authentication } from "../sdk/types"; | 49 | import { type Authentication } from "../sdk/types"; |
| 50 | import { resolveServerUrl } from "../sdk/openapi-utils"; | 50 | import { 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 | ||
| 145 | export default function AddOpenApiSource(props: { | 145 | export 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 integration | 326 | // 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 required | 332 | // 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 > 0 | 371 | ...(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 | <OpenApiSourceDetailsFields | 506 | <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 | ||
| @@ -51,12 +51,12 @@ const NO_AUTH_METHOD: AuthMethod = { · 50 unmodified lines | |||
| 51 | // --------------------------------------------------------------------------- | 51 | // --------------------------------------------------------------------------- |
| 52 | 52 | ||
| 53 | export default function OpenApiAccountsPanel(props: { | 53 | export 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 | ||
| @@ -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. */ |
| 24 | const isUrlInput = (value: string): boolean => URL.canParse(value.trim()); | 24 | const isUrlInput = (value: string): boolean => URL.canParse(value.trim()); |
| 25 | 25 | ||
| 26 | export function OpenApiSourceDetailsFields(props: { | 26 | export 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; |
| @@ -42,7 +42,7 @@ const outcomeSummary = (outcome: UpdateOutcome): string => { · 41 unmodified lines | |||
| 42 | }; | 42 | }; |
| 43 | 43 | ||
| 44 | export default function UpdateSpecSection(props: EditSheetSectionProps) { | 44 | export 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 to | 57 | // 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 | <Checkbox | 101 | <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> |
| @@ -1,6 +1,6 @@ | |||
| 1 | export { openApiIntegrationPlugin } from "./source-plugin"; | 1 | export { openApiIntegrationPlugin } from "./integration-plugin"; |
| 2 | export { OpenApiClient } from "./client"; | 2 | export { OpenApiClient } from "./client"; |
| 3 | export { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields"; | 3 | export { OpenApiIntegrationDetailsFields } from "./OpenApiIntegrationDetailsFields"; |
| 4 | export { | 4 | export { |
| 5 | authenticationFromEditorValue, | 5 | authenticationFromEditorValue, |
| 6 | authMethodsFromConfig, | 6 | authMethodsFromConfig, |
| @@ -2,7 +2,7 @@ import { lazy } from "react"; · 1 unmodified line | |||
| 2 | import type { IntegrationPlugin, IntegrationPreset } from "@executor-js/sdk/client"; | 2 | import type { IntegrationPlugin, IntegrationPreset } from "@executor-js/sdk/client"; |
| 3 | import { openApiPresets } from "../sdk/presets"; | 3 | import { openApiPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddOpenApiSource"); | 5 | const importAdd = () => import("./AddOpenApiIntegration"); |
| 6 | const importEditSheet = () => import("./UpdateSpecSection"); | 6 | const importEditSheet = () => import("./UpdateSpecSection"); |
| 7 | const importAccounts = () => import("./OpenApiAccountsPanel"); | 7 | const importAccounts = () => import("./OpenApiAccountsPanel"); |
| 8 | 8 | ||
| @@ -3,7 +3,7 @@ · 2 unmodified lines | |||
| 3 | // | 3 | // |
| 4 | // Aggregates the openapi plugin's frontend contributions into a single | 4 | // Aggregates the openapi plugin's frontend contributions into a single |
| 5 | // declarative spec. The host's Vite plugin reads this via | 5 | // declarative spec. The host's Vite plugin reads this via |
| 6 | // `virtual:executor/plugins-client`, so the host's sources page derives | 6 | // `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`; this | 9 | // The richer add/edit/summary components still live in `./react`; this |
| @@ -12,7 +12,7 @@ · 2 unmodified lines | |||
| 12 | 12 | ||
| 13 | import { defineClientPlugin } from "@executor-js/sdk/client"; | 13 | import { defineClientPlugin } from "@executor-js/sdk/client"; |
| 14 | 14 | ||
| 15 | import { createOpenApiIntegrationPlugin, type OpenApiClientConfig } from "./source-plugin"; | 15 | import { createOpenApiIntegrationPlugin, type OpenApiClientConfig } from "./integration-plugin"; |
| 16 | 16 | ||
| 17 | export default function createOpenApiClientPlugin(config?: OpenApiClientConfig) { | 17 | export default function createOpenApiClientPlugin(config?: OpenApiClientConfig) { |
| 18 | return defineClientPlugin({ | 18 | return defineClientPlugin({ |
| @@ -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 } : {}), |
| @@ -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). */ |
| @@ -31,7 +31,7 @@ import { openApiPlugin } from "./plugin"; · 30 unmodified lines | |||
| 31 | import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types"; | 31 | import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types"; |
| 32 | import { variable } from "@executor-js/sdk/http-auth"; | 32 | import { variable } from "@executor-js/sdk/http-auth"; |
| 33 | import { | 33 | import { |
| 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 | ||
| 68 | const specText = () => { | 68 | const 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 | }; |
| @@ -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" }); |
| @@ -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 this | 1182 | // via Effect.fn) — annotateCurrentSpan inside it never reaches this |
| @@ -41,7 +41,7 @@ import { openApiPlugin } from "./plugin"; · 40 unmodified lines | |||
| 41 | import { type AuthenticationInput } from "./types"; | 41 | import { type AuthenticationInput } from "./types"; |
| 42 | import { | 42 | import { |
| 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 | |||
| 104 | const TestApi = HttpApi.make("testApi").add(ItemsGroup); | 104 | const TestApi = HttpApi.make("testApi").add(ItemsGroup); |
| 105 | 105 | ||
| 106 | const testApiSpecText = () => { | 106 | const 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 | }; |
| @@ -122,7 +122,7 @@ export interface UpdateSpecResult { · 121 unmodified lines | |||
| 122 | 122 | ||
| 123 | export interface OpenApiUpdateSpecInput { | 123 | export interface OpenApiUpdateSpecInput { |
| 124 | /** New spec source. Omit to re-fetch from the integration's stored | 124 | /** 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 | ||
| 276 | const AddSourceInputSchema = Schema.Struct({ | 276 | const 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 | ||
| 290 | const AddSourceOutputSchema = Schema.Struct({ | 290 | const 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 | |||
| 298 | const PreviewSpecOutputStandardSchema = Schema.toStandardSchemaV1( | 298 | const PreviewSpecOutputStandardSchema = Schema.toStandardSchemaV1( |
| 299 | Schema.toStandardJSONSchemaV1(StaticPreviewSpecOutputSchema), | 299 | Schema.toStandardJSONSchemaV1(StaticPreviewSpecOutputSchema), |
| 300 | ); | 300 | ); |
| 301 | const AddSourceInputStandardSchema = Schema.toStandardSchemaV1( | 301 | const AddIntegrationInputStandardSchema = Schema.toStandardSchemaV1( |
| 302 | Schema.toStandardJSONSchemaV1(AddSourceInputSchema), | 302 | Schema.toStandardJSONSchemaV1(AddIntegrationInputSchema), |
| 303 | ); | 303 | ); |
| 304 | const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1( | 304 | const AddIntegrationOutputStandardSchema = Schema.toStandardSchemaV1( |
| 305 | Schema.toStandardJSONSchemaV1(AddSourceOutputSchema), | 305 | Schema.toStandardJSONSchemaV1(AddIntegrationOutputSchema), |
| 306 | ); | 306 | ); |
| 307 | 307 | ||
| 308 | const openApiToolFailure = (code: string, message: string, details?: unknown) => | 308 | const 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 | ||
| 381 | const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined => | 381 | const specInputToSpecUrl = (spec: OpenApiSpecInput): string | undefined => |
| 382 | spec.kind === "url" ? spec.url : undefined; | 382 | spec.kind === "url" ? spec.url : undefined; |
| 383 | 383 | ||
| 384 | const OAUTH_DISCOVERED_SCHEME_NAME = "DiscoveredOAuth2"; | 384 | const OAUTH_DISCOVERED_SCHEME_NAME = "DiscoveredOAuth2"; |
| @@ -418,7 +418,7 @@ const addProbeCandidate = (candidates: string[], value: string | undefined): voi · 33 unmodified lines | |||
| 418 | 418 | ||
| 419 | const oauthProbeCandidates = ( | 419 | const 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)) !== undefined | 775 | ...((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 otherwise | 778 | // baseUrl is an optional override only. The host is otherwise |
| 779 | // resolved per call from the operation's `servers` (extracted from | 779 | // 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 has | 871 | // 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)) !== undefined | 914 | ...((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 | self | 1110 | self |
| 1111 | .addSpec({ | 1111 | .addSpec({ |
| 1112 | spec: input.spec, | 1112 | spec: input.spec, |
| @@ -146,7 +146,7 @@ export const OAuth2Preset = Schema.Struct({ · 145 unmodified lines | |||
| 146 | export type OAuth2Preset = typeof OAuth2Preset.Type; | 146 | export type OAuth2Preset = typeof OAuth2Preset.Type; |
| 147 | 147 | ||
| 148 | // --------------------------------------------------------------------------- | 148 | // --------------------------------------------------------------------------- |
| 149 | // Preview operation — lightweight shape for the add-source UI list | 149 | // Preview operation — lightweight shape for the add-integration UI list |
| 150 | // --------------------------------------------------------------------------- | 150 | // --------------------------------------------------------------------------- |
| 151 | 151 | ||
| 152 | export const PreviewOperation = Schema.Struct({ | 152 | export 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), |
| @@ -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) moved | 2 | // DB-touching behaviour (addSpec, removeSpec, tool registration) moved |
| 3 | // to apps/cloud/src/services/sources-api.node.test.ts — those run | 3 | // to the apps/cloud integrations API node tests — those run |
| 4 | // through the real Drizzle/FumaDB path so storage regressions | 4 | // through the real Drizzle/FumaDB path so storage regressions |
| 5 | // (e.g. a per-row createMany fallback) surface automatically instead | 5 | // (e.g. a per-row createMany fallback) surface automatically instead |
| 6 | // of needing a dedicated budget assertion. | 6 | // of needing a dedicated budget assertion. |
| @@ -13,7 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | ToolAddress, | 13 | ToolAddress, |
| 14 | } from "@executor-js/sdk"; | 14 | } from "@executor-js/sdk"; |
| 15 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; | 15 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; |
| 16 | import { makeOpenApiHttpApiTestSourceConfig, unwrapInvocation } from "../testing"; | 16 | import { makeOpenApiHttpApiTestIntegrationConfig, unwrapInvocation } from "../testing"; |
| 17 | 17 | ||
| 18 | import { invokeWithLayer } from "./invoke"; | 18 | import { invokeWithLayer } from "./invoke"; |
| 19 | import { openApiPlugin } from "./plugin"; | 19 | import { 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", |
| @@ -34,7 +34,7 @@ import type { OpenapiStore } from "./store"; · 33 unmodified lines | |||
| 34 | import type { AuthenticationInput } from "./types"; | 34 | import type { AuthenticationInput } from "./types"; |
| 35 | import { defsBlobKey, specBlobKey } from "./store"; | 35 | import { defsBlobKey, specBlobKey } from "./store"; |
| 36 | import { | 36 | import { |
| 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 | ||
| 63 | const specText = () => { | 63 | const 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 | }; |
| @@ -26,7 +26,7 @@ export interface DerivedIdentity { · 25 unmodified lines | |||
| 26 | 26 | ||
| 27 | export interface ConvertedSpec { | 27 | export 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>>; |
| @@ -13,7 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | ToolAddress, | 13 | ToolAddress, |
| 14 | } from "@executor-js/sdk"; | 14 | } from "@executor-js/sdk"; |
| 15 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; | 15 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; |
| 16 | import { makeOpenApiHttpApiTestSourceConfig, unwrapInvocation } from "../testing"; | 16 | import { makeOpenApiHttpApiTestIntegrationConfig, unwrapInvocation } from "../testing"; |
| 17 | 17 | ||
| 18 | import { collectStreamingBody, STREAM_MAX_BYTES } from "./invoke"; | 18 | import { collectStreamingBody, STREAM_MAX_BYTES } from "./invoke"; |
| 19 | import { openApiPlugin } from "./plugin"; | 19 | import { 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", |
| @@ -35,7 +35,7 @@ import { · 34 unmodified lines | |||
| 35 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; | 35 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; |
| 36 | import { | 36 | import { |
| 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", |
| @@ -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 | ||
| 95 | export type OpenApiTestSourceOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & { | 95 | export 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 | ||
| 100 | export type OpenApiHttpApiTestSourceOptions = Omit< | 100 | export 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 | ||
| 115 | export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit< | 115 | export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit< |
| 116 | OpenApiHttpApiTestSourceOptions, | 116 | OpenApiHttpApiTestIntegrationOptions, |
| 117 | "headers" | "queryParams" | 117 | "headers" | "queryParams" |
| 118 | > & | 118 | > & |
| 119 | OpenApiHttpApiAddSpecHeadersInput; | 119 | OpenApiHttpApiAddSpecHeadersInput; |
| 120 | 120 | ||
| 121 | export type OpenApiTestSourceExecutor = { | 121 | export 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 | |||
| 160 | export const addOpenApiTestConnection = ( | 160 | export 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 | ||
| 212 | export const makeOpenApiTestSourceConfig = ( | 212 | export 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 | ||
| 225 | export const addOpenApiTestSource = ( | 225 | export 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 | ||
| 231 | export const makeOpenApiHttpApiTestSourceConfig = ( | 231 | export 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 | ||
| 247 | export const addOpenApiHttpApiTestSource = ( | 247 | export 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 | ||
| 253 | export const makeOpenApiHttpApiTestAddSpecPayload = ( | 253 | export 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, |
| @@ -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 | ||
| @@ -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.authenticationTemplate | 640 | ...(target.authenticationTemplate |
| @@ -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 | }); |
| @@ -198,7 +198,7 @@ type ToolkitConnectionGroup = { · 197 unmodified lines | |||
| 198 | 198 | ||
| 199 | type IntegrationMeta = { | 199 | type 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.integration | 327 | 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 | <IntegrationFavicon | 438 | <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 | <IntegrationFavicon | 888 | <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 | /> |
| @@ -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 | }); |
| @@ -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. an | 399 | // 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 requested | 400 | // OpenAPI integration like PostHog declares hundreds of scopes). Those are requested |
| 401 | // broadly but not individually required: a provider that narrows the grant to | 401 | // 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 treat | 402 | // 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. |
| @@ -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 plugin | 27 | // 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 | <CommandItem | 152 | <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"> |
| @@ -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-in | 52 | // Reconstruct the props the multiplayer shell derives for the built-in |
| 53 | // executor source (EXECUTOR_SOURCE: kind "built-in", name "Executor", no | 53 | // 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 the | 60 | // 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 null | 65 | // 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 | { |
| @@ -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 | ||
| 35 | export function integrationLocalIconUrl(sourceId: string | undefined): string | null { | 35 | export 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 | ||
| 128 | export function integrationInferredUrl(source: { | 128 | export 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 | ||
| 141 | const tokenMatches = (sourceValue: string, presetValue: string): boolean => | 141 | const 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 | ||
| 148 | export function integrationPresetIconUrl( | 148 | export 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 an | 183 | // 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, the | 184 | // 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 Google | 185 | // 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 preset | 186 | // 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: callers | 187 | // 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. |
| 189 | export function integrationFaviconSrc(args: { | 189 | export 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)) ?? null | 203 | ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null |
| @@ -206,17 +206,17 @@ export function integrationFaviconSrc(args: { · 2 unmodified lines | |||
| 206 | 206 | ||
| 207 | export function IntegrationFavicon({ | 207 | export 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 ( |
| @@ -2,7 +2,7 @@ import { IntegrationFavicon } from "./integration-favicon"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | export function IntegrationIconWithAccount(props: { | 3 | export 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 | <IntegrationFavicon | 12 | <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 | /> |
| @@ -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 it | 234 | 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> |
| @@ -1,5 +1,5 @@ | |||
| 1 | // --------------------------------------------------------------------------- | 1 | // --------------------------------------------------------------------------- |
| 2 | // Integration add-flow helpers — the plugin-agnostic pieces every Add-source | 2 | // Integration add-flow helpers — the plugin-agnostic pieces every Add-integration |
| 3 | // form repeats: decoding a mutation Exit into a user-facing message, the | 3 | // form repeats: decoding a mutation Exit into a user-facing message, the |
| 4 | // IntegrationAlreadyExistsError guard, the live slug-collision check against | 4 | // 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. |
| @@ -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 }) ?? |
| @@ -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.accounts | 435 | <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 | /> |
| @@ -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} /> |
| @@ -40,10 +40,10 @@ export function pascalCaseDomainLabel(label: string): string | null { · 39 unmodified lines | |||
| 40 | .join(""); | 40 | .join(""); |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | export function integrationDisplayNameFromUrl(url: string, sourceKind: string): string | null { | 43 | export 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 meaningful | 49 | // 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 | |||
| 129 | export function integrationDisplayNameFromStdio( | 129 | export 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 Edit | 210 | * When true, the namespace field is rendered disabled — useful on Edit |
| 211 | * forms, where the namespace is the source's identity and changing it | 211 | * 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 | (namespaceReadOnly | 227 | (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 | (namespaceReadOnly | 257 | (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 ( |
| @@ -117,7 +117,7 @@ describe("missingScopes (Part 2 informational subset warning)", () => { · 116 unmodified lines | |||
| 117 | 117 | ||
| 118 | describe("reconsentRequiredScopes", () => { | 118 | describe("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 scope | 120 | // 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({ |
| @@ -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 OpenAPI | 115 | * Spec-derived oauth scopes are the full per-operation catalog union (an OpenAPI |
| 116 | * source like PostHog declares hundreds): requested broadly to unlock as many | 116 | * integration like PostHog declares hundreds): requested broadly to unlock as many |
| 117 | * tools as possible, but none individually required. A provider that narrows the | 117 | * 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 NOT | 118 | * 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 and | 119 | * drive a reconnect prompt. Custom (user-configured) scopes are intentional and |
| @@ -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 }>( |
| @@ -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, |
| @@ -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 | ||
| @@ -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 to | 14 | - 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 of | 17 | - It is a category of **integration** and a way to **interop between them** through one set of |
| 18 | concepts. | 18 | concepts. |
| 19 | 19 | ||
| 20 | The bet: as work shifts toward agents acting through software, every company needs one layer | 20 | The 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 as | 32 | 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 OpenAPI | 34 | - **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 raw | 35 | 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 many | 38 | 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 a | 40 | - **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 it | 41 | `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 is | 43 | - **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 and | 45 | - **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 extension | 47 | - **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; an | 49 | - **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, surfaces | 52 | ## 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 | |||
| 63 | audit/runs, the internal-apps catalog. They're separable but they don't go through an abstract | 63 | audit/runs, the internal-apps catalog. They're separable but they don't go through an abstract |
| 64 | plugin contract, because nobody outside you needs to add a new _kind_ of them. | 64 | plugin 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; you | 66 | **One open plugin seam: integrations.** There are thousands of APIs; you'll never enumerate them; you |
| 67 | want others to add them. That is where an open contract (`resolveTools` / `invokeTool`) earns its | 67 | want others to add them. That is where an open contract (`resolveTools` / `invokeTool`) earns its |
| 68 | complexity. Secret **providers** are the one plausible second seam, for the same reason. | 68 | complexity. 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 seam | 71 | > 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 web | 74 | **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 & auth | 128 | ## 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 connects | 132 | - **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 just | 133 | 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:** a | 138 | 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 the | 141 | separately. **Credential lifecycle** (refresh, rotation, revocation, expiry) handled below the |
| 142 | agent. | 142 | agent. |
| 143 | 143 | ||
| 144 | ## Sources & source kinds | 144 | ## Integrations & integration kinds |
| 145 | 145 | ||
| 146 | Each kind is added through the **one open seam** (`resolveTools` + `invokeTool`): **OpenAPI**, | 146 | Each kind is added through the **one open seam** (`resolveTools` + `invokeTool`): **OpenAPI**, |
| 147 | **GraphQL**, **MCP servers**, **CLI** (install + run, no spec), **direct API / raw-fetch**, and | 147 | **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 | |||
| 150 | required capabilities, an LLM-authored description). **Spec auto-refresh** tracks upstream drift | 150 | required capabilities, an LLM-authored description). **Spec auto-refresh** tracks upstream drift |
| 151 | and **must never auto-expand authority** — new operations appear _ungranted until reviewed_. | 151 | and **must never auto-expand authority** — new operations appear _ungranted until reviewed_. |
| 152 | 152 | ||
| 153 | An **integrations registry** lets people discover and add sources. | 153 | An **integrations registry** lets people discover and add integrations. |
| 154 | 154 | ||
| 155 | ## Policies | 155 | ## Policies |
| 156 | 156 | ||
| 157 | Policies gate **execution** — `approve` / `require_approval` / `block` — and are distinct from | 157 | Policies gate **execution** — `approve` / `require_approval` / `block` — and are distinct from |
| 158 | toolkits (which gate visibility). Resolution is _most-restrictive-wins_; **plugin defaults** let | 158 | toolkits (which gate visibility). Resolution is _most-restrictive-wins_; **plugin defaults** let |
| 159 | a source ship a tool pre-marked `require_approval` (destructive detection), with `approve` as the | 159 | an integration ship a tool pre-marked `require_approval` (destructive detection), with `approve` as the |
| 160 | override when an import wrongly flags a safe action. | 160 | override when an import wrongly flags a safe action. |
| 161 | 161 | ||
| 162 | How people actually use them today (real usage): glob patterns over the address, where _every_ | 162 | How people actually use them today (real usage): glob patterns over the address, where _every_ |
| 163 | rule targets a **source** and wildcards the scope/connection (`stripe_api.*.*.disputes.*`, | 163 | rule 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 to | 164 | `pscale.*.*.execute_write_query`) — i.e. faking structural targeting because there's nowhere to |
| 165 | attach it. The direction: | 165 | attach it. The direction: |
| 166 | 166 | ||
| 167 | - **Kill the global glob list. Attach policy to the source and connection records** — the two | 167 | - **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 authority | 168 | 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/connection | 171 | - **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 merge | 172 | 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 paths | 236 | ## Authoring model — two paths |
| @@ -309,7 +309,7 @@ service install` (daemon), `executor web` (web app) — one instance, delivered · 72 unmodified lines | |||
| 309 | The hosted product runs on a **Cloudflare substrate** and lets users **write plugins and load | 309 | The hosted product runs on a **Cloudflare substrate** and lets users **write plugins and load |
| 310 | them dynamically, sandboxed** — each in a Worker isolate with a **declared capability manifest** | 310 | them 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 the | 311 | (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 via | 312 | **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 for | 313 | **substrate substitution**: a local V8 isolate stands in for the dynamic worker, Git for |
| 314 | Cloudflare Artifacts, local SQLite for hosted storage. | 314 | Cloudflare Artifacts, local SQLite for hosted storage. |
| 315 | 315 | ||
| @@ -329,7 +329,7 @@ Cloudflare Artifacts, local SQLite for hosted storage. · 13 unmodified lines | |||
| 329 | Built on **Effect**. The same SDK is in-memory _and_ client/server; SDK-only users need no | 329 | Built on **Effect**. The same SDK is in-memory _and_ client/server; SDK-only users need no |
| 330 | server; serving is a standard Web `Request → Response` handler. The discipline that keeps breadth | 330 | server; serving is a standard Web `Request → Response` handler. The discipline that keeps breadth |
| 331 | cheap is the **seam discipline**: the platform owns generic concerns (auth, caching, retries, | 331 | cheap is the **seam discipline**: the platform owns generic concerns (auth, caching, retries, |
| 332 | schema, rendering, transport, the Run); a plugin owns only the source-specific resolve/invoke. | 332 | schema, rendering, transport, the Run); a plugin owns only the integration-specific resolve/invoke. |
| 333 | 333 | ||
| 334 | ## Principles | 334 | ## 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 between | 358 | 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'd | 360 | - **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**, not | 361 | 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_ by | 362 | a dependency to satisfy. Build the smallest thing useful on its own; let the next be _pulled_ by |