History

Merge origin/main

Rhys Sullivan committed 7 months ago

Commit fd4ec3f2 parents 786819c 6dbf7b240 files · +2895 −481Browse files

Unified · Split

40 files
  1. .executor/executor.jsonc +41 −0
  2. .tmp-openapi-helper-probe.ts +0 −30
  3. AGENTS.md +0 −1
  4. CLAUDE.md +0 −7
  5. PLAN.md +0 −82
  6. TRACING.md +0 −43
  7. apps/executor/src/cli/main.ts +20 −6
  8. apps/web/src/components/source-favicon.tsx +88 −10
  9. apps/web/src/components/source-plugins-reset-state.tsx +1 −1
  10. apps/web/src/lib/source-favicon.ts +158 −0
  11. apps/web/src/plugins/index.ts +8 −10
  12. apps/web/src/plugins/pages.tsx +2 −2
  13. packages/clients/react/src/plugins/components/source-tool-workbench.tsx +17 −8
  14. packages/hosts/mcp/src/index.ts +8 −5
  15. packages/platform/sdk/src/executor-effect.ts +8 −9
  16. packages/platform/sdk/src/plugins.ts +321 −41
  17. packages/platform/sdk/src/runtime/catalog/source/reconcile.ts +26 −1
  18. packages/platform/sdk/src/runtime/catalog/source/sync.ts +11 −1
  19. packages/platform/sdk/src/runtime/execution/ir-execution.ts +8 −1
  20. packages/platform/sdk/src/runtime/execution/scope/environment.ts +7 −0
  21. packages/platform/sdk/src/runtime/execution/scope/tool-invoker.ts +7 −0
  22. packages/platform/sdk/src/runtime/index.ts +37 −3
  23. packages/platform/sdk/src/runtime/source-artifacts.ts +9 −1
  24. packages/platform/sdk/src/runtime/sources/executor-tools.test.ts +290 −0
  25. packages/platform/sdk/src/runtime/sources/executor-tools.ts +349 −33
  26. packages/platform/sdk/src/runtime/sources/source-definitions.ts +52 −16
  27. packages/platform/sdk/src/runtime/sources/source-plugins/index.ts +45 −21
  28. packages/platform/sdk/src/runtime/sources/source-store/config.ts +29 −5
  29. packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts +1 −0
  30. packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts +123 −0
  31. packages/platform/sdk/src/schema/models/local-config.ts +8 −0
  32. packages/platform/server/src/index.test.ts +225 −1
  33. personal-notes/otel-tracing-for-source-connect.md +0 −50
  34. plugins/google-discovery/react/index.ts +4 −1
  35. plugins/google-discovery/sdk/index.ts +427 −15
  36. plugins/graphql/sdk/index.ts +125 −15
  37. plugins/mcp/sdk/index.ts +275 −15
  38. plugins/openapi/sdk/index.ts +165 −15
  39. ralph.ts +0 −22
  40. skills-lock.json +0 −10
.executor/executor.jsonc+41−0
@@ -0,0 +1,41 @@
1{
2 "sources": {
3 "vercel": {
4 "name": "Vercel API",
5 "connection": {
6 "endpoint": "https://api.vercel.com/"
7 },
8 "kind": "openapi",
9 "binding": {
10 "specUrl": "https://openapi.vercel.sh/",
11 "defaultHeaders": null
12 }
13 },
14 "deepwiki": {
15 "name": "DeepWiki MCP",
16 "connection": {
17 "endpoint": "https://mcp.deepwiki.com/mcp"
18 },
19 "kind": "mcp",
20 "binding": {
21 "transport": "auto",
22 "queryParams": null,
23 "headers": null,
24 "command": null,
25 "args": null,
26 "env": null,
27 "cwd": null
28 }
29 },
30 "linear": {
31 "kind": "graphql",
32 "name": "Linear GraphQL",
33 "connection": {
34 "endpoint": "https://api.linear.app/graphql"
35 },
36 "binding": {
37 "defaultHeaders": null
38 }
39 }
40 }
41}
.tmp-openapi-helper-probe.ts+0−30
@@ -1,30 +0,0 @@
1import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from '@effect/platform';
2import { startOpenApiTestServer } from '@executor/effect-test-utils';
3import * as Effect from 'effect/Effect';
4import * as Layer from 'effect/Layer';
5import * as Schema from 'effect/Schema';
6
7const ownerParam = HttpApiSchema.param('owner', Schema.String);
8const repoParam = HttpApiSchema.param('repo', Schema.String);
9class DemoReposApi extends HttpApiGroup.make('repos').add(
10 HttpApiEndpoint.get('getRepo')`/repos/${ownerParam}/${repoParam}`.addSuccess(
11 Schema.Struct({ full_name: Schema.String, private: Schema.Boolean }),
12 ),
13) {}
14class DemoApi extends HttpApi.make('demo').add(DemoReposApi) {}
15const live = HttpApiBuilder.group(DemoApi, 'repos', (handlers) =>
16 handlers.handle('getRepo', ({ path, request }) => {
17 console.log('auth', request.headers.authorization);
18 return Effect.succeed({ full_name: `${path.owner}/${path.repo}`, private: false });
19 }),
20);
21const main = async () => {
22 const server = await startOpenApiTestServer({ apiLayer: HttpApiBuilder.api(DemoApi).pipe(Layer.provide(live)) });
23 console.log('base', server.baseUrl, server.specUrl);
24 const spec = await fetch(server.specUrl);
25 console.log('spec', spec.status, await spec.text());
26 const resp = await fetch(`${server.baseUrl}/repos/vercel/ai`, { headers: { authorization: 'Bearer test' } });
27 console.log('repo', resp.status, await resp.text());
28 await server.close();
29};
30main().catch((e) => { console.error(e); process.exit(1); });
AGENTS.md+0−1
@@ -1 +0,0 @@
1CLAUDE.md
CLAUDE.md+0−7
@@ -1,7 +0,0 @@
1# Repo Agent Notes
2
3- When a test needs an OpenAPI spec, generate it from Effect `HttpApi` with `OpenApi.fromApi(...)`.
4- When a test needs to serve an Effect `HttpApi` plus `/openapi.json`, use `@executor/effect-test-utils` via `startOpenApiTestServer(...)` or `makeOpenApiTestServer(...)`.
5- Do not hand-author inline OpenAPI documents in normal tests.
6- Do not hand-roll `HttpApiBuilder.toWebHandler(...)` to Node HTTP bridges for OpenAPI test servers.
7- Raw OpenAPI parser/import-fidelity tests are the exception; those may keep literal OpenAPI documents where the point of the test is the raw document itself.
PLAN.md+0−82
@@ -1,82 +0,0 @@
1Executor:
2
3Project goal: Build an execution environment for agents to interact with APIs, MCPs, GraphQL, and other services via writing TypeScript.
4
5The way things work today is if people want to interact with services, they either add an mcp or they call it through a cli. for mcp servers when someone adds it to their agent, it blows up the context window of tokens. the agent isn't able to call it in an efficient way and it adds a lot of bloat so people have ended up using clis instead.
6
7the problem with the cli is people run things in dangerously allowable permissions. along with that you don't have typing and information about what clis are installed, what they can do, et cetera. by creating this typescript code execution environment we're able to handle both those problems. you can read the article code mode to understand deeper the benefits of this approach.
8
9the basic premise is that you give the agent the ability to search over what can be called from typescript and write code to call it. it can see the typings properly through typescript.
10
11let's look at some examples.
12
13let's say the agent wants to list the github issues for a repository. the agent call is executed through an mcp call and it parses typescript code from that. that typescript code will basically just be
14
15`tools.discover` and then the query which is github issues list.
16
17so `tools.discover("github issues list")`
18
19that then returns the callable paths and then the agent writes another line of code. that is await tools.github.issues.list. that executes in a sandbox and it calls a proxy object which makes a fetch to run the call and then that runs that call in the secure environment. that call completes and the results are given to the sandbox and the sandbox resumes.
20
21another instance is the case where a tool needs some form of user action. the example that i will give is approvals and if it's a create issue for example, the agent would write `await tools.github.issues.create`. that would then call the proxy object. the proxy object would trigger the interaction and then once the user approves that interaction it resumes.
22
23a note on the interaction model: i believe we want to model this off of mcp elicitation which you can see the spec for in references. the reason we want to model it off of mcp elicitation is that for apis for example that will just be a standard approve/deny model but for something like this since we support calling mcp servers those mcp servers could actually try and elicit input for the user.
24
25So let's talk about interaction model a little bit. Executor is going to be configurable through Executor, and so you would be able to add a source, for example, by just prompting: "Hey, please add Axiom as a source." What it would do is it would call tools.executor.AddSource, which calls the MCP server. That MCP server matches kind of the web flow where there's information or an action you need to elicit from the user. What that information action looks like is in the OAUTH flow. It's: they have to go sign in somewhere; there's some callback that happens, and then something's created for that. It's details on that, but we get them onto a web page in the browser to sign in. A similar thing would be if a source required an API key. It's the same concept if you open up a web page to securely input that.
26
27Current state:
28
29We are on v3 of this codebase which is a completely fresh start, the original codebase is in legacy/, the v2 is in legacy2/.
30
31Architecture I like:
32
33Database:
34Local-file-backed control plane for local use, with room for future hosted backends
35
36Server:
37
38- API
39 The web dashboard calls the API via effect-atom.
40
41- MCP
42 Executor is configurable via executor. A user can enter the prompt "Please add the https://mcp.notion.com/mcp to my workspace"
43
44We use MCP here instead of the API as the user has to perform actions in many cases. An example of this is OAuthing to an MCP server or opening a page to set a secret in. This prevents the secrets from being pasted over a chat
45
46Doing MCP here w/ MCP elicitation allows us to not give special treatment to the executor app while also giving a nice UX
47
48Ideally the MCP server shares a lot of logic between API
49
50Clients:
51
52- Web client
53 Cloud product hosted at executor.sh, local one just on localhost
54 This is a next.js app, so we will actually host the api/ in a route handler on it however it's important we implement the API in a standalone package and it just exposes a standard web request / response handler
55- CLI
56 Connects to either the cloud api endpoint
57
58Standalone:
59
60- SDK
61 Allows for people to use everything we've built in their own apps, including ingesting API specs / MCPs for calling
62
63Runtimes:
64
65- Deno
66- In process
67- Cloudflare worker runtime
68
69Other:
70
71- Avoid hardcoding strings in the core libraries, prefer programming to interfaces, this is because it allows people to build custom adapters without having to modify the core
72- Aim for composability of adapters. For example, the cloud product may eventually store secrets in a hosted backend while still supporting BYO 1Password. This does not need to be implemented today but is worth noting.
73- This is a fresh start so we can make whatever changes we need.
74
75Rough architecture is: Turborepo monorepo, effect-vitest for testing, bun for package manager / running apps (but we leverage Effect wherever possible rather than bun's apis i.e for the server), Next.js for web app
76
77References:
78https://blog.cloudflare.com/code-mode/ - concept we are implementing
79https://mcp.axiom.co/mcp - MCP server that requires auth
80https://modelcontextprotocol.io/specification/draft/client/elicitation - How we are going to be handling interactions from the user
81legacy/ original implementation
82legacy2
TRACING.md+0−43
@@ -1,43 +0,0 @@
1# Local Tracing
2
3Start Jaeger locally:
4
5```bash
6bun run trace:up
7```
8
9Run the web/server dev process with tracing enabled:
10
11```bash
12EXECUTOR_TRACE_ENABLED=1 \
13EXECUTOR_TRACE_SERVICE_NAME=executor-local \
14EXECUTOR_TRACE_OTLP_ENDPOINT=http://127.0.0.1:4317 \
15bun run dev
16```
17
18Then open Jaeger:
19
20- UI: `http://127.0.0.1:16686/search?service=executor-local`
21
22You can also query traces over HTTP:
23
24```bash
25curl "http://127.0.0.1:16686/api/traces?service=executor-local&limit=20"
26```
27
28Useful operations to search for:
29
30- `source.connect.http`
31- `source.catalog.sync`
32- `graphql.syncCatalog`
33- `graphql.introspection.fetch`
34- `graphql.manifest.extract`
35- `graphql.definitions.compile`
36- `graphql.operations.build`
37- `graphql.snapshot.build`
38
39Stop Jaeger:
40
41```bash
42bun run trace:down
43```
apps/executor/src/cli/main.ts+20−6
@@ -36,9 +36,12 @@ import { · 35 unmodified lines
36 openApiSdkPlugin,36 openApiSdkPlugin,
37} from "@executor/plugin-openapi-sdk";37} from "@executor/plugin-openapi-sdk";
38import {38import {
39 getExecutorSourcesAddHelpLines,39 getExecutorInternalToolHelpLines,
40 RuntimeExecutionResolverService,40 RuntimeExecutionResolverService,
41} from "@executor/platform-sdk/runtime";41} from "@executor/platform-sdk/runtime";
42import type {
43 ExecutorSdkPluginRegistry,
44} from "@executor/platform-sdk/plugins";
42import {45import {
43 createExecutorEffect,46 createExecutorEffect,
44 type ExecutorEffect as Executor,47 type ExecutorEffect as Executor,
@@ -228,7 +231,10 @@ const toEffectCliArgv = (args: readonly string[]): string[] => [ · 183 unmodified lines
228 ...args,231 ...args,
229];232];
230 233
231const buildWorkflowText = (namespaces: readonly string[] = []): string =>234const buildWorkflowText = (
235 namespaces: readonly string[] = [],
236 pluginRegistry?: ExecutorSdkPluginRegistry,
237): string =>
232 [238 [
233 "Execute TypeScript in sandbox; call tools via discovery workflow.",239 "Execute TypeScript in sandbox; call tools via discovery workflow.",
234 ...(namespaces.length > 0240 ...(namespaces.length > 0
@@ -242,12 +248,15 @@ const buildWorkflowText = (namespaces: readonly string[] = []): string => · 7 unmodified lines
242 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",248 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
243 "3) Call selected tools.<path>(input).",249 "3) Call selected tools.<path>(input).",
244 "4) Use source plugins to inspect or add API sources.",250 "4) Use source plugins to inspect or add API sources.",
245 ...getExecutorSourcesAddHelpLines(),251 ...(pluginRegistry
252 ? getExecutorInternalToolHelpLines(pluginRegistry)
253 : ["Use executor.* tools to inspect or manage API sources."]),
246 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",254 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",
247 "Do not use fetch; use tools.* only.",255 "Do not use fetch; use tools.* only.",
248 ].join("\n");256 ].join("\n");
249 257
250const getDefaultRunWorkflow = () => buildWorkflowText();258const getDefaultRunWorkflow = (pluginRegistry?: ExecutorSdkPluginRegistry) =>
259 buildWorkflowText([], pluginRegistry);
251 260
252const indentBlock = (value: string, prefix: string = " "): string =>261const indentBlock = (value: string, prefix: string = " "): string =>
253 value262 value
@@ -290,9 +299,10 @@ const closeExecutor = (executor: Executor) => · 36 unmodified lines
290 299
291const buildRunWorkflowText = (300const buildRunWorkflowText = (
292 catalog?: ToolCatalog,301 catalog?: ToolCatalog,
302 pluginRegistry?: ExecutorSdkPluginRegistry,
293): Effect.Effect<string, Error, never> => {303): Effect.Effect<string, Error, never> => {
294 if (!catalog) {304 if (!catalog) {
295 return Effect.succeed(getDefaultRunWorkflow());305 return Effect.succeed(getDefaultRunWorkflow(pluginRegistry));
296 }306 }
297 307
298 return catalog.listNamespaces({ limit: 200 }).pipe(308 return catalog.listNamespaces({ limit: 200 }).pipe(
@@ -301,6 +311,7 @@ const buildRunWorkflowText = ( · 2 unmodified lines
301 namespaces.length > 0311 namespaces.length > 0
302 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)312 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)
303 : ["none discovered yet"],313 : ["none discovered yet"],
314 pluginRegistry,
304 )315 )
305 ),316 ),
306 Effect.mapError(toError),317 Effect.mapError(toError),
@@ -360,7 +371,10 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 53 unmodified lines
360 Effect.mapError(toError),371 Effect.mapError(toError),
361 );372 );
362 373
363 return yield* buildRunWorkflowText(environment.catalog);374 return yield* buildRunWorkflowText(
375 environment.catalog,
376 executor.runtime.pluginRegistry,
377 );
364 }),378 }),
365 closeExecutor,379 closeExecutor,
366 ).pipe(380 ).pipe(
apps/web/src/components/source-favicon.tsx+88−10
@@ -1,9 +1,12 @@
1import { useState } from "react";1import { useState } from "react";
2import type { Source } from "@executor/react";2import type { Source } from "@executor/react";
3import { getGoogleDiscoveryIconUrlForService } from "@executor/plugin-google-discovery-react";
3import { getSourceFrontendIconUrl } from "../plugins";4import { getSourceFrontendIconUrl } from "../plugins";
5import type { SourcePreset } from "../plugins/source-presets";
6import { getSourceFaviconUrl } from "../lib/source-favicon";
4import { cn } from "../lib/utils";7import { cn } from "../lib/utils";
5 8
6type SourceKind = Source["kind"] | string;9type SourceKind = Source["kind"] | SourcePreset["kind"] | string;
7 10
8export function SourceFavicon({11export function SourceFavicon({
9 source,12 source,
@@ -14,12 +17,55 @@ export function SourceFavicon({ · 4 unmodified lines
14 className?: string;17 className?: string;
15 size?: number;18 size?: number;
16}) {19}) {
17 const faviconUrl = getSourceFrontendIconUrl(source);20 return (
21 <ResolvedSourceIcon
22 kind={source.kind}
23 faviconUrl={getSourceFrontendIconUrl(source)}
24 className={className}
25 size={size}
26 />
27 );
28}
29
30export function SourcePresetFavicon({
31 preset,
32 className,
33 size = 16,
34}: {
35 preset: SourcePreset;
36 className?: string;
37 size?: number;
38}) {
39 const faviconUrl = preset.kind === "google_discovery"
40 ? getGoogleDiscoveryIconUrlForService(preset.service)
41 : getSourceFaviconUrl(preset.previewUrl);
42
43 return (
44 <ResolvedSourceIcon
45 kind={preset.kind}
46 faviconUrl={faviconUrl ?? getSourceFaviconUrl(preset.name)}
47 className={className}
48 size={size}
49 />
50 );
51}
52
53function ResolvedSourceIcon({
54 kind,
55 faviconUrl,
56 className,
57 size = 16,
58}: {
59 kind: SourceKind;
60 faviconUrl: string | null;
61 className?: string;
62 size?: number;
63}) {
18 const [failedUrl, setFailedUrl] = useState<string | null>(null);64 const [failedUrl, setFailedUrl] = useState<string | null>(null);
19 const isFailed = Boolean(faviconUrl && failedUrl === faviconUrl);65 const isFailed = Boolean(faviconUrl && failedUrl === faviconUrl);
20 66
21 if (!faviconUrl || isFailed) {67 if (!faviconUrl || isFailed) {
22 return <DefaultSourceIcon kind={source.kind} className={className} />;68 return <DefaultSourceIcon kind={kind} className={className} />;
23 }69 }
24 70
25 return (71 return (
@@ -38,7 +84,7 @@ export function SourceFavicon({ · 12 unmodified lines
38}84}
39 85
40export function DefaultSourceIcon({86export function DefaultSourceIcon({
41 kind: _kind,87 kind,
42 className,88 className,
43}: {89}: {
44 kind: SourceKind;90 kind: SourceKind;
@@ -46,10 +92,42 @@ export function DefaultSourceIcon({ · 1 unmodified line
46}) {92}) {
47 const base = cn("shrink-0", className);93 const base = cn("shrink-0", className);
48 94
49 return (95 switch (kind) {
50 <svg viewBox="0 0 16 16" fill="none" className={base}>96 case "mcp":
51 <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.2" />97 return (
52 <path d="M5 6h6M5 8h4M5 10h3" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />98 <svg viewBox="0 0 16 16" fill="none" className={base}>
53 </svg>99 <rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
54 );100 <path d="M5 7h1M5 9h4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
101 </svg>
102 );
103 case "graphql":
104 return (
105 <svg viewBox="0 0 16 16" fill="none" className={base}>
106 <path d="M8 2L14 5.5V10.5L8 14L2 10.5V5.5L8 2Z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
107 <circle cx="8" cy="8" r="1.5" fill="currentColor" opacity="0.5" />
108 </svg>
109 );
110 case "google_discovery":
111 return (
112 <svg viewBox="0 0 16 16" fill="none" className={base}>
113 <path d="M8 2.5a5.5 5.5 0 1 0 0 11 5.3 5.3 0 0 0 3.82-1.55" stroke="currentColor" strokeWidth="1.2" />
114 <path d="M13 8H8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
115 <circle cx="12.5" cy="8" r="1.2" fill="currentColor" />
116 </svg>
117 );
118 case "openapi":
119 return (
120 <svg viewBox="0 0 16 16" fill="none" className={base}>
121 <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
122 <path d="M5 6h6M5 8h4M5 10h5" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
123 </svg>
124 );
125 default:
126 return (
127 <svg viewBox="0 0 16 16" fill="none" className={base}>
128 <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.2" />
129 <path d="M5 6h6M5 8h4M5 10h3" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
130 </svg>
131 );
132 }
55}133}
apps/web/src/components/source-plugins-reset-state.tsx+1−1
@@ -10,7 +10,7 @@ export function SourcePluginsResetState( · 9 unmodified lines
10 input: SourcePluginsResetStateProps,10 input: SourcePluginsResetStateProps,
11) {11) {
12 return (12 return (
13 <div className="min-h-0 flex-1 overflow-y-auto">13 <div className="min-h-0 flex-1 overflow-y-auto w-[300px]">
14 <div className="mx-auto max-w-3xl px-6 py-10 lg:px-10 lg:py-14">14 <div className="mx-auto max-w-3xl px-6 py-10 lg:px-10 lg:py-14">
15 <div className="rounded-2xl border border-border bg-card p-8">15 <div className="rounded-2xl border border-border bg-card p-8">
16 <h2 className="font-display text-xl tracking-tight text-foreground">16 <h2 className="font-display text-xl tracking-tight text-foreground">
apps/web/src/lib/source-favicon.ts+158−0
@@ -0,0 +1,158 @@
1import { parse as parseDomain } from "tldts";
2
3const RAW_HOSTS = new Set([
4 "raw.githubusercontent.com",
5 "cdn.jsdelivr.net",
6 "raw.github.com",
7 "unpkg.com",
8]);
9
10const BRAND_DOMAINS: Record<string, string> = {
11 anilist: "anilist.co",
12 axiom: "axiom.co",
13 chrome: "developer.chrome.com",
14 deepwiki: "deepwiki.com",
15 github: "github.com",
16 gitlab: "gitlab.com",
17 linear: "linear.app",
18 neon: "neon.tech",
19 openai: "openai.com",
20 stripe: "stripe.com",
21 vercel: "vercel.com",
22};
23
24const IGNORED_BRAND_TOKENS = new Set([
25 "api",
26 "app",
27 "apps",
28 "cli",
29 "cloud",
30 "com",
31 "console",
32 "dev",
33 "docs",
34 "doc",
35 "graphql",
36 "http",
37 "https",
38 "json",
39 "latest",
40 "mcp",
41 "net",
42 "none",
43 "npm",
44 "npx",
45 "openapi",
46 "org",
47 "plugin",
48 "plugins",
49 "raw",
50 "rest",
51 "sdk",
52 "server",
53 "service",
54 "services",
55 "source",
56 "sources",
57 "stdio",
58 "transport",
59 "uv",
60 "uvx",
61 "yaml",
62 "yarn",
63 "yml",
64]);
65
66const parseUrl = (value: string): URL | null => {
67 try {
68 return new URL(value);
69 } catch {
70 return null;
71 }
72};
73
74const toDomainSeedUrl = (value: string): string | null => {
75 const parsed = parseDomain(value);
76 const domain = parsed.domain ?? BRAND_DOMAINS[value] ?? null;
77 return domain ? `https://${domain}` : null;
78};
79
80const isMeaninglessToken = (token: string): boolean =>
81 token.length <= 1
82 || IGNORED_BRAND_TOKENS.has(token)
83 || /^v?\d+$/.test(token);
84
85const inferBrandToken = (value: string): string | null => {
86 const token = value
87 .trim()
88 .toLowerCase()
89 .split(/[^a-z0-9]+/g)
90 .find((candidate) => !isMeaninglessToken(candidate));
91
92 return token && token.length > 0 ? token : null;
93};
94
95const toBrandSeedUrl = (value: string): string | null => {
96 const brand = inferBrandToken(value);
97 if (!brand) {
98 return null;
99 }
100
101 return `https://${BRAND_DOMAINS[brand] ?? `${brand}.com`}`;
102};
103
104const normalizeSeedUrl = (value: string | null | undefined): string | null => {
105 const trimmed = value?.trim();
106 if (!trimmed) {
107 return null;
108 }
109
110 const parsed = parseUrl(trimmed) ?? parseUrl(`https://${trimmed}`);
111 if (parsed) {
112 if (RAW_HOSTS.has(parsed.hostname)) {
113 const segments = parsed.pathname
114 .split("/")
115 .map((segment) => segment.trim())
116 .filter((segment) => segment.length > 0);
117
118 for (const segment of segments) {
119 const withoutExtension = segment.replace(/\.(ya?ml|json)$/i, "");
120 const seeded =
121 toDomainSeedUrl(withoutExtension)
122 ?? toBrandSeedUrl(withoutExtension);
123 if (seeded) {
124 return seeded;
125 }
126 }
127 }
128
129 return toDomainSeedUrl(parsed.hostname) ?? toBrandSeedUrl(parsed.hostname);
130 }
131
132 return toDomainSeedUrl(trimmed) ?? toBrandSeedUrl(trimmed);
133};
134
135export const getSourceFaviconUrl = (
136 value: string | null | undefined,
137): string | null => {
138 const seedUrl = normalizeSeedUrl(value);
139 if (!seedUrl) {
140 return null;
141 }
142
143 try {
144 const hostname = new URL(seedUrl).hostname;
145 const parsed = parseDomain(hostname);
146 const domain = parsed.domain ?? hostname;
147 return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`;
148 } catch {
149 return null;
150 }
151};
152
153export const getFallbackSourceFaviconUrl = (input: {
154 namespace?: string | null;
155 name?: string | null;
156}): string | null =>
157 getSourceFaviconUrl(input.namespace)
158 ?? getSourceFaviconUrl(input.name);
apps/web/src/plugins/index.ts+8−10
@@ -11,33 +11,26 @@ import { · 10 unmodified lines
11import {11import {
12 GraphqlReactPlugin,12 GraphqlReactPlugin,
13} from "@executor/plugin-graphql-react";13} from "@executor/plugin-graphql-react";
14import {
15 LocalToolsReactPlugin,
16} from "@executor/plugin-local-tools-react";
17import {14import {
18 McpReactPlugin,15 McpReactPlugin,
19} from "@executor/plugin-mcp-react";16} from "@executor/plugin-mcp-react";
20import {17import {
21 OpenApiReactPlugin,18 OpenApiReactPlugin,
22} from "@executor/plugin-openapi-react";19} from "@executor/plugin-openapi-react";
23import {
24 OnePasswordReactPlugin,
25} from "@executor/plugin-onepassword-react";
26import {20import {
27 createExecutorPluginPaths,21 createExecutorPluginPaths,
28 createSourcePluginPaths,22 createSourcePluginPaths,
29 registerExecutorFrontendPlugins,23 registerExecutorFrontendPlugins,
30 type ExecutorFrontendPlugin,24 type ExecutorFrontendPlugin,
31} from "@executor/react/plugins";25} from "@executor/react/plugins";
26import { getFallbackSourceFaviconUrl } from "../lib/source-favicon";
32 27
33const frontendPlugins = [28const frontendPlugins = [
34 ExecutionHistoryReactPlugin,29 ExecutionHistoryReactPlugin,
35 LocalToolsReactPlugin,
36 McpReactPlugin,30 McpReactPlugin,
37 GraphqlReactPlugin,31 GraphqlReactPlugin,
38 GoogleDiscoveryReactPlugin,32 GoogleDiscoveryReactPlugin,
39 OpenApiReactPlugin,33 OpenApiReactPlugin,
40 OnePasswordReactPlugin,
41] as const satisfies readonly ExecutorFrontendPlugin[];34] as const satisfies readonly ExecutorFrontendPlugin[];
42 35
43const frontendPluginRegistry = registerExecutorFrontendPlugins(frontendPlugins);36const frontendPluginRegistry = registerExecutorFrontendPlugins(frontendPlugins);
@@ -85,10 +78,15 @@ export const getSourceFrontendPaths = (kind: string) => { · 41 unmodified lines
85 return plugin ? createSourcePluginPaths(plugin.key) : null;78 return plugin ? createSourcePluginPaths(plugin.key) : null;
86};79};
87 80
81const isGoogleDiscoverySource = (kind: string): boolean =>
82 kind === "google_discovery"
83 || kind === "google-discovery";
84
88export const getSourceFrontendIconUrl = (source: Source) =>85export const getSourceFrontendIconUrl = (source: Source) =>
89 source.kind === "google-discovery"86 (isGoogleDiscoverySource(source.kind)
90 ? getGoogleDiscoveryIconUrl(source)87 ? getGoogleDiscoveryIconUrl(source)
91 : null;88 : null)
89 ?? getFallbackSourceFaviconUrl(source);
92 90
93export const getSecretStoreFrontendPlugin = (kind: string) =>91export const getSecretStoreFrontendPlugin = (kind: string) =>
94 registeredFrontendPlugins.find((plugin) => plugin.secretStore?.kind === kind) ?? null;92 registeredFrontendPlugins.find((plugin) => plugin.secretStore?.kind === kind) ?? null;
apps/web/src/plugins/pages.tsx+2−2
@@ -13,7 +13,7 @@ import { · 12 unmodified lines
13 type FrontendPluginRouteSearch,13 type FrontendPluginRouteSearch,
14} from "@executor/react/plugins";14} from "@executor/react/plugins";
15 15
16import { DefaultSourceIcon } from "../components/source-favicon";16import { SourcePresetFavicon } from "../components/source-favicon";
17import { LocalMcpInstallCard } from "../components/local-mcp-install-card";17import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
18import { SourcePluginsResetState } from "../components/source-plugins-reset-state";18import { SourcePluginsResetState } from "../components/source-plugins-reset-state";
19import {19import {
@@ -202,7 +202,7 @@ export function SourcePluginsIndexPage() { · 182 unmodified lines
202 >202 >
203 <div className="mb-2 flex items-center gap-2.5">203 <div className="mb-2 flex items-center gap-2.5">
204 <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">204 <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
205 <DefaultSourceIcon kind={preset.kind} className="size-4" />205 <SourcePresetFavicon preset={preset} className="size-4" />
206 </div>206 </div>
207 <div className="min-w-0">207 <div className="min-w-0">
208 <div className="truncate text-sm font-semibold text-foreground">208 <div className="truncate text-sm font-semibold text-foreground">
packages/clients/react/src/plugins/components/source-tool-workbench.tsx+17−8
@@ -304,12 +304,18 @@ const SourceToolTreeNodeView = (props: { · 303 unmodified lines
304 variant="ghost"304 variant="ghost"
305 size="icon"305 size="icon"
306 onClick={() => setOpen((value) => !value)}306 onClick={() => setOpen((value) => !value)}
307 aria-expanded={open}
307 className="shrink-0 size-auto rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"308 className="shrink-0 size-auto rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"
308 style={{ marginLeft: paddingLeft }}309 style={{ marginLeft: paddingLeft }}
309 >310 >
310 <IconChevron311 <IconChevron
311 className={cn("shrink-0 transition-transform duration-150", open && "rotate-90")}312 className="shrink-0 transition-transform duration-150"
312 style={{ width: 8, height: 8 }}313 style={{
314 width: 8,
315 height: 8,
316 transform: open ? "rotate(90deg)" : "rotate(0deg)",
317 transformOrigin: "50% 50%",
318 }}
313 />319 />
314 </Button>320 </Button>
315 <SourceToolListItem321 <SourceToolListItem
@@ -328,6 +334,7 @@ const SourceToolTreeNodeView = (props: { · 12 unmodified lines
328 type="button"334 type="button"
329 variant="ghost"335 variant="ghost"
330 onClick={() => setOpen((value) => !value)}336 onClick={() => setOpen((value) => !value)}
337 aria-expanded={open}
331 className={cn(338 className={cn(
332 "group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40",339 "group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40",
333 open ? "text-foreground/80" : "text-muted-foreground/60",340 open ? "text-foreground/80" : "text-muted-foreground/60",
@@ -335,11 +342,13 @@ const SourceToolTreeNodeView = (props: { · 1 unmodified line
335 style={{ paddingLeft }}342 style={{ paddingLeft }}
336 >343 >
337 <IconChevron344 <IconChevron
338 className={cn(345 className="shrink-0 text-muted-foreground/30 transition-transform duration-150"
339 "shrink-0 text-muted-foreground/30 transition-transform duration-150",346 style={{
340 open && "rotate-90",347 width: 8,
341 )}348 height: 8,
342 style={{ width: 8, height: 8 }}349 transform: open ? "rotate(90deg)" : "rotate(0deg)",
350 transformOrigin: "50% 50%",
351 }}
343 />352 />
344 <IconFolder353 <IconFolder
345 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}354 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}
@@ -632,7 +641,7 @@ export const SourceToolModelWorkbench = ( · 286 unmodified lines
632 </div>641 </div>
633 </div>642 </div>
634 643
635 <div className="flex-1 overflow-y-auto">644 <div className="flex-1 overflow-y-auto w-[300px]">
636 {filteredTools.length === 0 ? (645 {filteredTools.length === 0 ? (
637 <div className="p-4 text-center text-[13px] text-muted-foreground/50">646 <div className="p-4 text-center text-[13px] text-muted-foreground/50">
638 {terms.length > 0 ? "No tools match your filter" : "No tools available"}647 {terms.length > 0 ? "No tools match your filter" : "No tools available"}
packages/hosts/mcp/src/index.ts+8−5
@@ -2,7 +2,7 @@ import type { · 1 unmodified line
2 ExecutionEnvelope,2 ExecutionEnvelope,
3} from "@executor/platform-sdk/schema";3} from "@executor/platform-sdk/schema";
4import {4import {
5 getExecutorSourcesAddHelpLines,5 getExecutorInternalToolHelpLines,
6 RuntimeSourceCatalogStoreService,6 RuntimeSourceCatalogStoreService,
7 createExecution,7 createExecution,
8 getExecution,8 getExecution,
@@ -152,6 +152,7 @@ type CatalogLike = { · 143 unmodified lines
152const executeDescriptionToolsPerSource = 5;152const executeDescriptionToolsPerSource = 5;
153 153
154const buildExecuteWorkflowText = (154const buildExecuteWorkflowText = (
155 runtime: ExecutorRuntime,
155 sourceToolExamples: ReadonlyArray<{156 sourceToolExamples: ReadonlyArray<{
156 sourceName: string;157 sourceName: string;
157 toolPaths: readonly string[];158 toolPaths: readonly string[];
@@ -173,7 +174,7 @@ const buildExecuteWorkflowText = ( · 15 unmodified lines
173 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",174 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
174 "3) Call selected tools.<path>(input).",175 "3) Call selected tools.<path>(input).",
175 "4) Use source plugins to inspect or add API sources.",176 "4) Use source plugins to inspect or add API sources.",
176 ...getExecutorSourcesAddHelpLines(),177 ...getExecutorInternalToolHelpLines(runtime.pluginRegistry),
177 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",178 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",
178 "Do not use fetch; use tools.* only.",179 "Do not use fetch; use tools.* only.",
179 ].join("\n");180 ].join("\n");
@@ -201,11 +202,13 @@ const loadExecuteDescription = (runtime: ExecutorRuntime): Promise<string> => · 21 unmodified lines
201 .filter((catalog) => catalog.toolPaths.length > 0);202 .filter((catalog) => catalog.toolPaths.length > 0);
202 203
203 if (sourceToolExamples.length === 0) {204 if (sourceToolExamples.length === 0) {
204 return buildExecuteWorkflowText();205 return buildExecuteWorkflowText(runtime);
205 }206 }
206 207
207 return buildExecuteWorkflowText(sourceToolExamples);208 return buildExecuteWorkflowText(runtime, sourceToolExamples);
208 }).pipe(Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText()))),209 }).pipe(
210 Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText(runtime))),
211 ),
209 );212 );
210 213
211const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {214const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {
packages/platform/sdk/src/executor-effect.ts+8−9
@@ -83,15 +83,13 @@ import { · 82 unmodified lines
83 listExecutions,83 listExecutions,
84 resumeExecution,84 resumeExecution,
85} from "./runtime/execution/service";85} from "./runtime/execution/service";
86import type {
87 ExecutorSdkPluginHost,
88 ExecutorSdkPlugin,
89 ExecutorSdkPluginExtensions,
90 PluginCleanup,
91} from "./plugins";
92import {86import {
93 configureExecutorSourcePlugins,87 registerExecutorSdkPlugins,
94} from "./runtime/sources/source-plugins";88 type ExecutorSdkPluginHost,
89 type ExecutorSdkPlugin,
90 type ExecutorSdkPluginExtensions,
91 type PluginCleanup,
92} from "./plugins";
95 93
96type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<94type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<
97 Effect.Effect.Success<T>,95 Effect.Effect.Success<T>,
@@ -320,13 +318,14 @@ export const createExecutorEffect = < · 222 unmodified lines
320>(318>(
321 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,319 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,
322): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {320): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {
323 configureExecutorSourcePlugins(options.plugins ?? []);321 const pluginRegistry = registerExecutorSdkPlugins(options.plugins ?? []);
324 322
325 return Effect.flatMap(323 return Effect.flatMap(
326 options.backend.createRuntime({324 options.backend.createRuntime({
327 executionResolver: options.executionResolver,325 executionResolver: options.executionResolver,
328 resolveSecretMaterial: options.resolveSecretMaterial,326 resolveSecretMaterial: options.resolveSecretMaterial,
329 getLocalServerBaseUrl: options.getLocalServerBaseUrl,327 getLocalServerBaseUrl: options.getLocalServerBaseUrl,
328 pluginRegistry,
330 }),329 }),
331 (runtime) =>330 (runtime) =>
332 Effect.gen(function* () {331 Effect.gen(function* () {
packages/platform/sdk/src/plugins.ts+321−41
@@ -6,12 +6,14 @@ import type { · 5 unmodified lines
6} from "@executor/source-core";6} from "@executor/source-core";
7import type { ExecutorEffect } from "./executor-effect";7import type { ExecutorEffect } from "./executor-effect";
8import type { ExecutorScopeContext } from "./scope";8import type { ExecutorScopeContext } from "./scope";
9import type {9import {
10 Source as ExecutorSource,10 type LocalConfigSource,
11 SecretMaterial,11 type LocalExecutorConfig,
12 SecretMaterialPurpose,12 type Source as ExecutorSource,
13 SecretRef,13 type SecretMaterial,
14 SecretStore,14 type SecretMaterialPurpose,
15 type SecretRef,
16 type SecretStore,
15} from "./schema";17} from "./schema";
16import type {18import type {
17 SourceInvokeInput,19 SourceInvokeInput,
@@ -19,12 +21,13 @@ import type { · 1 unmodified line
19} from "@executor/source-core";21} from "@executor/source-core";
20import type * as Schema from "effect/Schema";22import type * as Schema from "effect/Schema";
21import { runtimeEffectError } from "./runtime/effect-errors";23import { runtimeEffectError } from "./runtime/effect-errors";
24import { ScopeConfigStore } from "./runtime/scope/storage";
22 25
23export type PluginCleanup = {26export type PluginCleanup = {
24 close: () => void | Promise<void>;27 close: () => void | Promise<void>;
25};28};
26 29
27export type ExecutorSdkPluginHost = {};30export type ExecutorSdkPluginHost = ExecutorSourcePluginInternalHost;
28 31
29export type ExecutorSdkPluginContext = {32export type ExecutorSdkPluginContext = {
30 executor: ExecutorEffect & Record<string, unknown>;33 executor: ExecutorEffect & Record<string, unknown>;
@@ -41,6 +44,7 @@ export type ExecutorSdkPluginStartContext< · 10 unmodified lines
41type ExecutorSdkPluginInternals = {44type ExecutorSdkPluginInternals = {
42 sources?: readonly ExecutorSourceContribution<any>[];45 sources?: readonly ExecutorSourceContribution<any>[];
43 secretStores?: readonly ExecutorSecretStoreContribution<any>[];46 secretStores?: readonly ExecutorSecretStoreContribution<any>[];
47 managementTools?: readonly ExecutorManagementToolContribution<any, any>[];
44};48};
45 49
46const executorSdkPluginInternalsSymbol = Symbol.for(50const executorSdkPluginInternalsSymbol = Symbol.for(
@@ -184,6 +188,45 @@ export type ExecutorSecretStorePluginApi< · 137 unmodified lines
184 ) => Effect.Effect<boolean, Error, any>;188 ) => Effect.Effect<boolean, Error, any>;
185};189};
186 190
191export type ExecutorManagementToolContribution<
192 TInput = unknown,
193 TOutput = unknown,
194> = {
195 path: `executor.${string}`;
196 description: string;
197 inputSchema: Schema.Schema<TInput, any, never>;
198 outputSchema: Schema.Schema<TOutput, any, never>;
199 execute: (input: {
200 args: TInput;
201 host: ExecutorSdkPluginHost;
202 }) => Effect.Effect<TOutput, Error, any>;
203};
204
205export type ExecutorSourcePluginManagementToolDefinition<
206 TName extends string = string,
207 TInput = unknown,
208 TOutput = unknown,
209 TConnectInput = unknown,
210 TSourceConfig = unknown,
211 TUpdateInput extends {
212 sourceId: string;
213 config: TSourceConfig;
214 } = {
215 sourceId: string;
216 config: TSourceConfig;
217 },
218> = {
219 name: TName;
220 description: string;
221 inputSchema: Schema.Schema<TInput, any, never>;
222 outputSchema: Schema.Schema<TOutput, any, never>;
223 execute: (input: {
224 args: TInput;
225 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
226 host: ExecutorSourcePluginInternalHost;
227 }) => Effect.Effect<TOutput, Error, any>;
228};
229
187export type ExecutorSourcePluginDefinition<230export type ExecutorSourcePluginDefinition<
188 TAddInput,231 TAddInput,
189 TConnectInput,232 TConnectInput,
@@ -227,6 +270,17 @@ export type ExecutorSourcePluginDefinition< · 37 unmodified lines
227 stored: TStored | null;270 stored: TStored | null;
228 }) => Effect.Effect<void, Error, any>;271 }) => Effect.Effect<void, Error, any>;
229 };272 };
273 localConfig?: {
274 toConfigSource: (input: {
275 source: ExecutorSource;
276 stored: TStored;
277 }) => LocalConfigSource;
278 recoverStored: (input: {
279 source: ExecutorSource;
280 config: LocalConfigSource;
281 loadedConfig: LocalExecutorConfig | null;
282 }) => Effect.Effect<TStored, Error, any> | TStored;
283 };
230 catalog: {284 catalog: {
231 kind: SourceCatalogKind;285 kind: SourceCatalogKind;
232 identity?: (input: {286 identity?: (input: {
@@ -397,6 +451,14 @@ export type ExecutorSourcePluginInput< · 164 unmodified lines
397 TStored,451 TStored,
398 TUpdateInput452 TUpdateInput
399 >;453 >;
454 tools?: readonly ExecutorSourcePluginManagementToolDefinition<
455 string,
456 any,
457 any,
458 TConnectInput,
459 TSourceConfig,
460 TUpdateInput
461 >[];
400 extendExecutor?: (input: ExecutorSdkPluginContext & {462 extendExecutor?: (input: ExecutorSdkPluginContext & {
401 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;463 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
402 }) => TExtension;464 }) => TExtension;
@@ -467,6 +529,121 @@ const loadSourceOfKind = ( · 64 unmodified lines
467 return source;529 return source;
468 });530 });
469 531
532const cloneJson = <T>(value: T): T =>
533 JSON.parse(JSON.stringify(value)) as T;
534
535const fromMaybeEffect = <A>(
536 value: Effect.Effect<A, Error, any> | A,
537): Effect.Effect<A, Error, any> =>
538 Effect.isEffect(value)
539 ? value
540 : Effect.succeed(value);
541
542const persistSourceLocalConfig = <
543 TAddInput,
544 TConnectInput,
545 TSourceConfig,
546 TStored,
547 TUpdateInput extends {
548 sourceId: string;
549 config: TSourceConfig;
550 },
551>(
552 definition: ExecutorSourcePluginDefinition<
553 TAddInput,
554 TConnectInput,
555 TSourceConfig,
556 TStored,
557 TUpdateInput
558 >,
559 input: {
560 source: ExecutorSource;
561 stored: TStored;
562 },
563): Effect.Effect<void, Error, ScopeConfigStore> =>
564 definition.localConfig
565 ? Effect.gen(function* () {
566 const localConfig = definition.localConfig!;
567 const scopeConfigStore = yield* ScopeConfigStore;
568 const loadedConfig = yield* scopeConfigStore.load();
569 const projectConfig = cloneJson(loadedConfig.projectConfig ?? {});
570 const sources = {
571 ...projectConfig.sources,
572 [input.source.id]: localConfig.toConfigSource({
573 source: input.source,
574 stored: input.stored,
575 }),
576 };
577
578 yield* scopeConfigStore.writeProject({
579 config: {
580 ...projectConfig,
581 sources,
582 },
583 });
584 })
585 : Effect.void;
586
587const loadStoredOrRecover = <
588 TAddInput,
589 TConnectInput,
590 TSourceConfig,
591 TStored,
592 TUpdateInput extends {
593 sourceId: string;
594 config: TSourceConfig;
595 },
596>(
597 definition: ExecutorSourcePluginDefinition<
598 TAddInput,
599 TConnectInput,
600 TSourceConfig,
601 TStored,
602 TUpdateInput
603 >,
604 source: ExecutorSource,
605): Effect.Effect<TStored | null, Error, ScopeConfigStore> =>
606 Effect.gen(function* () {
607 const localConfig = definition.localConfig;
608 const stored = yield* definition.storage.get({
609 scopeId: source.scopeId,
610 sourceId: source.id,
611 });
612 if (stored !== null || localConfig === undefined) {
613 return stored;
614 }
615
616 const scopeConfigStore = yield* ScopeConfigStore;
617 const loadedConfig = yield* scopeConfigStore.load();
618 const config = loadedConfig.config?.sources?.[source.id] ?? null;
619 if (config === null) {
620 return null;
621 }
622
623 if (config.kind !== definition.kind) {
624 return yield* runtimeEffectError(
625 "plugins",
626 `Source ${source.id} config kind ${config.kind} does not match ${definition.kind}.`,
627 );
628 }
629
630 const recovered = yield* fromMaybeEffect(
631 localConfig.recoverStored({
632 source,
633 config,
634 loadedConfig: loadedConfig.config,
635 }),
636 );
637
638 yield* definition.storage.put({
639 scopeId: source.scopeId,
640 sourceId: source.id,
641 value: recovered,
642 });
643
644 return recovered;
645 });
646
470const createExecutorSourcePluginApi = <647const createExecutorSourcePluginApi = <
471 TAddInput,648 TAddInput,
472 TConnectInput,649 TConnectInput,
@@ -497,10 +674,7 @@ const createExecutorSourcePluginApi = < · 24 unmodified lines
497 definition,674 definition,
498 host,675 host,
499 });676 });
500 const stored = yield* definition.storage.get({677 const stored = yield* loadStoredOrRecover(definition, source);
501 scopeId: source.scopeId,
502 sourceId: source.id,
503 });
504 if (stored === null) {678 if (stored === null) {
505 return yield* runtimeEffectError(679 return yield* runtimeEffectError(
506 "plugins",680 "plugins",
@@ -525,6 +699,10 @@ const createExecutorSourcePluginApi = < · 18 unmodified lines
525 sourceId: source.id,699 sourceId: source.id,
526 value: created.stored,700 value: created.stored,
527 });701 });
702 yield* persistSourceLocalConfig(definition, {
703 source,
704 stored: created.stored,
705 });
528 706
529 return yield* host.sources.refreshCatalog(source.id);707 return yield* host.sources.refreshCatalog(source.id);
530 }),708 }),
@@ -545,6 +723,10 @@ const createExecutorSourcePluginApi = < · 14 unmodified lines
545 sourceId: saved.id,723 sourceId: saved.id,
546 value: updated.stored,724 value: updated.stored,
547 });725 });
726 yield* persistSourceLocalConfig(definition, {
727 source: saved,
728 stored: updated.stored,
729 });
548 730
549 return yield* host.sources.refreshCatalog(saved.id);731 return yield* host.sources.refreshCatalog(saved.id);
550 }),732 }),
@@ -716,6 +898,7 @@ const createExecutorSecretStorePluginApi = < · 165 unmodified lines
716});898});
717 899
718type ExecutorSourceContribution<TInput = unknown> = {900type ExecutorSourceContribution<TInput = unknown> = {
901 pluginKey: string;
719 kind: string;902 kind: string;
720 displayName: string;903 displayName: string;
721 inputSchema: Schema.Schema<TInput, any, never>;904 inputSchema: Schema.Schema<TInput, any, never>;
@@ -816,7 +999,26 @@ type ExecutorSecretStoreContribution<TInput = unknown> = { · 94 unmodified lines
816 }, Error, any>;999 }, Error, any>;
817};1000};
818 1001
1002export type ExecutorSdkPluginRegistry = {
1003 plugins: readonly ExecutorSdkPlugin<any, any>[];
1004 sources: readonly ExecutorSourceContribution<any>[];
1005 secretStores: readonly ExecutorSecretStoreContribution<any>[];
1006 managementTools: readonly ExecutorManagementToolContribution<any, any>[];
1007 getSourceContribution: (kind: string) => ExecutorSourceContribution<any>;
1008 getSourceContributionForSource: (
1009 source: Pick<ExecutorSource, "kind">,
1010 ) => ExecutorSourceContribution<any>;
1011 getSecretStoreContribution: (kind: string) => ExecutorSecretStoreContribution<any>;
1012 getSecretStoreContributionForStore: (
1013 store: Pick<SecretStore, "kind">,
1014 ) => ExecutorSecretStoreContribution<any>;
1015 getManagementTool: (
1016 path: string,
1017 ) => ExecutorManagementToolContribution<any, any>;
1018};
1019
819const createExecutorSourceContribution = <1020const createExecutorSourceContribution = <
1021 TKey extends string,
820 TAddInput,1022 TAddInput,
821 TConnectInput,1023 TConnectInput,
822 TSourceConfig,1024 TSourceConfig,
@@ -826,46 +1028,44 @@ const createExecutorSourceContribution = < · 3 unmodified lines
826 config: TSourceConfig;1028 config: TSourceConfig;
827 },1029 },
828>(1030>(
829 definition: ExecutorSourcePluginDefinition<1031 input: {
830 TAddInput,1032 key: TKey;
831 TConnectInput,1033 source: ExecutorSourcePluginDefinition<
832 TSourceConfig,1034 TAddInput,
833 TStored,1035 TConnectInput,
834 TUpdateInput1036 TSourceConfig,
835 >,1037 TStored,
1038 TUpdateInput
1039 >;
1040 },
836): ExecutorSourceContribution<TAddInput> => ({1041): ExecutorSourceContribution<TAddInput> => ({
837 kind: definition.kind,1042 pluginKey: input.key,
838 displayName: definition.displayName,1043 kind: input.source.kind,
839 inputSchema: definition.add.inputSchema,1044 displayName: input.source.displayName,
840 inputSignatureWidth: definition.add.inputSignatureWidth,1045 inputSchema: input.source.add.inputSchema,
841 helpText: definition.add.helpText,1046 inputSignatureWidth: input.source.add.inputSignatureWidth,
842 catalogKind: definition.catalog.kind,1047 helpText: input.source.add.helpText,
843 catalogIdentity: definition.catalog.identity,1048 catalogKind: input.source.catalog.kind,
1049 catalogIdentity: input.source.catalog.identity,
844 createSource: ({ args, host }) =>1050 createSource: ({ args, host }) =>
845 createExecutorSourcePluginApi(definition, host).createSource(1051 createExecutorSourcePluginApi(input.source, host).createSource(
846 definition.add.toConnectInput(args),1052 input.source.add.toConnectInput(args),
847 ),1053 ),
848 syncCatalog: ({ source }) =>1054 syncCatalog: ({ source }) =>
849 Effect.flatMap(1055 Effect.flatMap(
850 definition.storage.get({1056 loadStoredOrRecover(input.source, source),
851 scopeId: source.scopeId,
852 sourceId: source.id,
853 }),
854 (stored) =>1057 (stored) =>
855 definition.catalog.sync({1058 input.source.catalog.sync({
856 source,1059 source,
857 stored,1060 stored,
858 }),1061 }),
859 ),1062 ),
860 invoke: (input) =>1063 invoke: (invokeInput) =>
861 Effect.flatMap(1064 Effect.flatMap(
862 definition.storage.get({1065 loadStoredOrRecover(input.source, invokeInput.source),
863 scopeId: input.source.scopeId,
864 sourceId: input.source.id,
865 }),
866 (stored) =>1066 (stored) =>
867 definition.catalog.invoke({1067 input.source.catalog.invoke({
868 ...input,1068 ...invokeInput,
869 stored,1069 stored,
870 }),1070 }),
871 ),1071 ),
@@ -1064,6 +1264,53 @@ const createExecutorSecretStoreContribution = < · 192 unmodified lines
1064 ),1264 ),
1065});1265});
1066 1266
1267const createExecutorSourceManagementToolContribution = <
1268 TKey extends string,
1269 TName extends string,
1270 TInput,
1271 TOutput,
1272 TConnectInput,
1273 TSourceConfig,
1274 TStored,
1275 TUpdateInput extends {
1276 sourceId: string;
1277 config: TSourceConfig;
1278 },
1279>(
1280 input: {
1281 key: TKey;
1282 source: ExecutorSourcePluginDefinition<
1283 any,
1284 TConnectInput,
1285 TSourceConfig,
1286 TStored,
1287 TUpdateInput
1288 >;
1289 tool: ExecutorSourcePluginManagementToolDefinition<
1290 TName,
1291 TInput,
1292 TOutput,
1293 TConnectInput,
1294 TSourceConfig,
1295 TUpdateInput
1296 >;
1297 },
1298): ExecutorManagementToolContribution<TInput, TOutput> => ({
1299 path: `executor.${input.key}.${input.tool.name}`,
1300 description: input.tool.description,
1301 inputSchema: input.tool.inputSchema,
1302 outputSchema: input.tool.outputSchema,
1303 execute: ({ args, host }) =>
1304 input.tool.execute({
1305 args,
1306 host: host as ExecutorSourcePluginInternalHost,
1307 source: createExecutorSourcePluginApi(
1308 input.source,
1309 host as ExecutorSourcePluginInternalHost,
1310 ),
1311 }),
1312});
1313
1067export const defineExecutorSourcePlugin = <1314export const defineExecutorSourcePlugin = <
1068 const TKey extends string,1315 const TKey extends string,
1069 TAddInput,1316 TAddInput,
@@ -1110,7 +1357,17 @@ export const defineExecutorSourcePlugin = < · 40 unmodified lines
1110 })1357 })
1111 : undefined,1358 : undefined,
1112 [executorSdkPluginInternalsSymbol]: {1359 [executorSdkPluginInternalsSymbol]: {
1113 sources: [createExecutorSourceContribution(input.source)],1360 sources: [createExecutorSourceContribution({
1361 key: input.key,
1362 source: input.source,
1363 })],
1364 managementTools: (input.tools ?? []).map((tool) =>
1365 createExecutorSourceManagementToolContribution({
1366 key: input.key,
1367 source: input.source,
1368 tool,
1369 })
1370 ),
1114 },1371 },
1115 }))(input.extendExecutor, input.start);1372 }))(input.extendExecutor, input.start);
1116 1373
@@ -1179,6 +1436,10 @@ export const registerExecutorSdkPlugins = ( · 62 unmodified lines
1179 const pluginKeys = new Set<string>();1436 const pluginKeys = new Set<string>();
1180 const sources = new Map<string, ExecutorSourceContribution<any>>();1437 const sources = new Map<string, ExecutorSourceContribution<any>>();
1181 const secretStores = new Map<string, ExecutorSecretStoreContribution<any>>();1438 const secretStores = new Map<string, ExecutorSecretStoreContribution<any>>();
1439 const managementTools = new Map<
1440 string,
1441 ExecutorManagementToolContribution<any, any>
1442 >();
1182 1443
1183 for (const plugin of plugins) {1444 for (const plugin of plugins) {
1184 if (pluginKeys.has(plugin.key)) {1445 if (pluginKeys.has(plugin.key)) {
@@ -1208,6 +1469,14 @@ export const registerExecutorSdkPlugins = ( · 23 unmodified lines
1208 1469
1209 secretStores.set(secretStore.kind, secretStore);1470 secretStores.set(secretStore.kind, secretStore);
1210 }1471 }
1472
1473 for (const tool of internals?.managementTools ?? []) {
1474 if (managementTools.has(tool.path)) {
1475 throw new Error(`Duplicate executor management tool: ${tool.path}`);
1476 }
1477
1478 managementTools.set(tool.path, tool);
1479 }
1211 }1480 }
1212 1481
1213 const getSourceContribution = (kind: string) => {1482 const getSourceContribution = (kind: string) => {
@@ -1235,13 +1504,24 @@ export const registerExecutorSdkPlugins = ( · 21 unmodified lines
1235 store: Pick<SecretStore, "kind">,1504 store: Pick<SecretStore, "kind">,
1236 ) => getSecretStoreContribution(store.kind);1505 ) => getSecretStoreContribution(store.kind);
1237 1506
1507 const getManagementTool = (path: string) => {
1508 const tool = managementTools.get(path);
1509 if (!tool) {
1510 throw new Error(`Unsupported executor management tool: ${path}`);
1511 }
1512
1513 return tool;
1514 };
1515
1238 return {1516 return {
1239 plugins,1517 plugins,
1240 sources: [...sources.values()],1518 sources: [...sources.values()],
1241 secretStores: [...secretStores.values()],1519 secretStores: [...secretStores.values()],
1520 managementTools: [...managementTools.values()],
1242 getSourceContribution,1521 getSourceContribution,
1243 getSourceContributionForSource,1522 getSourceContributionForSource,
1244 getSecretStoreContribution,1523 getSecretStoreContribution,
1245 getSecretStoreContributionForStore,1524 getSecretStoreContributionForStore,
1246 };1525 getManagementTool,
1526 } satisfies ExecutorSdkPluginRegistry;
1247};1527};
packages/platform/sdk/src/runtime/catalog/source/reconcile.ts+26−1
@@ -3,6 +3,8 @@ import type { · 2 unmodified lines
3 Source,3 Source,
4} from "#schema";4} from "#schema";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
6 8
7import {9import {
8 RuntimeLocalScopeService,10 RuntimeLocalScopeService,
@@ -17,10 +19,33 @@ import { · 8 unmodified lines
17 RuntimeSourceCatalogSyncService,19 RuntimeSourceCatalogSyncService,
18} from "./sync";20} from "./sync";
19 21
22const decodeArtifactImportMetadataOption = Schema.decodeUnknownOption(
23 Schema.parseJson(
24 Schema.Struct({
25 sourceConfigHash: Schema.String,
26 }),
27 ),
28);
29
20const shouldReconcileSource = (source: Source): boolean =>30const shouldReconcileSource = (source: Source): boolean =>
21 source.enabled31 source.enabled
22 && source.status === "connected";32 && source.status === "connected";
23 33
34const artifactNeedsRefresh = (artifact: {
35 revision: {
36 importMetadataJson: string | null;
37 };
38} | null): boolean => {
39 if (artifact === null) {
40 return true;
41 }
42
43 const decoded = decodeArtifactImportMetadataOption(
44 artifact.revision.importMetadataJson,
45 );
46 return Option.isSome(decoded) && decoded.value.sourceConfigHash === "missing";
47};
48
24export const reconcileMissingSourceCatalogArtifacts = (input: {49export const reconcileMissingSourceCatalogArtifacts = (input: {
25 scopeId: ScopeId;50 scopeId: ScopeId;
26 actorScopeId?: ScopeId | null;51 actorScopeId?: ScopeId | null;
@@ -49,7 +74,7 @@ export const reconcileMissingSourceCatalogArtifacts = (input: { · 22 unmodified lines
49 const artifact = yield* sourceArtifactStore.read({74 const artifact = yield* sourceArtifactStore.read({
50 sourceId: source.id,75 sourceId: source.id,
51 });76 });
52 if (artifact !== null) {77 if (!artifactNeedsRefresh(artifact)) {
53 continue;78 continue;
54 }79 }
55 80
packages/platform/sdk/src/runtime/catalog/source/sync.ts+11−1
@@ -22,6 +22,7 @@ import { · 21 unmodified lines
22} from "../../scope-state";22} from "../../scope-state";
23import {23import {
24 getSourceContributionForSource,24 getSourceContributionForSource,
25 ExecutorPluginRegistryService,
25} from "../../sources/source-plugins";26} from "../../sources/source-plugins";
26import {27import {
27 snapshotFromSourceCatalogSyncResult,28 snapshotFromSourceCatalogSyncResult,
@@ -39,6 +40,7 @@ const shouldIndexSource = (source: Source): boolean => · 11 unmodified lines
39 && source.status === "connected";40 && source.status === "connected";
40 41
41type RuntimeSourceCatalogSyncDeps = {42type RuntimeSourceCatalogSyncDeps = {
43 pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
42 runtimeLocalScope: RuntimeLocalScopeState;44 runtimeLocalScope: RuntimeLocalScopeState;
43 scopeStateStore: ScopeStateStoreShape;45 scopeStateStore: ScopeStateStoreShape;
44 sourceArtifactStore: SourceArtifactStoreShape;46 sourceArtifactStore: SourceArtifactStoreShape;
@@ -46,6 +48,7 @@ type RuntimeSourceCatalogSyncDeps = { · 1 unmodified line
46};48};
47 49
48type SourceCatalogSyncServices =50type SourceCatalogSyncServices =
51 | ExecutorPluginRegistryService
49 | RuntimeLocalScopeService52 | RuntimeLocalScopeService
50 | ScopeStateStore53 | ScopeStateStore
51 | SourceArtifactStore54 | SourceArtifactStore
@@ -112,7 +115,10 @@ const syncSourceCatalogWithDeps = ( · 60 unmodified lines
112 return;115 return;
113 }116 }
114 117
115 const definition = getSourceContributionForSource(input.source);118 const definition = getSourceContributionForSource(
119 deps.pluginRegistry,
120 input.source,
121 );
116 const irModel = yield* definition.syncCatalog({122 const irModel = yield* definition.syncCatalog({
117 source: input.source,123 source: input.source,
118 });124 });
@@ -165,12 +171,14 @@ export const syncSourceCatalog = (input: { · 46 unmodified lines
165}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>171}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
166 Effect.gen(function* () {172 Effect.gen(function* () {
167 const runtimeLocalScope = yield* RuntimeLocalScopeService;173 const runtimeLocalScope = yield* RuntimeLocalScopeService;
174 const pluginRegistry = yield* ExecutorPluginRegistryService;
168 const scopeStateStore = yield* ScopeStateStore;175 const scopeStateStore = yield* ScopeStateStore;
169 const sourceArtifactStore = yield* SourceArtifactStore;176 const sourceArtifactStore = yield* SourceArtifactStore;
170 const sourceTypeDeclarationsRefresher =177 const sourceTypeDeclarationsRefresher =
171 yield* SourceTypeDeclarationsRefresherService;178 yield* SourceTypeDeclarationsRefresherService;
172 return yield* syncSourceCatalogWithDeps(179 return yield* syncSourceCatalogWithDeps(
173 {180 {
181 pluginRegistry,
174 runtimeLocalScope,182 runtimeLocalScope,
175 scopeStateStore,183 scopeStateStore,
176 sourceArtifactStore,184 sourceArtifactStore,
@@ -187,11 +195,13 @@ export const RuntimeSourceCatalogSyncLive = Layer.effect( · 10 unmodified lines
187 RuntimeSourceCatalogSyncService,195 RuntimeSourceCatalogSyncService,
188 Effect.gen(function* () {196 Effect.gen(function* () {
189 const runtimeLocalScope = yield* RuntimeLocalScopeService;197 const runtimeLocalScope = yield* RuntimeLocalScopeService;
198 const pluginRegistry = yield* ExecutorPluginRegistryService;
190 const scopeStateStore = yield* ScopeStateStore;199 const scopeStateStore = yield* ScopeStateStore;
191 const sourceArtifactStore = yield* SourceArtifactStore;200 const sourceArtifactStore = yield* SourceArtifactStore;
192 const sourceTypeDeclarationsRefresher =201 const sourceTypeDeclarationsRefresher =
193 yield* SourceTypeDeclarationsRefresherService;202 yield* SourceTypeDeclarationsRefresherService;
194 const deps: RuntimeSourceCatalogSyncDeps = {203 const deps: RuntimeSourceCatalogSyncDeps = {
204 pluginRegistry,
195 runtimeLocalScope,205 runtimeLocalScope,
196 scopeStateStore,206 scopeStateStore,
197 sourceArtifactStore,207 sourceArtifactStore,
packages/platform/sdk/src/runtime/execution/ir-execution.ts+8−1
@@ -13,6 +13,9 @@ import type { · 12 unmodified lines
13import {13import {
14 getSourceContribution,14 getSourceContribution,
15} from "../sources/source-plugins";15} from "../sources/source-plugins";
16import type {
17 ExecutorSdkPluginRegistry,
18} from "../../plugins";
16import {19import {
17 runtimeEffectError,20 runtimeEffectError,
18} from "../effect-errors";21} from "../effect-errors";
@@ -49,6 +52,7 @@ export const invocationDescriptorFromTool = (input: { · 30 unmodified lines
49});52});
50 53
51export const invokeIrTool = (input: {54export const invokeIrTool = (input: {
55 pluginRegistry: ExecutorSdkPluginRegistry;
52 scopeId: Source["scopeId"];56 scopeId: Source["scopeId"];
53 actorScopeId: ScopeId;57 actorScopeId: ScopeId;
54 tool: LoadedSourceCatalogToolIndexEntry;58 tool: LoadedSourceCatalogToolIndexEntry;
@@ -56,7 +60,10 @@ export const invokeIrTool = (input: { · 1 unmodified line
56 onElicitation?: OnElicitation;60 onElicitation?: OnElicitation;
57 context?: Record<string, unknown>;61 context?: Record<string, unknown>;
58}) => {62}) => {
59 const definition = getSourceContribution(input.tool.executable.pluginKey);63 const definition = getSourceContribution(
64 input.pluginRegistry,
65 input.tool.executable.pluginKey,
66 );
60 if (definition.kind !== input.tool.source.kind) {67 if (definition.kind !== input.tool.source.kind) {
61 return Effect.fail(68 return Effect.fail(
62 runtimeEffectError("execution/ir-execution", 69 runtimeEffectError("execution/ir-execution",
packages/platform/sdk/src/runtime/execution/scope/environment.ts+7−0
@@ -47,6 +47,9 @@ import { · 46 unmodified lines
47import {47import {
48 RuntimeSourceStoreService,48 RuntimeSourceStoreService,
49} from "../../sources/source-store";49} from "../../sources/source-store";
50import {
51 ExecutorPluginRegistryService,
52} from "../../sources/source-plugins";
50import {53import {
51 SecretMaterialDeleterService,54 SecretMaterialDeleterService,
52 SecretMaterialResolverService,55 SecretMaterialResolverService,
@@ -71,6 +74,7 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 18 unmodified lines
71 74
72export const createScopeExecutionEnvironmentResolver =75export const createScopeExecutionEnvironmentResolver =
73 (input: {76 (input: {
77 pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
74 executorStateStore: Effect.Effect.Success<typeof ExecutorStateStore>;78 executorStateStore: Effect.Effect.Success<typeof ExecutorStateStore>;
75 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;79 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
76 sourceCatalogSyncService: Effect.Effect.Success<80 sourceCatalogSyncService: Effect.Effect.Success<
@@ -103,6 +107,7 @@ export const createScopeExecutionEnvironmentResolver = · 26 unmodified lines
103 ? createEmptyLocalToolRuntime()107 ? createEmptyLocalToolRuntime()
104 : yield* input.localToolRuntimeLoader.load();108 : yield* input.localToolRuntimeLoader.load();
105 const { catalog, toolInvoker } = createScopeToolInvoker({109 const { catalog, toolInvoker } = createScopeToolInvoker({
110 pluginRegistry: input.pluginRegistry,
106 scopeId,111 scopeId,
107 actorScopeId,112 actorScopeId,
108 executorStateStore: input.executorStateStore,113 executorStateStore: input.executorStateStore,
@@ -147,6 +152,7 @@ export const RuntimeExecutionResolverLive = ( · 38 unmodified lines
147 : Layer.effect(152 : Layer.effect(
148 RuntimeExecutionResolverService,153 RuntimeExecutionResolverService,
149 Effect.gen(function* () {154 Effect.gen(function* () {
155 const pluginRegistry = yield* ExecutorPluginRegistryService;
150 const executorStateStore = yield* ExecutorStateStore;156 const executorStateStore = yield* ExecutorStateStore;
151 const sourceStore = yield* RuntimeSourceStoreService;157 const sourceStore = yield* RuntimeSourceStoreService;
152 const sourceCatalogSyncService =158 const sourceCatalogSyncService =
@@ -163,6 +169,7 @@ export const RuntimeExecutionResolverLive = ( · 10 unmodified lines
163 const updateSecretMaterial = yield* SecretMaterialUpdaterService;169 const updateSecretMaterial = yield* SecretMaterialUpdaterService;
164 170
165 return createScopeExecutionEnvironmentResolver({171 return createScopeExecutionEnvironmentResolver({
172 pluginRegistry,
166 executorStateStore,173 executorStateStore,
167 sourceStore,174 sourceStore,
168 sourceCatalogSyncService,175 sourceCatalogSyncService,
packages/platform/sdk/src/runtime/execution/scope/tool-invoker.ts+7−0
@@ -45,6 +45,9 @@ import type { · 44 unmodified lines
45import {45import {
46 type RuntimeSourceStore,46 type RuntimeSourceStore,
47} from "../../sources/source-store";47} from "../../sources/source-store";
48import type {
49 ExecutorSdkPluginRegistry,
50} from "../../../plugins";
48import {51import {
49 createExecutorToolMap,52 createExecutorToolMap,
50} from "../../sources/executor-tools";53} from "../../sources/executor-tools";
@@ -73,6 +76,7 @@ import { · 22 unmodified lines
73import * as Layer from "effect/Layer";76import * as Layer from "effect/Layer";
74 77
75export const createScopeToolInvoker = (input: {78export const createScopeToolInvoker = (input: {
79 pluginRegistry: ExecutorSdkPluginRegistry;
76 scopeId: Source["scopeId"];80 scopeId: Source["scopeId"];
77 actorScopeId: ScopeId;81 actorScopeId: ScopeId;
78 executorStateStore: ExecutorStateStoreShape;82 executorStateStore: ExecutorStateStoreShape;
@@ -120,6 +124,7 @@ export const createScopeToolInvoker = (input: { · 41 unmodified lines
120 ) => effect.pipe(Effect.provide(secretMaterialLayer));124 ) => effect.pipe(Effect.provide(secretMaterialLayer));
121 125
122 const executorTools = createExecutorToolMap({126 const executorTools = createExecutorToolMap({
127 pluginRegistry: input.pluginRegistry,
123 scopeId: input.scopeId,128 scopeId: input.scopeId,
124 actorScopeId: input.actorScopeId,129 actorScopeId: input.actorScopeId,
125 executorStateStore: input.executorStateStore,130 executorStateStore: input.executorStateStore,
@@ -130,6 +135,7 @@ export const createScopeToolInvoker = (input: { · 4 unmodified lines
130 scopeStateStore: input.scopeStateStore,135 scopeStateStore: input.scopeStateStore,
131 sourceArtifactStore: input.sourceArtifactStore,136 sourceArtifactStore: input.sourceArtifactStore,
132 runtimeLocalScope: input.runtimeLocalScope,137 runtimeLocalScope: input.runtimeLocalScope,
138 secretMaterialServices: input.secretMaterialServices,
133 });139 });
134 const sourceCatalog = createScopeSourceCatalog({140 const sourceCatalog = createScopeSourceCatalog({
135 scopeId: input.scopeId,141 scopeId: input.scopeId,
@@ -201,6 +207,7 @@ export const createScopeToolInvoker = (input: { · 65 unmodified lines
201 });207 });
202 208
203 return yield* invokeIrTool({209 return yield* invokeIrTool({
210 pluginRegistry: input.pluginRegistry,
204 scopeId: input.scopeId,211 scopeId: input.scopeId,
205 actorScopeId: input.actorScopeId,212 actorScopeId: input.actorScopeId,
206 tool: catalogTool,213 tool: catalogTool,
packages/platform/sdk/src/runtime/index.ts+37−3
@@ -40,6 +40,9 @@ import type { · 39 unmodified lines
40import type {40import type {
41 InstanceConfig,41 InstanceConfig,
42} from "../local/contracts";42} from "../local/contracts";
43import type {
44 ExecutorSdkPluginRegistry,
45} from "../plugins";
43import type {46import type {
44 ExecutorScopeContext,47 ExecutorScopeContext,
45 ExecutorScopeDescriptor,48 ExecutorScopeDescriptor,
@@ -87,6 +90,10 @@ import { · 41 unmodified lines
87import {90import {
88 RuntimeSourceStoreLive,91 RuntimeSourceStoreLive,
89} from "./sources/source-store";92} from "./sources/source-store";
93import {
94 emptyExecutorPluginRegistry,
95 ExecutorPluginRegistryLive,
96} from "./sources/source-plugins";
90 97
91export * from "./execution/state";98export * from "./execution/state";
92export * from "./secret-stores";99export * from "./secret-stores";
@@ -106,6 +113,8 @@ export * from "./source-artifacts"; · 13 unmodified lines
106export * from "./scope-config";113export * from "./scope-config";
107export * from "./scope-state";114export * from "./scope-state";
108export * from "./scope/config-secrets";115export * from "./scope/config-secrets";
116export * from "./sources/source-store/config";
117export * from "./sources/source-store/plugin-local-config";
109export {118export {
110 LocalInstanceConfigService,119 LocalInstanceConfigService,
111 SecretMaterialDeleterService,120 SecretMaterialDeleterService,
@@ -139,7 +148,10 @@ export type { · 27 unmodified lines
139export {148export {
140 registeredSourceContributions,149 registeredSourceContributions,
141 registeredSecretStoreContributions,150 registeredSecretStoreContributions,
151 registeredManagementToolContributions,
142 hasRegisteredExternalSourcePlugins,152 hasRegisteredExternalSourcePlugins,
153 ExecutorPluginRegistryLive,
154 ExecutorPluginRegistryService,
143 getSourceContribution,155 getSourceContribution,
144 getSourceContributionForSource,156 getSourceContributionForSource,
145 getSecretStoreContribution,157 getSecretStoreContribution,
@@ -150,6 +162,7 @@ export type ExecutorRuntimeOptions = { · 4 unmodified lines
150 executionResolver?: ResolveExecutionEnvironment;162 executionResolver?: ResolveExecutionEnvironment;
151 resolveSecretMaterial?: ResolveSecretMaterial;163 resolveSecretMaterial?: ResolveSecretMaterial;
152 getLocalServerBaseUrl?: () => string | undefined;164 getLocalServerBaseUrl?: () => string | undefined;
165 pluginRegistry?: ExecutorSdkPluginRegistry;
153};166};
154 167
155type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;168type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;
@@ -185,7 +198,11 @@ export type BoundScopeStateStore = { · 29 unmodified lines
185};198};
186 199
187export type BoundSourceArtifactStore = {200export type BoundSourceArtifactStore = {
188 build: SourceArtifactStoreShape["build"];201 build: (input: {
202 source: Parameters<SourceArtifactStoreShape["build"]>[0]["source"];
203 syncResult: Parameters<SourceArtifactStoreShape["build"]>[0]["syncResult"];
204 pluginRegistry?: ExecutorSdkPluginRegistry;
205 }) => LocalSourceArtifact;
189 read: (206 read: (
190 sourceId: string,207 sourceId: string,
191 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;208 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;
@@ -296,8 +313,14 @@ const toScopeStateStoreShape = ( · 104 unmodified lines
296 313
297const toSourceArtifactStoreShape = (314const toSourceArtifactStoreShape = (
298 input: BoundSourceArtifactStore,315 input: BoundSourceArtifactStore,
316 pluginRegistry: ExecutorSdkPluginRegistry,
299): SourceArtifactStoreShape => ({317): SourceArtifactStoreShape => ({
300 build: input.build,318 build: ({ source, syncResult }) =>
319 input.build({
320 source,
321 syncResult,
322 pluginRegistry,
323 }),
301 read: ({ sourceId }) => input.read(sourceId),324 read: ({ sourceId }) => input.read(sourceId),
302 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),325 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),
303 remove: ({ sourceId }) => input.remove(sourceId),326 remove: ({ sourceId }) => input.remove(sourceId),
@@ -330,11 +353,16 @@ export const createExecutorRuntimeLayer = ( · 26 unmodified lines
330 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;353 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;
331 },354 },
332) => {355) => {
356 const pluginRegistry =
357 input.pluginRegistry ?? emptyExecutorPluginRegistry();
333 const storageLayer = makeLocalStorageLayer({358 const storageLayer = makeLocalStorageLayer({
334 installationStore: toInstallationStoreShape(input.storage.installation),359 installationStore: toInstallationStoreShape(input.storage.installation),
335 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),360 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),
336 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),361 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),
337 sourceArtifactStore: toSourceArtifactStoreShape(input.storage.sourceArtifacts),362 sourceArtifactStore: toSourceArtifactStoreShape(
363 input.storage.sourceArtifacts,
364 pluginRegistry,
365 ),
338 });366 });
339 const localToolRuntimeLayer = Layer.succeed(367 const localToolRuntimeLayer = Layer.succeed(
340 LocalToolRuntimeLoaderService,368 LocalToolRuntimeLoaderService,
@@ -353,6 +381,7 @@ export const createExecutorRuntimeLayer = ( · 12 unmodified lines
353 381
354 const baseLayer = Layer.mergeAll(382 const baseLayer = Layer.mergeAll(
355 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),383 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),
384 ExecutorPluginRegistryLive(pluginRegistry),
356 RuntimeLocalScopeLive(input.localScopeState),385 RuntimeLocalScopeLive(input.localScopeState),
357 storageLayer,386 storageLayer,
358 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),387 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),
@@ -410,6 +439,7 @@ export type ExecutorRuntime = { · 51 unmodified lines
410 scope: ExecutorScopeContext;439 scope: ExecutorScopeContext;
411 storage: RuntimeStorageServices;440 storage: RuntimeStorageServices;
412 localInstallation: LocalInstallation;441 localInstallation: LocalInstallation;
442 pluginRegistry: ExecutorSdkPluginRegistry;
413 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;443 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;
414 runtimeLayer: ExecutorRuntimeLayer;444 runtimeLayer: ExecutorRuntimeLayer;
415 close: () => Promise<void>;445 close: () => Promise<void>;
@@ -425,6 +455,8 @@ export const createExecutorRuntimeFromServices = (input: { · 9 unmodified lines
425 services: ExecutorRuntimeServices;455 services: ExecutorRuntimeServices;
426} & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>456} & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>
427 (Effect.gen(function* () {457 (Effect.gen(function* () {
458 const pluginRegistry =
459 input.pluginRegistry ?? emptyExecutorPluginRegistry();
428 const localInstallation = yield* input.services.storage.installation460 const localInstallation = yield* input.services.storage.installation
429 .getOrProvision()461 .getOrProvision()
430 .pipe(Effect.mapError(toRuntimeBootstrapError));462 .pipe(Effect.mapError(toRuntimeBootstrapError));
@@ -457,6 +489,7 @@ export const createExecutorRuntimeFromServices = (input: { · 26 unmodified lines
457 ),489 ),
458 sourceArtifactStore: toSourceArtifactStoreShape(490 sourceArtifactStore: toSourceArtifactStoreShape(
459 input.services.storage.sourceArtifacts,491 input.services.storage.sourceArtifacts,
492 pluginRegistry,
460 ),493 ),
461 }),494 }),
462 ),495 ),
@@ -499,6 +532,7 @@ export const createExecutorRuntimeFromServices = (input: { · 36 unmodified lines
499 scope: runtimeWorkspace,532 scope: runtimeWorkspace,
500 storage: input.services.storage,533 storage: input.services.storage,
501 localInstallation,534 localInstallation,
535 pluginRegistry,
502 managedRuntime,536 managedRuntime,
503 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,537 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,
504 close: async () => {538 close: async () => {
packages/platform/sdk/src/runtime/source-artifacts.ts+9−1
@@ -27,6 +27,9 @@ import { · 26 unmodified lines
27 createSourceCatalogRevisionRecord,27 createSourceCatalogRevisionRecord,
28 stableSourceCatalogId,28 stableSourceCatalogId,
29} from "./sources/source-definitions";29} from "./sources/source-definitions";
30import type {
31 ExecutorSdkPluginRegistry,
32} from "../plugins";
30 33
31const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const;34const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const;
32const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const;35const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const;
@@ -200,14 +203,19 @@ export const decodeStoredLocalSourceArtifact = ( · 167 unmodified lines
200export const buildLocalSourceArtifact = (input: {203export const buildLocalSourceArtifact = (input: {
201 source: Source;204 source: Source;
202 syncResult: SourceCatalogSyncResult;205 syncResult: SourceCatalogSyncResult;
206 pluginRegistry?: ExecutorSdkPluginRegistry;
203}): LocalSourceArtifact => {207}): LocalSourceArtifact => {
204 const catalogId: SourceCatalogId = stableSourceCatalogId(input.source);208 const catalogId: SourceCatalogId = stableSourceCatalogId(
209 input.source,
210 input.pluginRegistry,
211 );
205 const generatedAt = Date.now();212 const generatedAt = Date.now();
206 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);213 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);
207 const importHash = importMetadataHash(snapshot);214 const importHash = importMetadataHash(snapshot);
208 const hash = snapshotHash(snapshot);215 const hash = snapshotHash(snapshot);
209 const revision = createSourceCatalogRevisionRecord({216 const revision = createSourceCatalogRevisionRecord({
210 source: input.source,217 source: input.source,
218 pluginRegistry: input.pluginRegistry,
211 catalogId,219 catalogId,
212 revisionNumber: 1,220 revisionNumber: 1,
213 importMetadataJson: JSON.stringify(snapshot.import),221 importMetadataJson: JSON.stringify(snapshot.import),
packages/platform/sdk/src/runtime/sources/executor-tools.test.ts+290−0
@@ -0,0 +1,290 @@
1import {
2 createServer,
3} from "node:http";
4import type {
5 AddressInfo,
6} from "node:net";
7import {
8 FileSystem,
9} from "@effect/platform";
10import {
11 NodeFileSystem,
12} from "@effect/platform-node";
13import {
14 describe,
15 expect,
16 it,
17} from "@effect/vitest";
18import * as Effect from "effect/Effect";
19
20import {
21 createLocalExecutorEffect,
22} from "@executor/platform-sdk-file/effect";
23import {
24 ExecutionIdSchema,
25} from "../../schema";
26import {
27 RuntimeExecutionResolverService,
28} from "../execution/scope/environment";
29import {
30 openApiSdkPlugin,
31} from "../../../../../../plugins/openapi/sdk";
32
33const staticOpenApiDocument = JSON.stringify({
34 openapi: "3.1.0",
35 info: {
36 title: "Demo API",
37 version: "1.0.0",
38 },
39 servers: [
40 {
41 url: "https://api.example.test",
42 },
43 ],
44 paths: {
45 "/widgets": {
46 get: {
47 operationId: "listWidgets",
48 responses: {
49 "200": {
50 description: "ok",
51 },
52 },
53 },
54 },
55 },
56});
57
58const startStaticSpecServer = async () => {
59 const server = createServer((request, response) => {
60 if (request.url !== "/openapi.json") {
61 response.writeHead(404);
62 response.end("Not Found");
63 return;
64 }
65
66 response.writeHead(200, {
67 "content-type": "application/json; charset=utf-8",
68 });
69 response.end(staticOpenApiDocument);
70 });
71
72 await new Promise<void>((resolve, reject) => {
73 server.once("error", reject);
74 server.listen(0, "127.0.0.1", () => {
75 server.off("error", reject);
76 resolve();
77 });
78 });
79
80 const address = server.address();
81 if (!address || typeof address === "string") {
82 server.close();
83 throw new Error("Failed to determine OpenAPI test server address.");
84 }
85
86 return {
87 specUrl: `http://127.0.0.1:${(address as AddressInfo).port}/openapi.json`,
88 close: () =>
89 new Promise<void>((resolve, reject) => {
90 server.close((error) => {
91 if (error) {
92 reject(error);
93 return;
94 }
95
96 resolve();
97 });
98 }),
99 };
100};
101
102const resolveExecutionEnvironment = (executor: {
103 scopeId: string;
104 actorScopeId: string;
105 runtime: {
106 runtimeLayer: any;
107 };
108}) =>
109 Effect.gen(function* () {
110 const resolver = yield* RuntimeExecutionResolverService;
111 return yield* resolver({
112 scopeId: executor.scopeId,
113 actorScopeId: executor.actorScopeId,
114 executionId: ExecutionIdSchema.make("exec_executor_tools_test"),
115 });
116 }).pipe(Effect.provide(executor.runtime.runtimeLayer));
117
118describe("executor internal tools", () => {
119 it.scoped("registers plugin-owned executor.openapi tools and delegates generic remove through them", () =>
120 Effect.gen(function* () {
121 const fs = yield* FileSystem.FileSystem;
122 const workspaceRoot = yield* fs.makeTempDirectory({
123 prefix: "executor-internal-tools-",
124 });
125 const stored = new Map<string, Record<string, unknown>>();
126 const specServer = yield* Effect.acquireRelease(
127 Effect.tryPromise({
128 try: () => startStaticSpecServer(),
129 catch: (cause) =>
130 cause instanceof Error ? cause : new Error(String(cause)),
131 }),
132 (server) =>
133 Effect.promise(() => server.close()).pipe(Effect.orDie),
134 );
135 const executor = yield* Effect.acquireRelease(
136 createLocalExecutorEffect({
137 localDataDir: ":memory:",
138 workspaceRoot,
139 plugins: [
140 openApiSdkPlugin({
141 storage: {
142 get: ({ sourceId }) =>
143 Effect.succeed((stored.get(sourceId) as any) ?? null),
144 put: ({ sourceId, value }) =>
145 Effect.sync(() => {
146 stored.set(sourceId, value as any);
147 }),
148 remove: ({ sourceId }) =>
149 Effect.sync(() => {
150 stored.delete(sourceId);
151 }),
152 },
153 }),
154 ] as const,
155 }),
156 (openedExecutor) =>
157 Effect.promise(() => openedExecutor.close()).pipe(
158 Effect.orDie,
159 Effect.zipRight(
160 fs.remove(workspaceRoot, {
161 recursive: true,
162 force: true,
163 }),
164 ),
165 ),
166 );
167
168 const environment = yield* resolveExecutionEnvironment(executor);
169 expect(environment.catalog).toBeDefined();
170
171 const createTool = yield* environment.catalog!.getToolByPath({
172 path: "executor.openapi.createSource",
173 includeSchemas: false,
174 });
175 const previewTool = yield* environment.catalog!.getToolByPath({
176 path: "executor.openapi.previewDocument",
177 includeSchemas: false,
178 });
179
180 expect(createTool?.path).toBe("executor.openapi.createSource");
181 expect(previewTool?.path).toBe("executor.openapi.previewDocument");
182
183 const preview = yield* environment.toolInvoker.invoke({
184 path: "executor.openapi.previewDocument",
185 args: {
186 specUrl: specServer.specUrl,
187 },
188 });
189 expect(preview).toMatchObject({
190 title: "Demo API",
191 operationCount: 1,
192 });
193
194 const created = yield* environment.toolInvoker.invoke({
195 path: "executor.openapi.createSource",
196 args: {
197 name: "Demo API",
198 specUrl: specServer.specUrl,
199 baseUrl: null,
200 auth: {
201 kind: "none",
202 },
203 },
204 });
205
206 expect((created as { kind: string }).kind).toBe("openapi");
207 expect(stored.has((created as { id: string }).id)).toBe(true);
208
209 const removed = yield* environment.toolInvoker.invoke({
210 path: "executor.sources.remove",
211 args: {
212 sourceId: (created as { id: string }).id,
213 },
214 });
215
216 expect(removed).toEqual({
217 removed: true,
218 });
219 expect(stored.has((created as { id: string }).id)).toBe(false);
220 }).pipe(Effect.provide(NodeFileSystem.layer)),
221 );
222
223 it.scoped("keeps executor.sources.add as a compatibility alias", () =>
224 Effect.gen(function* () {
225 const fs = yield* FileSystem.FileSystem;
226 const workspaceRoot = yield* fs.makeTempDirectory({
227 prefix: "executor-add-alias-",
228 });
229 const specServer = yield* Effect.acquireRelease(
230 Effect.tryPromise({
231 try: () => startStaticSpecServer(),
232 catch: (cause) =>
233 cause instanceof Error ? cause : new Error(String(cause)),
234 }),
235 (server) =>
236 Effect.promise(() => server.close()).pipe(Effect.orDie),
237 );
238 const executor = yield* Effect.acquireRelease(
239 createLocalExecutorEffect({
240 localDataDir: ":memory:",
241 workspaceRoot,
242 plugins: [
243 openApiSdkPlugin({
244 storage: {
245 get: () => Effect.succeed(null),
246 put: () => Effect.void,
247 remove: () => Effect.void,
248 },
249 }),
250 ] as const,
251 }),
252 (openedExecutor) =>
253 Effect.promise(() => openedExecutor.close()).pipe(
254 Effect.orDie,
255 Effect.zipRight(
256 fs.remove(workspaceRoot, {
257 recursive: true,
258 force: true,
259 }),
260 ),
261 ),
262 );
263
264 const environment = yield* resolveExecutionEnvironment(executor);
265 const aliasTool = yield* environment.catalog!.getToolByPath({
266 path: "executor.sources.add",
267 includeSchemas: false,
268 });
269 expect(aliasTool?.path).toBe("executor.sources.add");
270
271 const created = yield* environment.toolInvoker.invoke({
272 path: "executor.sources.add",
273 args: {
274 kind: "openapi",
275 name: "Alias API",
276 specUrl: specServer.specUrl,
277 baseUrl: null,
278 auth: {
279 kind: "none",
280 },
281 },
282 });
283
284 expect(created).toMatchObject({
285 kind: "openapi",
286 name: "Alias API",
287 });
288 }).pipe(Effect.provide(NodeFileSystem.layer)),
289 );
290});
packages/platform/sdk/src/runtime/sources/executor-tools.ts+349−33
@@ -1,6 +1,7 @@
1import { toTool, type ToolMap } from "@executor/codemode-core";1import { toTool, type ToolMap } from "@executor/codemode-core";
2import {2import {
3 type ScopeId,3 type ScopeId,
4 SourceIdSchema,
4 SourceSchema,5 SourceSchema,
5 type Source,6 type Source,
6} from "#schema";7} from "#schema";
@@ -9,12 +10,16 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines
9import * as Option from "effect/Option";10import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
11 12
13import type {
14 ExecutorSdkPluginRegistry,
15} from "../../plugins";
12import {16import {
13 createManagedSourceRecord,
14 getSource,17 getSource,
18 listSources,
15 refreshManagedSourceCatalog,19 refreshManagedSourceCatalog,
16 removeSource,20 removeSource,
17 saveManagedSourceRecord,21 saveManagedSourceRecord,
22 createManagedSourceRecord,
18} from "../../sources/operations";23} from "../../sources/operations";
19import {24import {
20 deriveSchemaJson,25 deriveSchemaJson,
@@ -31,23 +36,48 @@ import { · 10 unmodified lines
31 provideOptionalRuntimeLocalScope,36 provideOptionalRuntimeLocalScope,
32 type RuntimeLocalScopeState,37 type RuntimeLocalScopeState,
33} from "../scope/runtime-context";38} from "../scope/runtime-context";
39import {
40 SecretMaterialDeleterService,
41 SecretMaterialResolverService,
42 SecretMaterialStorerService,
43 SecretMaterialUpdaterService,
44 type DeleteSecretMaterial,
45 type ResolveSecretMaterial,
46 type StoreSecretMaterial,
47 type UpdateSecretMaterial,
48} from "../scope/secret-material-providers";
34import {49import {
35 type InstallationStoreShape,50 type InstallationStoreShape,
36 makeLocalStorageLayer,51 makeLocalStorageLayer,
37 type SourceArtifactStoreShape,
38 type ScopeConfigStoreShape,52 type ScopeConfigStoreShape,
39 type ScopeStateStoreShape,53 type ScopeStateStoreShape,
54 type SourceArtifactStoreShape,
40} from "../scope/storage";55} from "../scope/storage";
56import {
57 registeredManagementToolContributions,
58 registeredSourceContributions,
59} from "./source-plugins";
41import {60import {
42 type RuntimeSourceStore,61 type RuntimeSourceStore,
43 RuntimeSourceStoreService,62 RuntimeSourceStoreService,
44} from "./source-store";63} from "./source-store";
45import {
46 registeredSourceContributions,
47} from "./source-plugins";
48 64
49const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => {65const SourceArraySchema = Schema.Array(SourceSchema);
50 const sources = registeredSourceContributions();66const SourceIdInputSchema = Schema.Struct({
67 sourceId: SourceIdSchema,
68});
69const SourceRemoveResultSchema = Schema.Struct({
70 removed: Schema.Boolean,
71});
72
73type RegisteredSourceContribution =
74 ExecutorSdkPluginRegistry["sources"][number];
75type RegisteredManagementToolContribution =
76 ExecutorSdkPluginRegistry["managementTools"][number];
77
78const createExecutorSourcesAddSchema = (
79 sources: ReadonlyArray<RegisteredSourceContribution>,
80): Schema.Schema<any, any, never> => {
51 if (sources.length === 0) {81 if (sources.length === 0) {
52 return Schema.Unknown;82 return Schema.Unknown;
53 }83 }
@@ -61,27 +91,78 @@ const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => { · 7 unmodified lines
61 Schema.Schema<any, any, never>,91 Schema.Schema<any, any, never>,
62 Schema.Schema<any, any, never>,92 Schema.Schema<any, any, never>,
63 ...Array<Schema.Schema<any, any, never>>,93 ...Array<Schema.Schema<any, any, never>>,
64 ])94 ]),
65 );95 );
66};96};
67 97
68export const getExecutorSourcesAddInputHint = (): string =>98export const getExecutorSourcesAddInputHint = (
69 deriveSchemaTypeSignature(createExecutorSourcesAddSchema(), 340);99 pluginRegistry: ExecutorSdkPluginRegistry,
100): string =>
101 deriveSchemaTypeSignature(
102 createExecutorSourcesAddSchema(
103 registeredSourceContributions(pluginRegistry),
104 ),
105 340,
106 );
70 107
71export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature(108export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature(
72 SourceSchema,109 SourceSchema,
73 260,110 260,
74);111);
75 112
76export const getExecutorSourcesAddInputSchemaJson = (): Record<string, unknown> =>113export const getExecutorSourcesAddInputSchemaJson = (
77 deriveSchemaJson(createExecutorSourcesAddSchema()) ?? {};114 pluginRegistry: ExecutorSdkPluginRegistry,
115): Record<string, unknown> =>
116 deriveSchemaJson(
117 createExecutorSourcesAddSchema(
118 registeredSourceContributions(pluginRegistry),
119 ),
120 ) ?? {};
78 121
79export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(122export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(
80 SourceSchema,123 SourceSchema,
81) ?? {};124) ?? {};
82 125
83export const getExecutorSourcesAddHelpLines = (): readonly string[] => {126const coreSourceManagementHelpLines = (): readonly string[] => [
84 const sources = registeredSourceContributions();127 "- executor.sources.list",
128 `- executor.sources.get: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
129 `- executor.sources.refresh: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
130 `- executor.sources.remove: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
131];
132
133export const getExecutorInternalToolHelpLines = (
134 pluginRegistry: ExecutorSdkPluginRegistry,
135): readonly string[] => {
136 const sources = registeredSourceContributions(pluginRegistry);
137 const managementTools = registeredManagementToolContributions(pluginRegistry);
138
139 return [
140 "Core source management tools:",
141 ...coreSourceManagementHelpLines(),
142 ...(managementTools.length === 0
143 ? ["No plugin management tools are registered in this build."]
144 : [
145 "Plugin management tools:",
146 ...managementTools.map((tool) =>
147 `- ${tool.path}: ${deriveSchemaTypeSignature(tool.inputSchema, 260)}`
148 ),
149 ]),
150 ...(sources.length === 0
151 ? []
152 : [
153 "Legacy compatibility tool:",
154 `- executor.sources.add: ${deriveSchemaTypeSignature(
155 createExecutorSourcesAddSchema(sources),
156 340,
157 )}`,
158 ]),
159 ];
160};
161
162export const getExecutorSourcesAddHelpLines = (
163 pluginRegistry: ExecutorSdkPluginRegistry,
164): readonly string[] => {
165 const sources = registeredSourceContributions(pluginRegistry);
85 if (sources.length === 0) {166 if (sources.length === 0) {
86 return ["No source plugins are registered in this build."] as const;167 return ["No source plugins are registered in this build."] as const;
87 }168 }
@@ -98,15 +179,18 @@ export const getExecutorSourcesAddHelpLines = (): readonly string[] => { · 10 unmodified lines
98 ];179 ];
99};180};
100 181
101export const buildExecutorSourcesAddDescription = (): string => {182export const buildExecutorSourcesAddDescription = (
102 const sources = registeredSourceContributions();183 pluginRegistry: ExecutorSdkPluginRegistry,
184): string => {
185 const sources = registeredSourceContributions(pluginRegistry);
103 if (sources.length === 0) {186 if (sources.length === 0) {
104 return "No source plugins are registered in this build.";187 return "No source plugins are registered in this build.";
105 }188 }
106 189
107 return [190 return [
108 "Add a source using one of the registered source plugins.",191 "Legacy compatibility alias for creating a source.",
109 ...getExecutorSourcesAddHelpLines(),192 "Prefer plugin-specific tools such as executor.openapi.createSource or executor.mcp.createSource.",
193 ...getExecutorSourcesAddHelpLines(pluginRegistry),
110 ].join("\n");194 ].join("\n");
111};195};
112 196
@@ -153,7 +237,7 @@ const createSourceConnectorHost = (input: { · 40 unmodified lines
153 },237 },
154});238});
155 239
156const runExecutorSourceEffect = async <A>(240const runExecutorToolEffect = async <A>(
157 effect: Effect.Effect<A, unknown, any>,241 effect: Effect.Effect<A, unknown, any>,
158 input: {242 input: {
159 executorStateStore: ExecutorStateStoreShape;243 executorStateStore: ExecutorStateStoreShape;
@@ -166,6 +250,12 @@ const runExecutorSourceEffect = async <A>( · 6 unmodified lines
166 scopeStateStore: ScopeStateStoreShape;250 scopeStateStore: ScopeStateStoreShape;
167 sourceArtifactStore: SourceArtifactStoreShape;251 sourceArtifactStore: SourceArtifactStoreShape;
168 runtimeLocalScope: RuntimeLocalScopeState | null;252 runtimeLocalScope: RuntimeLocalScopeState | null;
253 secretMaterialServices: {
254 resolve: ResolveSecretMaterial;
255 store: StoreSecretMaterial;
256 delete: DeleteSecretMaterial;
257 update: UpdateSecretMaterial;
258 };
169 },259 },
170): Promise<A> => {260): Promise<A> => {
171 const servicesLayer = Layer.mergeAll(261 const servicesLayer = Layer.mergeAll(
@@ -181,6 +271,22 @@ const runExecutorSourceEffect = async <A>( · 9 unmodified lines
181 RuntimeSourceCatalogSyncService,271 RuntimeSourceCatalogSyncService,
182 input.sourceCatalogSyncService,272 input.sourceCatalogSyncService,
183 ),273 ),
274 Layer.succeed(
275 SecretMaterialResolverService,
276 input.secretMaterialServices.resolve,
277 ),
278 Layer.succeed(
279 SecretMaterialStorerService,
280 input.secretMaterialServices.store,
281 ),
282 Layer.succeed(
283 SecretMaterialDeleterService,
284 input.secretMaterialServices.delete,
285 ),
286 Layer.succeed(
287 SecretMaterialUpdaterService,
288 input.secretMaterialServices.update,
289 ),
184 );290 );
185 291
186 return Effect.runPromise(292 return Effect.runPromise(
@@ -192,11 +298,11 @@ const runExecutorSourceEffect = async <A>( · 5 unmodified lines
192};298};
193 299
194const resolveSourceContribution = (300const resolveSourceContribution = (
195 sources: ReturnType<typeof registeredSourceContributions>,301 sources: ReadonlyArray<RegisteredSourceContribution>,
196 args: unknown,302 args: unknown,
197):303):
198 | {304 | {
199 source: ReturnType<typeof registeredSourceContributions>[number];305 source: RegisteredSourceContribution;
200 parsedArgs: unknown;306 parsedArgs: unknown;
201 }307 }
202 | null => {308 | null => {
@@ -216,7 +322,52 @@ const resolveSourceContribution = ( · 13 unmodified lines
216const toSerializableValue = <A>(value: A): A =>322const toSerializableValue = <A>(value: A): A =>
217 JSON.parse(JSON.stringify(value)) as A;323 JSON.parse(JSON.stringify(value)) as A;
218 324
325const pluginRemoveToolPath = (
326 source: RegisteredSourceContribution,
327): `executor.${string}` =>
328 `executor.${source.pluginKey}.removeSource`;
329
330const pluginRefreshToolPath = (
331 source: RegisteredSourceContribution,
332): `executor.${string}` =>
333 `executor.${source.pluginKey}.refreshSource`;
334
335const findManagementTool = (
336 managementTools: ReadonlyArray<RegisteredManagementToolContribution>,
337 path: string,
338): RegisteredManagementToolContribution | null =>
339 managementTools.find((tool) => tool.path === path) ?? null;
340
341const createPluginManagementToolMap = (input: {
342 managementTools: ReadonlyArray<RegisteredManagementToolContribution>;
343 host: ReturnType<typeof createSourceConnectorHost>;
344 runtime: Parameters<typeof runExecutorToolEffect>[1];
345}): ToolMap =>
346 Object.fromEntries(
347 input.managementTools.map((tool) => [
348 tool.path,
349 toTool({
350 tool: {
351 description: tool.description,
352 inputSchema: Schema.standardSchemaV1(tool.inputSchema),
353 outputSchema: Schema.standardSchemaV1(tool.outputSchema),
354 execute: async (args: unknown) =>
355 toSerializableValue(
356 await runExecutorToolEffect(
357 tool.execute({
358 args: args as never,
359 host: input.host,
360 }),
361 input.runtime,
362 ),
363 ),
364 },
365 }),
366 ]),
367 );
368
219export const createExecutorToolMap = (input: {369export const createExecutorToolMap = (input: {
370 pluginRegistry: ExecutorSdkPluginRegistry;
220 scopeId: ScopeId;371 scopeId: ScopeId;
221 actorScopeId: ScopeId;372 actorScopeId: ScopeId;
222 executorStateStore: ExecutorStateStoreShape;373 executorStateStore: ExecutorStateStoreShape;
@@ -229,22 +380,182 @@ export const createExecutorToolMap = (input: { · 6 unmodified lines
229 scopeStateStore: ScopeStateStoreShape;380 scopeStateStore: ScopeStateStoreShape;
230 sourceArtifactStore: SourceArtifactStoreShape;381 sourceArtifactStore: SourceArtifactStoreShape;
231 runtimeLocalScope: RuntimeLocalScopeState | null;382 runtimeLocalScope: RuntimeLocalScopeState | null;
383 secretMaterialServices: {
384 resolve: ResolveSecretMaterial;
385 store: StoreSecretMaterial;
386 delete: DeleteSecretMaterial;
387 update: UpdateSecretMaterial;
388 };
232}): ToolMap => {389}): ToolMap => {
233 const sources = registeredSourceContributions();390 const sources = registeredSourceContributions(input.pluginRegistry);
234 if (sources.length === 0) {391 const managementTools = registeredManagementToolContributions(
235 return {};392 input.pluginRegistry,
236 }393 );
237
238 const host = createSourceConnectorHost({394 const host = createSourceConnectorHost({
239 scopeId: input.scopeId,395 scopeId: input.scopeId,
240 actorScopeId: input.actorScopeId,396 actorScopeId: input.actorScopeId,
241 });397 });
242 398
243 return {399 const runtime = {
244 "executor.sources.add": toTool({400 executorStateStore: input.executorStateStore,
401 sourceStore: input.sourceStore,
402 sourceCatalogSyncService: input.sourceCatalogSyncService,
403 installationStore: input.installationStore,
404 scopeConfigStore: input.scopeConfigStore,
405 scopeStateStore: input.scopeStateStore,
406 sourceArtifactStore: input.sourceArtifactStore,
407 runtimeLocalScope: input.runtimeLocalScope,
408 secretMaterialServices: input.secretMaterialServices,
409 };
410
411 const pluginToolMap = createPluginManagementToolMap({
412 managementTools,
413 host,
414 runtime,
415 });
416
417 const coreTools: ToolMap = {
418 "executor.sources.list": toTool({
419 tool: {
420 description: "List sources in the current executor scope.",
421 inputSchema: Schema.standardSchemaV1(Schema.Struct({})),
422 outputSchema: Schema.standardSchemaV1(SourceArraySchema),
423 execute: async () =>
424 toSerializableValue(
425 await runExecutorToolEffect(
426 listSources({
427 scopeId: input.scopeId,
428 actorScopeId: input.actorScopeId,
429 }),
430 runtime,
431 ),
432 ),
433 },
434 }),
435 "executor.sources.get": toTool({
436 tool: {
437 description: "Get a source by id.",
438 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
439 outputSchema: Schema.standardSchemaV1(SourceSchema),
440 execute: async (args: typeof SourceIdInputSchema.Type) =>
441 toSerializableValue(
442 await runExecutorToolEffect(
443 getSource({
444 scopeId: input.scopeId,
445 sourceId: args.sourceId,
446 actorScopeId: input.actorScopeId,
447 }),
448 runtime,
449 ),
450 ),
451 },
452 }),
453 "executor.sources.refresh": toTool({
245 tool: {454 tool: {
246 description: buildExecutorSourcesAddDescription(),455 description: "Refresh catalog artifacts for a source by id.",
247 inputSchema: Schema.standardSchemaV1(createExecutorSourcesAddSchema()),456 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
457 outputSchema: Schema.standardSchemaV1(SourceSchema),
458 execute: async (args: typeof SourceIdInputSchema.Type) => {
459 const source = await runExecutorToolEffect(
460 getSource({
461 scopeId: input.scopeId,
462 sourceId: args.sourceId,
463 actorScopeId: input.actorScopeId,
464 }),
465 runtime,
466 );
467 const contribution = sources.find(
468 (candidate) => candidate.kind === source.kind,
469 );
470 const pluginRefreshTool = contribution
471 ? findManagementTool(
472 managementTools,
473 pluginRefreshToolPath(contribution),
474 )
475 : undefined;
476 if (pluginRefreshTool) {
477 return toSerializableValue(
478 await runExecutorToolEffect(
479 pluginRefreshTool.execute({
480 args,
481 host,
482 }),
483 runtime,
484 ),
485 );
486 }
487
488 return toSerializableValue(
489 await runExecutorToolEffect(
490 refreshManagedSourceCatalog({
491 scopeId: input.scopeId,
492 sourceId: args.sourceId,
493 actorScopeId: input.actorScopeId,
494 }),
495 runtime,
496 ),
497 );
498 },
499 },
500 }),
501 "executor.sources.remove": toTool({
502 tool: {
503 description:
504 "Remove a source by id. Delegates to plugin-owned cleanup when available.",
505 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
506 outputSchema: Schema.standardSchemaV1(SourceRemoveResultSchema),
507 execute: async (args: typeof SourceIdInputSchema.Type) => {
508 const source = await runExecutorToolEffect(
509 getSource({
510 scopeId: input.scopeId,
511 sourceId: args.sourceId,
512 actorScopeId: input.actorScopeId,
513 }),
514 runtime,
515 );
516 const contribution = sources.find(
517 (candidate) => candidate.kind === source.kind,
518 );
519 const pluginRemoveTool = contribution
520 ? findManagementTool(
521 managementTools,
522 pluginRemoveToolPath(contribution),
523 )
524 : undefined;
525 if (pluginRemoveTool) {
526 const removed = await runExecutorToolEffect(
527 pluginRemoveTool.execute({
528 args,
529 host,
530 }),
531 runtime,
532 );
533 return toSerializableValue({
534 removed: Boolean(removed),
535 });
536 }
537
538 return toSerializableValue(
539 await runExecutorToolEffect(
540 removeSource({
541 scopeId: input.scopeId,
542 sourceId: args.sourceId,
543 }),
544 runtime,
545 ),
546 );
547 },
548 },
549 }),
550 };
551
552 if (sources.length > 0) {
553 coreTools["executor.sources.add"] = toTool({
554 tool: {
555 description: buildExecutorSourcesAddDescription(input.pluginRegistry),
556 inputSchema: Schema.standardSchemaV1(
557 createExecutorSourcesAddSchema(sources),
558 ),
248 outputSchema: Schema.standardSchemaV1(SourceSchema),559 outputSchema: Schema.standardSchemaV1(SourceSchema),
249 execute: async (args: unknown): Promise<Source> => {560 execute: async (args: unknown): Promise<Source> => {
250 const matched = resolveSourceContribution(sources, args);561 const matched = resolveSourceContribution(sources, args);
@@ -254,17 +565,22 @@ export const createExecutorToolMap = (input: { · 3 unmodified lines
254 );565 );
255 }566 }
256 567
257 const createdSource = await runExecutorSourceEffect(568 const createdSource = await runExecutorToolEffect(
258 matched.source.createSource({569 matched.source.createSource({
259 args: matched.parsedArgs,570 args: matched.parsedArgs,
260 host,571 host,
261 }),572 }),
262 input,573 runtime,
263 );574 );
264 575
265 return toSerializableValue(createdSource);576 return toSerializableValue(createdSource);
266 },577 },
267 },578 },
268 }),579 });
580 }
581
582 return {
583 ...coreTools,
584 ...pluginToolMap,
269 };585 };
270};586};
packages/platform/sdk/src/runtime/sources/source-definitions.ts+52−16
@@ -16,7 +16,9 @@ import { · 15 unmodified lines
16} from "#schema";16} from "#schema";
17import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
18 18
19import { getSourceContributionForSource } from "./source-plugins";19import type {
20 ExecutorSdkPluginRegistry,
21} from "../../plugins";
20 22
21const trimOrNull = (value: string | null | undefined): string | null => {23const trimOrNull = (value: string | null | undefined): string | null => {
22 if (value === null || value === undefined) {24 if (value === null || value === undefined) {
@@ -29,29 +31,56 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 6 unmodified lines
29 31
30type SourceCatalogSourceConfig = Record<string, unknown>;32type SourceCatalogSourceConfig = Record<string, unknown>;
31 33
32const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig =>34const getSourceContributionOption = (
33 getSourceContributionForSource(source).catalogIdentity?.({35 source: Source,
36 pluginRegistry?: ExecutorSdkPluginRegistry,
37) => {
38 if (!pluginRegistry) {
39 return null;
40 }
41
42 try {
43 return pluginRegistry.getSourceContributionForSource(source);
44 } catch {
45 return null;
46 }
47};
48
49const sourceConfigFromSource = (
50 source: Source,
51 pluginRegistry?: ExecutorSdkPluginRegistry,
52): SourceCatalogSourceConfig =>
53 getSourceContributionOption(source, pluginRegistry)?.catalogIdentity?.({
34 source,54 source,
35 }) ?? {55 }) ?? {
36 kind: source.kind,56 kind: source.kind,
37 namespace: source.namespace,57 namespace: source.namespace,
38 };58 };
39 59
40const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind =>60const sourceCatalogKindFromSource = (
41 getSourceContributionForSource(source).catalogKind;61 source: Source,
62 pluginRegistry?: ExecutorSdkPluginRegistry,
63): SourceCatalogKind =>
64 getSourceContributionOption(source, pluginRegistry)?.catalogKind ?? "imported";
42 65
43const sourceCatalogPluginKeyFromSource = (source: Source): SourceCatalogPluginKey =>66const sourceCatalogPluginKeyFromSource = (
44 getSourceContributionForSource(source).kind;67 source: Source,
68 pluginRegistry?: ExecutorSdkPluginRegistry,
69): SourceCatalogPluginKey =>
70 getSourceContributionOption(source, pluginRegistry)?.kind ?? source.kind;
45 71
46const stableHash = (value: string): string =>72const stableHash = (value: string): string =>
47 sha256Hex(value).slice(0, 24);73 sha256Hex(value).slice(0, 24);
48 74
49const sourceCatalogSignature = (source: Source): string =>75const sourceCatalogSignature = (
76 source: Source,
77 pluginRegistry?: ExecutorSdkPluginRegistry,
78): string =>
50 JSON.stringify({79 JSON.stringify({
51 catalogKind: sourceCatalogKindFromSource(source),80 catalogKind: sourceCatalogKindFromSource(source, pluginRegistry),
52 pluginKey: sourceCatalogPluginKeyFromSource(source),81 pluginKey: sourceCatalogPluginKeyFromSource(source, pluginRegistry),
53 sourceId: source.id,82 sourceId: source.id,
54 sourceConfig: sourceConfigFromSource(source),83 sourceConfig: sourceConfigFromSource(source, pluginRegistry),
55 });84 });
56 85
57export const sourceConfigSignature = (source: Source): string =>86export const sourceConfigSignature = (source: Source): string =>
@@ -64,8 +93,13 @@ export const sourceConfigSignature = (source: Source): string => · 6 unmodified lines
64 updatedAt: source.updatedAt,93 updatedAt: source.updatedAt,
65 });94 });
66 95
67export const stableSourceCatalogId = (source: Source): SourceCatalogId =>96export const stableSourceCatalogId = (
68 SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`);97 source: Source,
98 pluginRegistry?: ExecutorSdkPluginRegistry,
99): SourceCatalogId =>
100 SourceCatalogIdSchema.make(
101 `src_catalog_${stableHash(sourceCatalogSignature(source, pluginRegistry))}`,
102 );
69 103
70export const stableSourceCatalogRevisionId = (104export const stableSourceCatalogRevisionId = (
71 source: Source,105 source: Source,
@@ -109,12 +143,13 @@ export const normalizeSourceForSave = (input: { · 37 unmodified lines
109 143
110export const createSourceCatalogRecord = (input: {144export const createSourceCatalogRecord = (input: {
111 source: Source;145 source: Source;
146 pluginRegistry?: ExecutorSdkPluginRegistry;
112 catalogId?: SourceCatalogId | null;147 catalogId?: SourceCatalogId | null;
113 latestRevisionId: SourceCatalogRevisionId;148 latestRevisionId: SourceCatalogRevisionId;
114}): StoredSourceCatalogRecord => ({149}): StoredSourceCatalogRecord => ({
115 id: input.catalogId ?? stableSourceCatalogId(input.source),150 id: input.catalogId ?? stableSourceCatalogId(input.source, input.pluginRegistry),
116 kind: sourceCatalogKindFromSource(input.source),151 kind: sourceCatalogKindFromSource(input.source, input.pluginRegistry),
117 pluginKey: sourceCatalogPluginKeyFromSource(input.source),152 pluginKey: sourceCatalogPluginKeyFromSource(input.source, input.pluginRegistry),
118 name: input.source.name,153 name: input.source.name,
119 summary: null,154 summary: null,
120 visibility: "scope",155 visibility: "scope",
@@ -125,6 +160,7 @@ export const createSourceCatalogRecord = (input: { · 4 unmodified lines
125 160
126export const createSourceCatalogRevisionRecord = (input: {161export const createSourceCatalogRevisionRecord = (input: {
127 source: Source;162 source: Source;
163 pluginRegistry?: ExecutorSdkPluginRegistry;
128 catalogId: SourceCatalogId;164 catalogId: SourceCatalogId;
129 catalogRevisionId?: SourceCatalogRevisionId | null;165 catalogRevisionId?: SourceCatalogRevisionId | null;
130 revisionNumber: number;166 revisionNumber: number;
packages/platform/sdk/src/runtime/sources/source-plugins/index.ts+45−21
@@ -1,35 +1,59 @@
1import * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
3
1import {4import {
2 registerExecutorSdkPlugins,5 registerExecutorSdkPlugins,
3 type ExecutorSdkPlugin,6 type ExecutorSdkPlugin,
7 type ExecutorSdkPluginRegistry,
4} from "../../../plugins";8} from "../../../plugins";
5 9
6let configuredSourcePlugins: readonly ExecutorSdkPlugin<any, any>[] = [];10export class ExecutorPluginRegistryService extends Context.Tag(
7let registry = registerExecutorSdkPlugins(configuredSourcePlugins);11 "#runtime/ExecutorPluginRegistryService",
12)<ExecutorPluginRegistryService, ExecutorSdkPluginRegistry>() {}
13
14export const createExecutorPluginRegistry = (
15 plugins: readonly ExecutorSdkPlugin<any, any>[] = [],
16): ExecutorSdkPluginRegistry => registerExecutorSdkPlugins(plugins);
17
18export const emptyExecutorPluginRegistry = (): ExecutorSdkPluginRegistry =>
19 createExecutorPluginRegistry();
20
21export const ExecutorPluginRegistryLive = (
22 registry: ExecutorSdkPluginRegistry,
23) => Layer.succeed(ExecutorPluginRegistryService, registry);
8 24
9const refreshRegistry = () => {25export const registeredSourceContributions = (
10 registry = registerExecutorSdkPlugins(configuredSourcePlugins);26 registry: ExecutorSdkPluginRegistry,
11};27) => registry.sources;
12 28
13export const configureExecutorSourcePlugins = (29export const registeredSecretStoreContributions = (
14 plugins: readonly ExecutorSdkPlugin<any, any>[],30 registry: ExecutorSdkPluginRegistry,
15): void => {31) => registry.secretStores;
16 configuredSourcePlugins = plugins;
17 refreshRegistry();
18};
19 32
20export const registeredSourceContributions = () => registry.sources;33export const registeredManagementToolContributions = (
21export const registeredSecretStoreContributions = () => registry.secretStores;34 registry: ExecutorSdkPluginRegistry,
35) => registry.managementTools;
36
37export const getSourceContribution = (
38 registry: ExecutorSdkPluginRegistry,
39 kind: string,
40) => registry.getSourceContribution(kind);
22 41
23export const getSourceContribution = (kind: string) =>
24 registry.getSourceContribution(kind);
25export const getSourceContributionForSource = (42export const getSourceContributionForSource = (
26 source: Parameters<typeof registry.getSourceContributionForSource>[0],43 registry: ExecutorSdkPluginRegistry,
44 source: Parameters<ExecutorSdkPluginRegistry["getSourceContributionForSource"]>[0],
27) => registry.getSourceContributionForSource(source);45) => registry.getSourceContributionForSource(source);
28export const getSecretStoreContribution = (kind: string) =>46
29 registry.getSecretStoreContribution(kind);47export const getSecretStoreContribution = (
48 registry: ExecutorSdkPluginRegistry,
49 kind: string,
50) => registry.getSecretStoreContribution(kind);
51
30export const getSecretStoreContributionForStore = (52export const getSecretStoreContributionForStore = (
31 store: Parameters<typeof registry.getSecretStoreContributionForStore>[0],53 registry: ExecutorSdkPluginRegistry,
54 store: Parameters<ExecutorSdkPluginRegistry["getSecretStoreContributionForStore"]>[0],
32) => registry.getSecretStoreContributionForStore(store);55) => registry.getSecretStoreContributionForStore(store);
33 56
34export const hasRegisteredExternalSourcePlugins = () =>57export const hasRegisteredExternalSourcePlugins = (
35 configuredSourcePlugins.length > 0;58 registry: ExecutorSdkPluginRegistry,
59) => registry.plugins.length > 0;
packages/platform/sdk/src/runtime/sources/source-store/config.ts+29−5
@@ -38,17 +38,41 @@ export const deriveLocalSourceId = ( · 37 unmodified lines
38 return SourceIdSchema.make(candidate);38 return SourceIdSchema.make(candidate);
39};39};
40 40
41export const configSourceBaseFromLocalSource = (input: {
42 source: Source;
43}): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({
44 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
45 ? { name: input.source.name }
46 : {}),
47 ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)
48 ? { namespace: input.source.namespace ?? undefined }
49 : {}),
50 ...(input.source.enabled === false ? { enabled: false } : {}),
51});
52
41export const configSourceFromLocalSource = (input: {53export const configSourceFromLocalSource = (input: {
42 source: Source;54 source: Source;
55 existingConfig?: LocalConfigSource | null;
43}): LocalConfigSource => {56}): LocalConfigSource => {
44 return {57 return {
58 ...configSourceBaseFromLocalSource({
59 source: input.source,
60 }),
45 kind: input.source.kind as LocalConfigSource["kind"],61 kind: input.source.kind as LocalConfigSource["kind"],
46 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)62 ...(input.existingConfig?.config !== undefined
47 ? { name: input.source.name }63 ? {
64 config: cloneJson(input.existingConfig.config),
65 }
66 : {}),
67 ...(input.existingConfig?.connection !== undefined
68 ? {
69 connection: cloneJson(input.existingConfig.connection),
70 }
48 : {}),71 : {}),
49 ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)72 ...(input.existingConfig?.binding !== undefined
50 ? { namespace: input.source.namespace ?? undefined }73 ? {
74 binding: cloneJson(input.existingConfig.binding),
75 }
51 : {}),76 : {}),
52 ...(input.source.enabled === false ? { enabled: false } : {}),
53 } as LocalConfigSource;77 } as LocalConfigSource;
54};78};
packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts+1−0
@@ -95,6 +95,7 @@ export const persistSourceWithDeps = ( · 94 unmodified lines
95 };95 };
96 sources[nextSource.id] = configSourceFromLocalSource({96 sources[nextSource.id] = configSourceFromLocalSource({
97 source: nextSource,97 source: nextSource,
98 existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,
98 });99 });
99 yield* localScope.scopeConfigStore.writeProject({100 yield* localScope.scopeConfigStore.writeProject({
100 config: {101 config: {
packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts+123−0
@@ -0,0 +1,123 @@
1import type {
2 LocalConfigSecretInput,
3 LocalConfigSource,
4 LocalExecutorConfig,
5 SecretRef,
6 Source,
7} from "#schema";
8import {
9 LocalConfigSecretInputSchema,
10} from "#schema";
11import * as Option from "effect/Option";
12import * as Schema from "effect/Schema";
13
14import {
15 toConfigSecretProviderId,
16} from "../../scope/config-secrets";
17import {
18 cloneJson,
19 configSourceBaseFromLocalSource,
20 trimOrNull,
21} from "./config";
22
23export const createPluginLocalConfigEntrySchema = <
24 TKind extends string,
25 TConfig,
26>(
27 input: {
28 kind: TKind;
29 config: Schema.Schema<TConfig, any, never>;
30 },
31) =>
32 Schema.Struct({
33 kind: Schema.Literal(input.kind),
34 name: Schema.optional(Schema.String),
35 namespace: Schema.optional(Schema.String),
36 enabled: Schema.optional(Schema.Boolean),
37 config: input.config,
38 connection: Schema.optional(Schema.Unknown),
39 binding: Schema.optional(Schema.Unknown),
40 });
41
42export const pluginLocalConfigSourceFromConfig = <TConfig>(input: {
43 source: Source;
44 config: TConfig;
45}): LocalConfigSource => ({
46 ...configSourceBaseFromLocalSource({
47 source: input.source,
48 }),
49 kind: input.source.kind as LocalConfigSource["kind"],
50 config: cloneJson(input.config),
51});
52
53const decodeLocalConfigSecretInputOption = Schema.decodeUnknownOption(
54 LocalConfigSecretInputSchema,
55);
56
57const resolveDefaultConfigSecretProviderAlias = (
58 config: LocalExecutorConfig | null | undefined,
59): string | null => {
60 const defaultAlias = trimOrNull(config?.secrets?.defaults?.env);
61 if (defaultAlias !== null && config?.secrets?.providers?.[defaultAlias]) {
62 return defaultAlias;
63 }
64
65 return config?.secrets?.providers?.default ? "default" : null;
66};
67
68export const secretRefFromLocalConfigSecretInput = (input: {
69 auth: LocalConfigSecretInput;
70 loadedConfig: LocalExecutorConfig | null;
71}): SecretRef => {
72 if (typeof input.auth === "string") {
73 const providerAlias = resolveDefaultConfigSecretProviderAlias(
74 input.loadedConfig,
75 );
76
77 return {
78 providerId: providerAlias ? toConfigSecretProviderId(providerAlias) : "env",
79 handle: input.auth,
80 };
81 }
82
83 return {
84 providerId:
85 input.auth.provider === "params"
86 ? "params"
87 : input.auth.provider
88 ? toConfigSecretProviderId(input.auth.provider)
89 : input.auth.source === "params"
90 ? "params"
91 : "env",
92 handle: input.auth.id,
93 };
94};
95
96export const decodeCurrentOrLegacyLocalConfigAuth = <TAuth>(input: {
97 auth: unknown;
98 authSchema: Schema.Schema<TAuth, any, never>;
99 loadedConfig: LocalExecutorConfig | null;
100 onLegacySecretRef: (ref: SecretRef) => TAuth;
101 fallback: TAuth;
102}): TAuth => {
103 if (input.auth === undefined) {
104 return input.fallback;
105 }
106
107 const current = Schema.decodeUnknownOption(input.authSchema)(input.auth);
108 if (Option.isSome(current)) {
109 return current.value;
110 }
111
112 const legacy = decodeLocalConfigSecretInputOption(input.auth);
113 if (Option.isSome(legacy)) {
114 return input.onLegacySecretRef(
115 secretRefFromLocalConfigSecretInput({
116 auth: legacy.value,
117 loadedConfig: input.loadedConfig,
118 }),
119 );
120 }
121
122 throw new Error("Unsupported local source auth configuration.");
123};
packages/platform/sdk/src/schema/models/local-config.ts+8−0
@@ -56,10 +56,18 @@ export const LocalConfigSecretInputSchema = Schema.Union( · 55 unmodified lines
56 LocalConfigExplicitSecretRefSchema,56 LocalConfigExplicitSecretRefSchema,
57);57);
58 58
59export const LocalConfigSourceConnectionSchema = Schema.Struct({
60 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
61 auth: Schema.optional(Schema.Unknown),
62});
63
59const LocalConfigSourceEntryBaseSchema = Schema.Struct({64const LocalConfigSourceEntryBaseSchema = Schema.Struct({
60 name: Schema.optional(Schema.String),65 name: Schema.optional(Schema.String),
61 namespace: Schema.optional(Schema.String),66 namespace: Schema.optional(Schema.String),
62 enabled: Schema.optional(Schema.Boolean),67 enabled: Schema.optional(Schema.Boolean),
68 config: Schema.optional(Schema.Unknown),
69 connection: Schema.optional(LocalConfigSourceConnectionSchema),
70 binding: Schema.optional(Schema.Unknown),
63});71});
64 72
65export const LocalConfigSourceSchema = Schema.extend(73export const LocalConfigSourceSchema = Schema.extend(
packages/platform/server/src/index.test.ts+225−1
@@ -35,7 +35,12 @@ import { · 34 unmodified lines
35 writeLocalSourceArtifact,35 writeLocalSourceArtifact,
36 writeProjectLocalExecutorConfig,36 writeProjectLocalExecutorConfig,
37} from "@executor/platform-sdk-file/effect";37} from "@executor/platform-sdk-file/effect";
38import { SourceIdSchema } from "@executor/platform-sdk/schema";38import {
39 type LocalConfigSource,
40 SourceCatalogIdSchema,
41 SourceCatalogRevisionIdSchema,
42 SourceIdSchema,
43} from "@executor/platform-sdk/schema";
39import { googleDiscoveryHttpPlugin } from "@executor/plugin-google-discovery-http";44import { googleDiscoveryHttpPlugin } from "@executor/plugin-google-discovery-http";
40import { graphqlHttpPlugin } from "@executor/plugin-graphql-http";45import { graphqlHttpPlugin } from "@executor/plugin-graphql-http";
41import { mcpHttpPlugin } from "@executor/plugin-mcp-http";46import { mcpHttpPlugin } from "@executor/plugin-mcp-http";
@@ -56,6 +61,7 @@ import { · 14 unmodified lines
56 contentHash,61 contentHash,
57 createCatalogImportMetadata,62 createCatalogImportMetadata,
58 createSourceCatalogSyncResult,63 createSourceCatalogSyncResult,
64 snapshotFromSourceCatalogSyncResult,
59} from "../../../sources/core/src/index";65} from "../../../sources/core/src/index";
60import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";66import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";
61 67
@@ -258,6 +264,103 @@ const writeConfiguredLocalMcpSource = (input: { · 196 unmodified lines
258 });264 });
259 }).pipe(Effect.provide(NodeFileSystem.layer));265 }).pipe(Effect.provide(NodeFileSystem.layer));
260 266
267const writeLegacyConfiguredWorkspaceSources = (input: {
268 workspaceRoot: string;
269 sources: Record<string, LocalConfigSource>;
270}) =>
271 Effect.gen(function* () {
272 const context = yield* resolveLocalWorkspaceContext({
273 workspaceRoot: input.workspaceRoot,
274 });
275 const installation = yield* getOrProvisionLocalInstallation({ context });
276
277 yield* writeProjectLocalExecutorConfig({
278 context,
279 config: {
280 sources: input.sources,
281 },
282 });
283
284 return {
285 context,
286 installation,
287 };
288 }).pipe(Effect.provide(NodeFileSystem.layer));
289
290const writeMissingSourceCatalogArtifact = (input: {
291 context: Effect.Effect.Success<ReturnType<typeof writeLegacyConfiguredWorkspaceSources>>["context"];
292 installation: Effect.Effect.Success<ReturnType<typeof writeLegacyConfiguredWorkspaceSources>>["installation"];
293 source: {
294 id: string;
295 name: string;
296 kind: "mcp" | "openapi" | "graphql" | "google_discovery";
297 namespace: string;
298 };
299 pluginKey: string;
300 importerVersion: string;
301}) =>
302 Effect.gen(function* () {
303 const now = Date.now();
304 const source = {
305 id: SourceIdSchema.make(input.source.id),
306 scopeId: input.installation.scopeId,
307 name: input.source.name,
308 kind: input.source.kind,
309 status: "connected" as const,
310 enabled: true,
311 namespace: input.source.namespace,
312 createdAt: now,
313 updatedAt: now,
314 };
315 const syncResult = createSourceCatalogSyncResult({
316 fragment: {
317 version: "ir.v1.fragment",
318 },
319 importMetadata: {
320 ...createCatalogImportMetadata({
321 source,
322 pluginKey: input.pluginKey,
323 }),
324 importerVersion: input.importerVersion,
325 sourceConfigHash: "missing",
326 },
327 sourceHash: null,
328 });
329 const snapshot = snapshotFromSourceCatalogSyncResult(syncResult);
330 const catalogId = SourceCatalogIdSchema.make(`test_catalog_${input.source.id}`);
331 const importMetadataJson = JSON.stringify(snapshot.import);
332
333 yield* writeLocalSourceArtifact({
334 context: input.context,
335 sourceId: source.id,
336 artifact: {
337 version: 4,
338 sourceId: source.id,
339 catalogId,
340 generatedAt: now,
341 revision: {
342 id: SourceCatalogRevisionIdSchema.make(`test_catalog_rev_${input.source.id}`),
343 catalogId,
344 revisionNumber: 1,
345 sourceConfigJson: JSON.stringify({
346 sourceId: source.id,
347 kind: source.kind,
348 name: source.name,
349 namespace: source.namespace,
350 enabled: source.enabled,
351 updatedAt: source.updatedAt,
352 }),
353 importMetadataJson,
354 importMetadataHash: contentHash(importMetadataJson),
355 snapshotHash: contentHash(JSON.stringify(snapshot)),
356 createdAt: now,
357 updatedAt: now,
358 },
359 snapshot,
360 },
361 });
362 }).pipe(Effect.provide(NodeFileSystem.layer));
363
261const makeServer = createIsolatedLocalExecutorServer({364const makeServer = createIsolatedLocalExecutorServer({
262 port: 0,365 port: 0,
263 localDataDir: ":memory:",366 localDataDir: ":memory:",
@@ -1715,6 +1818,127 @@ describe("local-executor-server", () => { · 1451 unmodified lines
1715 15_000,1818 15_000,
1716 );1819 );
1717 1820
1821 it.scoped("recovers config-backed MCP and OpenAPI sources from config-only state", () =>
1822 Effect.gen(function* () {
1823 const workspaceRoot = yield* makeTempWorkspaceRoot();
1824 const localDataDir = yield* makeTempWorkspaceRoot();
1825 const demoServer = yield* Effect.acquireRelease(
1826 Effect.promise(() => startMcpElicitationDemoServer()),
1827 (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
1828 );
1829 const openApiServer = yield* Effect.acquireRelease(
1830 Effect.promise(() => startOpenApiDemoServer()),
1831 (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
1832 );
1833
1834 const workspace = yield* writeLegacyConfiguredWorkspaceSources({
1835 workspaceRoot,
1836 sources: {
1837 deepwiki: {
1838 kind: "mcp",
1839 name: "DeepWiki MCP",
1840 connection: {
1841 endpoint: demoServer.endpoint,
1842 },
1843 binding: {
1844 transport: "streamable-http",
1845 queryParams: null,
1846 headers: null,
1847 command: null,
1848 args: null,
1849 env: null,
1850 cwd: null,
1851 },
1852 },
1853 vercel: {
1854 kind: "openapi",
1855 name: "Vercel API",
1856 connection: {
1857 endpoint: openApiServer.baseUrl,
1858 },
1859 binding: {
1860 specUrl: openApiServer.specUrl,
1861 defaultHeaders: null,
1862 },
1863 },
1864 },
1865 });
1866
1867 yield* writeMissingSourceCatalogArtifact({
1868 context: workspace.context,
1869 installation: workspace.installation,
1870 source: {
1871 id: "deepwiki",
1872 name: "DeepWiki MCP",
1873 kind: "mcp",
1874 namespace: "deepwiki",
1875 },
1876 pluginKey: "mcp",
1877 importerVersion: "ir.v1.mcp",
1878 });
1879 yield* writeMissingSourceCatalogArtifact({
1880 context: workspace.context,
1881 installation: workspace.installation,
1882 source: {
1883 id: "vercel",
1884 name: "Vercel API",
1885 kind: "openapi",
1886 namespace: "vercel",
1887 },
1888 pluginKey: "openapi",
1889 importerVersion: "ir.v1.openapi",
1890 });
1891
1892 const server = yield* createLocalExecutorServer({
1893 workspaceRoot,
1894 localDataDir,
1895 port: 0,
1896 });
1897 const client = yield* makeApiClient(server.baseUrl);
1898
1899 const mcpConfig = yield* client.mcp.getSourceConfig({
1900 path: {
1901 workspaceId: workspace.installation.scopeId,
1902 sourceId: "deepwiki" as never,
1903 },
1904 });
1905 expect(mcpConfig.endpoint).toBe(demoServer.endpoint);
1906 expect(mcpConfig.transport).toBe("streamable-http");
1907
1908 const openApiConfig = yield* client.openapi.getSourceConfig({
1909 path: {
1910 workspaceId: workspace.installation.scopeId,
1911 sourceId: "vercel" as never,
1912 },
1913 });
1914 expect(openApiConfig.specUrl).toBe(openApiServer.specUrl);
1915 expect(openApiConfig.baseUrl).toBe(openApiServer.baseUrl);
1916
1917 const mcpInspectionResponse = yield* Effect.promise(() =>
1918 fetch(
1919 `${server.baseUrl}/v1/workspaces/${workspace.installation.scopeId}/sources/deepwiki/inspection`,
1920 ),
1921 );
1922 assertTrue(mcpInspectionResponse.ok);
1923 const mcpInspection = yield* Effect.promise(
1924 () => mcpInspectionResponse.json() as Promise<{ toolCount: number }>,
1925 );
1926 expect(mcpInspection.toolCount).toBeGreaterThan(0);
1927
1928 const openApiInspectionResponse = yield* Effect.promise(() =>
1929 fetch(
1930 `${server.baseUrl}/v1/workspaces/${workspace.installation.scopeId}/sources/vercel/inspection`,
1931 ),
1932 );
1933 assertTrue(openApiInspectionResponse.ok);
1934 const openApiInspection = yield* Effect.promise(
1935 () => openApiInspectionResponse.json() as Promise<{ toolCount: number }>,
1936 );
1937 expect(openApiInspection.toolCount).toBeGreaterThan(0);
1938 }),
1939 20_000,
1940 );
1941
1718 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>1942 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>
1719 Effect.gen(function* () {1943 Effect.gen(function* () {
1720 const localDataDir = yield* makeTempWorkspaceRoot();1944 const localDataDir = yield* makeTempWorkspaceRoot();
personal-notes/otel-tracing-for-source-connect.md+0−50
@@ -1,50 +0,0 @@
1# OTel Tracing For Source Connect
2
3## Setup
4
5- Use the Effect-native OTel layer from `@effect/opentelemetry`, not a hand-rolled tracer bridge.
6- Start Jaeger with `bun run trace:up`.
7- Run the dev server with:
8
9```bash
10EXECUTOR_TRACE_ENABLED=1 \
11EXECUTOR_TRACE_SERVICE_NAME=executor-local \
12EXECUTOR_TRACE_OTLP_ENDPOINT=http://127.0.0.1:4317 \
13bun run dev
14```
15
16- Query traces via:
17
18```bash
19curl "http://127.0.0.1:16686/api/traces?service=executor-local&limit=20"
20```
21
22## Useful spans
23
24- `source.connect.http`
25- `source.catalog.sync`
26- `source.store.persist`
27- `source.store.load_by_id`
28- `source.status.update`
29- `graphql.syncCatalog`
30- `graphql.introspection.fetch`
31- `graphql.manifest.extract`
32- `graphql.definitions.compile`
33- `graphql.operations.build`
34- `graphql.snapshot.build`
35
36## What the traces showed
37
38- Raw Linear introspection fetch is fast, around sub-second to about one second.
39- The biggest remaining cost in the GraphQL import path is `graphql.operations.build`, around 5s on Linear.
40- A large earlier regression was not GraphQL import itself. It was declaration refresh churn interfering with the connect flow.
41- The fix was:
42 - keep `.d.ts` generation backgrounded
43 - debounce it with Effect fibers / `Effect.sleep`
44 - avoid scheduling workspace-wide declaration refresh during normal `probing` / `connected` source persists
45
46## Current state
47
48- Fresh Linear `connect` is back down to about 7-8s instead of ~70s.
49- `linear.d.ts` is much smaller now because source-level shared shapes are deduped.
50- The next performance target is `graphql.operations.build`, not tracing or `.d.ts` scheduling.
plugins/google-discovery/react/index.ts+4−1
@@ -9,7 +9,10 @@ import { · 8 unmodified lines
9 GoogleDiscoveryDetailRoute,9 GoogleDiscoveryDetailRoute,
10 GoogleDiscoveryEditRoute,10 GoogleDiscoveryEditRoute,
11} from "./components";11} from "./components";
12export { getGoogleDiscoveryIconUrl } from "./icons";12export {
13 getGoogleDiscoveryIconUrl,
14 getGoogleDiscoveryIconUrlForService,
15} from "./icons";
13 16
14export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({17export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({
15 key: GOOGLE_DISCOVERY_PLUGIN_KEY,18 key: GOOGLE_DISCOVERY_PLUGIN_KEY,
plugins/google-discovery/sdk/index.ts+427−15
@@ -1,15 +1,25 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
3 4
4import {5import {
5 createCatalogImportMetadata,6 createCatalogImportMetadata,
6 createSourceCatalogSyncResult,7 createSourceCatalogSyncResult,
7} from "@executor/source-core";8} from "@executor/source-core";
8import type { Source } from "@executor/platform-sdk/schema";9import type {
10 LocalExecutorConfig,
11 Source,
12} from "@executor/platform-sdk/schema";
13import {
14 SourceSchema,
15} from "@executor/platform-sdk/schema";
9import {16import {
10 defineExecutorSourcePlugin,17 defineExecutorSourcePlugin,
11} from "@executor/platform-sdk/plugins";18} from "@executor/platform-sdk/plugins";
12import {19import {
20 createPluginLocalConfigEntrySchema,
21 decodeCurrentOrLegacyLocalConfigAuth,
22 pluginLocalConfigSourceFromConfig,
13 SecretMaterialDeleterService,23 SecretMaterialDeleterService,
14 SecretMaterialResolverService,24 SecretMaterialResolverService,
15 SecretMaterialStorerService,25 SecretMaterialStorerService,
@@ -21,11 +31,18 @@ import { · 5 unmodified lines
21 GOOGLE_DISCOVERY_EXECUTOR_KEY,31 GOOGLE_DISCOVERY_EXECUTOR_KEY,
22 GOOGLE_DISCOVERY_SOURCE_KIND,32 GOOGLE_DISCOVERY_SOURCE_KIND,
23 GoogleDiscoveryBatchSourceInputSchema,33 GoogleDiscoveryBatchSourceInputSchema,
34 GoogleDiscoveryConnectInputSchema,
24 GoogleDiscoveryConnectionAuthSchema,35 GoogleDiscoveryConnectionAuthSchema,
36 GoogleDiscoveryOAuthPopupResultSchema,
25 GoogleDiscoveryOAuthSessionSchema,37 GoogleDiscoveryOAuthSessionSchema,
38 GoogleDiscoverySourceConfigPayloadSchema,
39 GoogleDiscoveryStartBatchOAuthInputSchema,
40 GoogleDiscoveryStartOAuthInputSchema,
41 GoogleDiscoveryStartOAuthResultSchema,
26 defaultGoogleDiscoveryUrl,42 defaultGoogleDiscoveryUrl,
27 deriveGoogleDiscoveryNamespace,43 deriveGoogleDiscoveryNamespace,
28 GoogleDiscoveryStoredSourceDataSchema,44 GoogleDiscoveryStoredSourceDataSchema,
45 GoogleDiscoveryUpdateSourceInputSchema,
29 type GoogleDiscoveryBatchSourceInput,46 type GoogleDiscoveryBatchSourceInput,
30 type GoogleDiscoveryConnectInput,47 type GoogleDiscoveryConnectInput,
31 type GoogleDiscoveryConnectionAuth,48 type GoogleDiscoveryConnectionAuth,
@@ -62,6 +79,16 @@ import { · 30 unmodified lines
62 refreshOAuth2AccessToken,79 refreshOAuth2AccessToken,
63} from "./oauth2";80} from "./oauth2";
64 81
82const GoogleDiscoverySourceIdInputSchema = Schema.Struct({
83 sourceId: Schema.String,
84});
85const GoogleDiscoveryCompleteOAuthInputSchema = Schema.Struct({
86 state: Schema.String,
87 code: Schema.optional(Schema.String),
88 error: Schema.optional(Schema.String),
89 errorDescription: Schema.optional(Schema.String),
90});
91
65const decodeStoredSourceData = Schema.decodeUnknownSync(92const decodeStoredSourceData = Schema.decodeUnknownSync(
66 GoogleDiscoveryStoredSourceDataSchema,93 GoogleDiscoveryStoredSourceDataSchema,
67);94);
@@ -70,6 +97,34 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 2 unmodified lines
70);97);
71const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);98const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);
72const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);99const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);
100const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(
101 createPluginLocalConfigEntrySchema({
102 kind: GOOGLE_DISCOVERY_SOURCE_KIND,
103 config: GoogleDiscoveryStoredSourceDataSchema,
104 }),
105);
106const decodeGoogleDiscoveryLegacyLocalConfigOption = Schema.decodeUnknownOption(
107 Schema.Struct({
108 kind: Schema.Literal(GOOGLE_DISCOVERY_SOURCE_KIND),
109 name: Schema.optional(Schema.String),
110 namespace: Schema.optional(Schema.String),
111 enabled: Schema.optional(Schema.Boolean),
112 connection: Schema.Struct({
113 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
114 auth: Schema.optional(Schema.Unknown),
115 }),
116 binding: Schema.Struct({
117 service: Schema.String,
118 version: Schema.String,
119 discoveryUrl: Schema.optional(Schema.NullOr(Schema.String)),
120 defaultHeaders: Schema.optional(
121 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
122 ),
123 scopes: Schema.optional(Schema.Array(Schema.String)),
124 }),
125 config: Schema.optional(Schema.Unknown),
126 }),
127);
73 128
74export type GoogleDiscoverySourceStorage = {129export type GoogleDiscoverySourceStorage = {
75 get: (input: {130 get: (input: {
@@ -518,6 +573,44 @@ const googleDiscoveryConnectInputFromAddInput = ( · 442 unmodified lines
518 auth: input.auth,573 auth: input.auth,
519});574});
520 575
576const googleDiscoveryStoredSourceDataFromLocalConfig = (input: {
577 config: unknown;
578 loadedConfig: LocalExecutorConfig | null;
579}): GoogleDiscoveryStoredSourceData => {
580 const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config);
581 if (Option.isSome(current)) {
582 return current.value.config;
583 }
584
585 const legacy = decodeGoogleDiscoveryLegacyLocalConfigOption(input.config);
586 if (Option.isSome(legacy)) {
587 return storedSourceDataFromInput({
588 name:
589 legacy.value.name
590 ?? `${legacy.value.binding.service} ${legacy.value.binding.version}`,
591 service: legacy.value.binding.service,
592 version: legacy.value.binding.version,
593 discoveryUrl: legacy.value.binding.discoveryUrl ?? null,
594 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
595 scopes: legacy.value.binding.scopes ?? [],
596 auth: decodeCurrentOrLegacyLocalConfigAuth({
597 auth: legacy.value.connection.auth,
598 authSchema: GoogleDiscoveryConnectionAuthSchema,
599 loadedConfig: input.loadedConfig,
600 onLegacySecretRef: (tokenSecretRef) => ({
601 kind: "bearer",
602 tokenSecretRef,
603 }),
604 fallback: {
605 kind: "none",
606 },
607 }),
608 });
609 }
610
611 throw new Error("Unsupported Google Discovery local source config.");
612};
613
521export const googleDiscoverySdkPlugin = (options: {614export const googleDiscoverySdkPlugin = (options: {
522 storage: GoogleDiscoverySourceStorage;615 storage: GoogleDiscoverySourceStorage;
523 oauthSessions: GoogleDiscoveryOAuthSessionStorage;616 oauthSessions: GoogleDiscoveryOAuthSessionStorage;
@@ -575,6 +668,18 @@ export const googleDiscoverySdkPlugin = (options: { · 51 unmodified lines
575 }668 }
576 }),669 }),
577 },670 },
671 localConfig: {
672 toConfigSource: ({ source, stored }) =>
673 pluginLocalConfigSourceFromConfig({
674 source,
675 config: stored,
676 }),
677 recoverStored: ({ config, loadedConfig }) =>
678 googleDiscoveryStoredSourceDataFromLocalConfig({
679 config,
680 loadedConfig,
681 }),
682 },
578 catalog: {683 catalog: {
579 kind: "imported",684 kind: "imported",
580 identity: ({ source }) => ({685 identity: ({ source }) => ({
@@ -584,20 +689,10 @@ export const googleDiscoverySdkPlugin = (options: { · 3 unmodified lines
584 sync: ({ source, stored }) =>689 sync: ({ source, stored }) =>
585 Effect.gen(function* () {690 Effect.gen(function* () {
586 if (stored === null) {691 if (stored === null) {
587 return createSourceCatalogSyncResult({692 return yield* runtimeEffectError(
588 fragment: {693 "plugins/google-discovery/sdk",
589 version: "ir.v1.fragment",694 `Google Discovery source storage missing for ${source.id}`,
590 },695 );
591 importMetadata: {
592 ...createCatalogImportMetadata({
593 source,
594 pluginKey: GOOGLE_DISCOVERY_SOURCE_KIND,
595 }),
596 importerVersion: "ir.v1.google_discovery",
597 sourceConfigHash: "missing",
598 },
599 sourceHash: null,
600 });
601 }696 }
602 697
603 const headers = yield* resolveGoogleAuthHeaders({698 const headers = yield* resolveGoogleAuthHeaders({
@@ -725,6 +820,323 @@ export const googleDiscoverySdkPlugin = (options: { · 121 unmodified lines
725 }),820 }),
726 },821 },
727 },822 },
823 tools: [
824 {
825 name: "getSourceConfig",
826 description: "Load the saved configuration for a Google Discovery source.",
827 inputSchema: GoogleDiscoverySourceIdInputSchema,
828 outputSchema: GoogleDiscoverySourceConfigPayloadSchema,
829 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
830 },
831 {
832 name: "createSource",
833 description: "Create a Google Discovery source.",
834 inputSchema: GoogleDiscoveryConnectInputSchema,
835 outputSchema: SourceSchema,
836 execute: ({ args, source }) => source.createSource(args),
837 },
838 {
839 name: "updateSource",
840 description: "Update a Google Discovery source configuration.",
841 inputSchema: GoogleDiscoveryUpdateSourceInputSchema,
842 outputSchema: SourceSchema,
843 execute: ({ args, source }) => source.updateSource(args),
844 },
845 {
846 name: "removeSource",
847 description:
848 "Remove a Google Discovery source and its stored plugin data.",
849 inputSchema: GoogleDiscoverySourceIdInputSchema,
850 outputSchema: Schema.Boolean,
851 execute: ({ args, source }) => source.removeSource(args.sourceId),
852 },
853 {
854 name: "startOAuth",
855 description: "Start a Google OAuth flow for a single API source.",
856 inputSchema: GoogleDiscoveryStartOAuthInputSchema,
857 outputSchema: GoogleDiscoveryStartOAuthResultSchema,
858 execute: ({ args }) =>
859 Effect.gen(function* () {
860 const discoveryUrl =
861 args.discoveryUrl?.trim()
862 || defaultGoogleDiscoveryUrl(args.service, args.version);
863 let scopes: ReadonlyArray<string>;
864 if (args.scopes.length > 0) {
865 const configuredScopes: Array<string> = args.scopes
866 .map((scope: string) => scope.trim())
867 .filter((scope: string) => scope.length > 0);
868 scopes = [...new Set(configuredScopes)];
869 } else {
870 scopes = yield* fetchGoogleDiscoveryDocumentWithHeaders({
871 url: discoveryUrl,
872 headers: args.defaultHeaders ?? {},
873 }).pipe(
874 Effect.flatMap((document) =>
875 inferScopesFromDiscoveryDocument({
876 service: args.service,
877 document,
878 })
879 ),
880 );
881 }
882 const sessionId = `gdisc_oauth_${crypto.randomUUID()}`;
883 const codeVerifier = createPkceCodeVerifier();
884
885 yield* options.oauthSessions.put({
886 sessionId,
887 value: decodeSession({
888 kind: "single",
889 service: args.service.trim(),
890 version: args.version.trim(),
891 discoveryUrl,
892 defaultHeaders: args.defaultHeaders,
893 scopes: [...scopes],
894 clientId: args.clientId.trim(),
895 clientSecretRef: args.clientSecretRef,
896 clientAuthentication: args.clientAuthentication,
897 redirectUrl: args.redirectUrl,
898 codeVerifier,
899 }),
900 });
901
902 return {
903 sessionId,
904 authorizationUrl: buildOAuth2AuthorizationUrl({
905 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
906 clientId: args.clientId.trim(),
907 redirectUri: args.redirectUrl,
908 scopes,
909 state: sessionId,
910 codeVerifier,
911 extraParams: {
912 access_type: "offline",
913 prompt: "consent",
914 include_granted_scopes: "true",
915 },
916 }),
917 scopes: [...scopes],
918 };
919 }),
920 },
921 {
922 name: "startBatchOAuth",
923 description: "Start one Google OAuth flow for multiple API sources.",
924 inputSchema: GoogleDiscoveryStartBatchOAuthInputSchema,
925 outputSchema: GoogleDiscoveryStartOAuthResultSchema,
926 execute: ({ args }) =>
927 Effect.gen(function* () {
928 const normalizedSources: Array<GoogleDiscoveryBatchSourceInput> =
929 args.sources.map((source: GoogleDiscoveryBatchSourceInput) =>
930 normalizeBatchSourceInput(source)
931 );
932 if (normalizedSources.length === 0) {
933 return yield* runtimeEffectError(
934 "plugins/google-discovery/sdk",
935 "Select at least one Google API to connect.",
936 );
937 }
938
939 const inferredScopes: ReadonlyArray<ReadonlyArray<string>> = yield* Effect.forEach(
940 normalizedSources,
941 (source: GoogleDiscoveryBatchSourceInput) =>
942 resolveScopesForBatchSource({
943 source,
944 }),
945 );
946 const scopes: ReadonlyArray<string> = [
947 ...new Set(inferredScopes.flat()),
948 ];
949 const sessionId = `gdisc_oauth_${crypto.randomUUID()}`;
950 const codeVerifier = createPkceCodeVerifier();
951
952 yield* options.oauthSessions.put({
953 sessionId,
954 value: decodeSession({
955 kind: "batch",
956 sources: normalizedSources.map((
957 source: GoogleDiscoveryBatchSourceInput,
958 index: number,
959 ) => ({
960 ...source,
961 scopes: inferredScopes[index] ?? [],
962 })),
963 scopes,
964 clientId: args.clientId.trim(),
965 clientSecretRef: args.clientSecretRef,
966 clientAuthentication: args.clientAuthentication,
967 redirectUrl: args.redirectUrl,
968 codeVerifier,
969 }),
970 });
971
972 return {
973 sessionId,
974 authorizationUrl: buildOAuth2AuthorizationUrl({
975 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
976 clientId: args.clientId.trim(),
977 redirectUri: args.redirectUrl,
978 scopes,
979 state: sessionId,
980 codeVerifier,
981 extraParams: {
982 access_type: "offline",
983 prompt: "consent",
984 include_granted_scopes: "true",
985 },
986 }),
987 scopes,
988 };
989 }),
990 },
991 {
992 name: "completeOAuth",
993 description:
994 "Complete a Google OAuth callback and return the connected auth payload.",
995 inputSchema: GoogleDiscoveryCompleteOAuthInputSchema,
996 outputSchema: GoogleDiscoveryOAuthPopupResultSchema,
997 execute: ({ args, source }) =>
998 Effect.gen(function* () {
999 if (args.error) {
1000 return yield* runtimeEffectError(
1001 "plugins/google-discovery/sdk",
1002 args.errorDescription || args.error || "Google OAuth failed",
1003 );
1004 }
1005 if (!args.code) {
1006 return yield* runtimeEffectError(
1007 "plugins/google-discovery/sdk",
1008 "Missing Google OAuth code.",
1009 );
1010 }
1011
1012 const session = yield* options.oauthSessions.get(args.state);
1013 if (session === null) {
1014 return yield* runtimeEffectError(
1015 "plugins/google-discovery/sdk",
1016 `Google OAuth session not found: ${args.state}`,
1017 );
1018 }
1019
1020 const resolveSecretMaterial = yield* SecretMaterialResolverService;
1021 const clientSecret = session.clientSecretRef
1022 ? yield* resolveSecretMaterial({
1023 ref: session.clientSecretRef,
1024 })
1025 : null;
1026 const tokenResponse = yield* exchangeOAuth2AuthorizationCode({
1027 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1028 clientId: session.clientId,
1029 clientAuthentication: session.clientAuthentication,
1030 clientSecret,
1031 redirectUri: session.redirectUrl,
1032 codeVerifier: session.codeVerifier,
1033 code: args.code,
1034 });
1035 const expiresAt =
1036 typeof tokenResponse.expires_in === "number"
1037 ? Date.now() + tokenResponse.expires_in * 1000
1038 : null;
1039 const storeSecretMaterial = yield* SecretMaterialStorerService;
1040
1041 if (session.kind === "single") {
1042 const accessTokenRef = yield* storeSecretMaterial({
1043 purpose: "oauth_access_token",
1044 value: tokenResponse.access_token,
1045 name: `${session.service} Google Access Token`,
1046 });
1047 const refreshTokenRef = tokenResponse.refresh_token
1048 ? yield* storeSecretMaterial({
1049 purpose: "oauth_refresh_token",
1050 value: tokenResponse.refresh_token,
1051 name: `${session.service} Google Refresh Token`,
1052 })
1053 : null;
1054
1055 if (options.oauthSessions.remove) {
1056 yield* options.oauthSessions.remove(args.state);
1057 }
1058
1059 return {
1060 type: "executor:oauth-result" as const,
1061 ok: true as const,
1062 sessionId: args.state,
1063 mode: "single" as const,
1064 auth: {
1065 kind: "oauth2" as const,
1066 clientId: session.clientId,
1067 clientSecretRef: session.clientSecretRef,
1068 clientAuthentication: session.clientAuthentication,
1069 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
1070 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1071 scopes: session.scopes,
1072 accessTokenRef,
1073 refreshTokenRef,
1074 expiresAt,
1075 },
1076 };
1077 }
1078
1079 const createdSources = [];
1080 for (const sessionSource of session.sources) {
1081 const accessTokenRef = yield* storeSecretMaterial({
1082 purpose: "oauth_access_token",
1083 value: tokenResponse.access_token,
1084 name: `${sessionSource.service} Google Access Token`,
1085 });
1086 const refreshTokenRef = tokenResponse.refresh_token
1087 ? yield* storeSecretMaterial({
1088 purpose: "oauth_refresh_token",
1089 value: tokenResponse.refresh_token,
1090 name: `${sessionSource.service} Google Refresh Token`,
1091 })
1092 : null;
1093
1094 const createdSource = yield* source.createSource({
1095 name: sessionSource.name,
1096 service: sessionSource.service,
1097 version: sessionSource.version,
1098 discoveryUrl: sessionSource.discoveryUrl,
1099 defaultHeaders: sessionSource.defaultHeaders,
1100 scopes:
1101 sessionSource.scopes.length > 0
1102 ? sessionSource.scopes
1103 : session.scopes,
1104 auth: {
1105 kind: "oauth2",
1106 clientId: session.clientId,
1107 clientSecretRef: session.clientSecretRef,
1108 clientAuthentication: session.clientAuthentication,
1109 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
1110 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1111 scopes:
1112 sessionSource.scopes.length > 0
1113 ? sessionSource.scopes
1114 : session.scopes,
1115 accessTokenRef,
1116 refreshTokenRef,
1117 expiresAt,
1118 },
1119 });
1120 createdSources.push(createdSource);
1121 }
1122
1123 if (options.oauthSessions.remove) {
1124 yield* options.oauthSessions.remove(args.state);
1125 }
1126
1127 return {
1128 type: "executor:oauth-result" as const,
1129 ok: true as const,
1130 sessionId: args.state,
1131 mode: "batch" as const,
1132 sources: createdSources.map((createdSource) => ({
1133 id: createdSource.id,
1134 name: createdSource.name,
1135 })),
1136 };
1137 }),
1138 },
1139 ] as const,
728 extendExecutor: ({ source, executor }) => {1140 extendExecutor: ({ source, executor }) => {
729 const provideRuntime = <A>(1141 const provideRuntime = <A>(
730 effect: Effect.Effect<A, Error, any>,1142 effect: Effect.Effect<A, Error, any>,
plugins/graphql/sdk/index.ts+125−15
@@ -4,25 +4,38 @@ import { · 3 unmodified lines
4 applyJsonBodyPlacements,4 applyJsonBodyPlacements,
5} from "@executor/codemode-core";5} from "@executor/codemode-core";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
8 9
9import {10import {
10 createCatalogImportMetadata,11 createCatalogImportMetadata,
11 createSourceCatalogSyncResult,12 createSourceCatalogSyncResult,
12} from "@executor/source-core";13} from "@executor/source-core";
13import type { Source } from "@executor/platform-sdk/schema";14import type {
15 LocalExecutorConfig,
16 Source,
17} from "@executor/platform-sdk/schema";
18import {
19 SourceSchema,
20} from "@executor/platform-sdk/schema";
14import {21import {
15 defineExecutorSourcePlugin,22 defineExecutorSourcePlugin,
16} from "@executor/platform-sdk/plugins";23} from "@executor/platform-sdk/plugins";
17import {24import {
18 SecretMaterialResolverService,25 SecretMaterialResolverService,
26 createPluginLocalConfigEntrySchema,
27 decodeCurrentOrLegacyLocalConfigAuth,
28 pluginLocalConfigSourceFromConfig,
19 provideExecutorRuntime,29 provideExecutorRuntime,
20 runtimeEffectError,30 runtimeEffectError,
21} from "@executor/platform-sdk/runtime";31} from "@executor/platform-sdk/runtime";
22import {32import {
23 deriveGraphqlNamespace,33 deriveGraphqlNamespace,
34 GraphqlConnectInputSchema,
24 GraphqlConnectionAuthSchema,35 GraphqlConnectionAuthSchema,
36 GraphqlSourceConfigPayloadSchema,
25 GraphqlStoredSourceDataSchema,37 GraphqlStoredSourceDataSchema,
38 GraphqlUpdateSourceInputSchema,
26 type GraphqlConnectInput,39 type GraphqlConnectInput,
27 type GraphqlSourceConfigPayload,40 type GraphqlSourceConfigPayload,
28 type GraphqlStoredSourceData,41 type GraphqlStoredSourceData,
@@ -44,6 +57,10 @@ import { · 15 unmodified lines
44 type GraphqlToolProviderData,57 type GraphqlToolProviderData,
45} from "./provider-data";58} from "./provider-data";
46 59
60const GraphqlSourceIdInputSchema = Schema.Struct({
61 sourceId: Schema.String,
62});
63
47const decodeStoredSourceData = Schema.decodeUnknownSync(GraphqlStoredSourceDataSchema);64const decodeStoredSourceData = Schema.decodeUnknownSync(GraphqlStoredSourceDataSchema);
48const decodeProviderData = Schema.decodeUnknownSync(GraphqlToolProviderDataSchema);65const decodeProviderData = Schema.decodeUnknownSync(GraphqlToolProviderDataSchema);
49 66
@@ -134,6 +151,32 @@ const asStringRecord = (value: unknown): Record<string, string> => · 84 unmodified lines
134 ),151 ),
135 );152 );
136 153
154const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(
155 createPluginLocalConfigEntrySchema({
156 kind: "graphql",
157 config: GraphqlStoredSourceDataSchema,
158 }),
159);
160
161const decodeGraphqlLegacyLocalConfigOption = Schema.decodeUnknownOption(
162 Schema.Struct({
163 kind: Schema.Literal("graphql"),
164 name: Schema.optional(Schema.String),
165 namespace: Schema.optional(Schema.String),
166 enabled: Schema.optional(Schema.Boolean),
167 connection: Schema.Struct({
168 endpoint: Schema.String,
169 auth: Schema.optional(Schema.Unknown),
170 }),
171 binding: Schema.Struct({
172 defaultHeaders: Schema.optional(
173 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
174 ),
175 }),
176 config: Schema.optional(Schema.Unknown),
177 }),
178);
179
137const withoutUndefinedEntries = (180const withoutUndefinedEntries = (
138 record: Readonly<Record<string, unknown>>,181 record: Readonly<Record<string, unknown>>,
139): Record<string, unknown> =>182): Record<string, unknown> =>
@@ -311,6 +354,41 @@ const graphqlConnectInputFromAddInput = ( · 171 unmodified lines
311 auth: input.auth,354 auth: input.auth,
312});355});
313 356
357const graphqlStoredSourceDataFromLocalConfig = (input: {
358 config: unknown;
359 loadedConfig: LocalExecutorConfig | null;
360}): GraphqlStoredSourceData => {
361 const current = decodeGraphqlCurrentLocalConfigOption(input.config);
362 if (Option.isSome(current)) {
363 return normalizeStoredSourceData(current.value.config);
364 }
365
366 const legacy = decodeGraphqlLegacyLocalConfigOption(input.config);
367 if (Option.isSome(legacy)) {
368 return storedSourceDataFromInput({
369 name: legacy.value.name ?? "GraphQL",
370 endpoint: legacy.value.connection.endpoint,
371 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
372 auth: decodeCurrentOrLegacyLocalConfigAuth({
373 auth: legacy.value.connection.auth,
374 authSchema: GraphqlConnectionAuthSchema,
375 loadedConfig: input.loadedConfig,
376 onLegacySecretRef: (tokenSecretRef) => ({
377 kind: "bearer",
378 tokenSecretRef,
379 headerName: null,
380 prefix: null,
381 }),
382 fallback: {
383 kind: "none",
384 },
385 }),
386 });
387 }
388
389 throw new Error("Unsupported GraphQL local source config.");
390};
391
314export const graphqlSdkPlugin = (options: {392export const graphqlSdkPlugin = (options: {
315 storage: GraphqlSourceStorage;393 storage: GraphqlSourceStorage;
316}) => defineExecutorSourcePlugin<394}) => defineExecutorSourcePlugin<
@@ -363,6 +441,18 @@ export const graphqlSdkPlugin = (options: { · 46 unmodified lines
363 toConfig: ({ source, stored }) =>441 toConfig: ({ source, stored }) =>
364 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),442 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),
365 },443 },
444 localConfig: {
445 toConfigSource: ({ source, stored }) =>
446 pluginLocalConfigSourceFromConfig({
447 source,
448 config: normalizeStoredSourceData(stored),
449 }),
450 recoverStored: ({ config, loadedConfig }) =>
451 graphqlStoredSourceDataFromLocalConfig({
452 config,
453 loadedConfig,
454 }),
455 },
366 catalog: {456 catalog: {
367 kind: "imported",457 kind: "imported",
368 identity: ({ source }) => ({458 identity: ({ source }) => ({
@@ -372,20 +462,10 @@ export const graphqlSdkPlugin = (options: { · 3 unmodified lines
372 sync: ({ source, stored }) =>462 sync: ({ source, stored }) =>
373 Effect.gen(function* () {463 Effect.gen(function* () {
374 if (stored === null) {464 if (stored === null) {
375 return createSourceCatalogSyncResult({465 return yield* runtimeEffectError(
376 fragment: {466 "plugins/graphql/sdk",
377 version: "ir.v1.fragment",467 `GraphQL source storage missing for ${source.id}`,
378 },468 );
379 importMetadata: {
380 ...createCatalogImportMetadata({
381 source,
382 pluginKey: "graphql",
383 }),
384 importerVersion: "ir.v1.graphql",
385 sourceConfigHash: "missing",
386 },
387 sourceHash: null,
388 });
389 }469 }
390 470
391 const normalizedStored = normalizeStoredSourceData(stored);471 const normalizedStored = normalizeStoredSourceData(stored);
@@ -522,6 +602,36 @@ export const graphqlSdkPlugin = (options: { · 130 unmodified lines
522 }),602 }),
523 },603 },
524 },604 },
605 tools: [
606 {
607 name: "getSourceConfig",
608 description: "Load the saved configuration for a GraphQL source.",
609 inputSchema: GraphqlSourceIdInputSchema,
610 outputSchema: GraphqlSourceConfigPayloadSchema,
611 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
612 },
613 {
614 name: "createSource",
615 description: "Create a GraphQL source.",
616 inputSchema: GraphqlConnectInputSchema,
617 outputSchema: SourceSchema,
618 execute: ({ args, source }) => source.createSource(args),
619 },
620 {
621 name: "updateSource",
622 description: "Update a GraphQL source configuration.",
623 inputSchema: GraphqlUpdateSourceInputSchema,
624 outputSchema: SourceSchema,
625 execute: ({ args, source }) => source.updateSource(args),
626 },
627 {
628 name: "removeSource",
629 description: "Remove a GraphQL source and its stored plugin data.",
630 inputSchema: GraphqlSourceIdInputSchema,
631 outputSchema: Schema.Boolean,
632 execute: ({ args, source }) => source.removeSource(args.sourceId),
633 },
634 ] as const,
525 extendExecutor: ({ source, executor }) => {635 extendExecutor: ({ source, executor }) => {
526 const provideRuntime = <A>(636 const provideRuntime = <A>(
527 effect: Effect.Effect<A, Error, any>,637 effect: Effect.Effect<A, Error, any>,
plugins/mcp/sdk/index.ts+275−15
@@ -21,13 +21,21 @@ import { · 20 unmodified lines
21 createSourceCatalogSyncResult,21 createSourceCatalogSyncResult,
22 normalizeSourceDiscoveryUrl,22 normalizeSourceDiscoveryUrl,
23 probeHeadersFromAuth,23 probeHeadersFromAuth,
24 SourceTransportSchema,
25 StringArraySchema,
26 StringMapSchema,
24 type SourceDiscoveryResult,27 type SourceDiscoveryResult,
25} from "@executor/source-core";28} from "@executor/source-core";
26import type { Source } from "@executor/platform-sdk/schema";29import {
30 type Source,
31 SourceSchema,
32} from "@executor/platform-sdk/schema";
27import {33import {
28 defineExecutorSourcePlugin,34 defineExecutorSourcePlugin,
29} from "@executor/platform-sdk/plugins";35} from "@executor/platform-sdk/plugins";
30import {36import {
37 createPluginLocalConfigEntrySchema,
38 pluginLocalConfigSourceFromConfig,
31 SecretMaterialDeleterService,39 SecretMaterialDeleterService,
32 SecretMaterialResolverService,40 SecretMaterialResolverService,
33 SecretMaterialStorerService,41 SecretMaterialStorerService,
@@ -36,9 +44,17 @@ import { · 2 unmodified lines
36 runtimeEffectError,44 runtimeEffectError,
37} from "@executor/platform-sdk/runtime";45} from "@executor/platform-sdk/runtime";
38import {46import {
47 McpConnectInputSchema,
39 McpConnectionAuthSchema,48 McpConnectionAuthSchema,
49 McpDiscoverInputSchema,
50 McpDiscoverResultSchema,
51 McpOAuthPopupResultSchema,
40 McpOAuthSessionSchema,52 McpOAuthSessionSchema,
53 McpSourceConfigPayloadSchema,
54 McpStartOAuthInputSchema,
55 McpStartOAuthResultSchema,
41 McpStoredSourceDataSchema,56 McpStoredSourceDataSchema,
57 McpUpdateSourceInputSchema,
42 deriveMcpNamespace,58 deriveMcpNamespace,
43 resolveMcpEndpoint,59 resolveMcpEndpoint,
44 type McpConnectInput,60 type McpConnectInput,
@@ -54,6 +70,7 @@ import { · 9 unmodified lines
54 type McpUpdateSourceInput,70 type McpUpdateSourceInput,
55} from "@executor/plugin-mcp-shared";71} from "@executor/plugin-mcp-shared";
56import * as Effect from "effect/Effect";72import * as Effect from "effect/Effect";
73import * as Option from "effect/Option";
57import * as Schema from "effect/Schema";74import * as Schema from "effect/Schema";
58 75
59import {76import {
@@ -83,6 +100,16 @@ import { · 23 unmodified lines
83const decodeStoredSourceData = Schema.decodeUnknownSync(McpStoredSourceDataSchema);100const decodeStoredSourceData = Schema.decodeUnknownSync(McpStoredSourceDataSchema);
84const decodeSession = Schema.decodeUnknownSync(McpOAuthSessionSchema);101const decodeSession = Schema.decodeUnknownSync(McpOAuthSessionSchema);
85 102
103const McpSourceIdInputSchema = Schema.Struct({
104 sourceId: Schema.String,
105});
106const McpCompleteOAuthInputSchema = Schema.Struct({
107 state: Schema.String,
108 code: Schema.optional(Schema.String),
109 error: Schema.optional(Schema.String),
110 errorDescription: Schema.optional(Schema.String),
111});
112
86const McpExecutableBindingSchema = Schema.Struct({113const McpExecutableBindingSchema = Schema.Struct({
87 toolId: Schema.String,114 toolId: Schema.String,
88 toolName: Schema.String,115 toolName: Schema.String,
@@ -222,6 +249,38 @@ const normalizeStringArray = ( · 133 unmodified lines
222 return normalized.length > 0 ? normalized : null;249 return normalized.length > 0 ? normalized : null;
223};250};
224 251
252const decodeMcpAuthOption = Schema.decodeUnknownOption(McpConnectionAuthSchema);
253const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(
254 createPluginLocalConfigEntrySchema({
255 kind: "mcp",
256 config: McpStoredSourceDataSchema,
257 }),
258);
259const decodeMcpLegacyLocalConfigOption = Schema.decodeUnknownOption(
260 Schema.Struct({
261 kind: Schema.Literal("mcp"),
262 name: Schema.optional(Schema.String),
263 namespace: Schema.optional(Schema.String),
264 enabled: Schema.optional(Schema.Boolean),
265 connection: Schema.optional(
266 Schema.Struct({
267 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
268 auth: Schema.optional(Schema.Unknown),
269 }),
270 ),
271 binding: Schema.Struct({
272 transport: Schema.optional(Schema.NullOr(SourceTransportSchema)),
273 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
274 headers: Schema.optional(Schema.NullOr(StringMapSchema)),
275 command: Schema.optional(Schema.NullOr(Schema.String)),
276 args: Schema.optional(Schema.NullOr(StringArraySchema)),
277 env: Schema.optional(Schema.NullOr(StringMapSchema)),
278 cwd: Schema.optional(Schema.NullOr(Schema.String)),
279 }),
280 config: Schema.optional(Schema.Unknown),
281 }),
282);
283
225const normalizeStoredSourceData = (284const normalizeStoredSourceData = (
226 input: McpConnectInput | McpSourceConfigPayload,285 input: McpConnectInput | McpSourceConfigPayload,
227): Effect.Effect<McpStoredSourceData, Error, never> =>286): Effect.Effect<McpStoredSourceData, Error, never> =>
@@ -332,6 +391,51 @@ const mcpConnectInputFromAddInput = ( · 104 unmodified lines
332 auth: input.auth,391 auth: input.auth,
333});392});
334 393
394const mcpStoredSourceDataFromLocalConfig = (
395 config: unknown,
396): Effect.Effect<McpStoredSourceData, Error, never> => {
397 const current = decodeMcpCurrentLocalConfigOption(config);
398 if (Option.isSome(current)) {
399 return Effect.succeed(current.value.config);
400 }
401
402 const legacy = decodeMcpLegacyLocalConfigOption(config);
403 if (Option.isSome(legacy)) {
404 const auth = decodeMcpAuthOption(legacy.value.connection?.auth);
405 if (
406 legacy.value.connection?.auth !== undefined &&
407 Option.isNone(auth)
408 ) {
409 return runtimeEffectError(
410 "plugins/mcp/sdk",
411 "Unsupported MCP auth configuration in local source config.",
412 );
413 }
414
415 return normalizeStoredSourceData({
416 name: legacy.value.name ?? "MCP",
417 endpoint: legacy.value.connection?.endpoint ?? null,
418 transport: legacy.value.binding.transport ?? null,
419 queryParams: legacy.value.binding.queryParams ?? null,
420 headers: legacy.value.binding.headers ?? null,
421 command: legacy.value.binding.command ?? null,
422 args: legacy.value.binding.args ?? null,
423 env: legacy.value.binding.env ?? null,
424 cwd: legacy.value.binding.cwd ?? null,
425 auth: Option.isSome(auth)
426 ? auth.value
427 : {
428 kind: "none",
429 },
430 });
431 }
432
433 return runtimeEffectError(
434 "plugins/mcp/sdk",
435 "Unsupported MCP local source config.",
436 );
437};
438
335const mcpCatalogOperationFromManifestEntry = (input: {439const mcpCatalogOperationFromManifestEntry = (input: {
336 entry: McpToolManifestEntry;440 entry: McpToolManifestEntry;
337 server: McpServerMetadata | null | undefined;441 server: McpServerMetadata | null | undefined;
@@ -658,6 +762,15 @@ export const mcpSdkPlugin = ( · 320 unmodified lines
658 }762 }
659 }),763 }),
660 },764 },
765 localConfig: {
766 toConfigSource: ({ source, stored }) =>
767 pluginLocalConfigSourceFromConfig({
768 source,
769 config: stored,
770 }),
771 recoverStored: ({ config }) =>
772 mcpStoredSourceDataFromLocalConfig(config),
773 },
661 catalog: {774 catalog: {
662 kind: "imported",775 kind: "imported",
663 identity: ({ source }) => ({776 identity: ({ source }) => ({
@@ -667,20 +780,10 @@ export const mcpSdkPlugin = ( · 3 unmodified lines
667 sync: ({ source, stored }) =>780 sync: ({ source, stored }) =>
668 Effect.gen(function* () {781 Effect.gen(function* () {
669 if (stored === null) {782 if (stored === null) {
670 return createSourceCatalogSyncResult({783 return yield* runtimeEffectError(
671 fragment: {784 "plugins/mcp/sdk",
672 version: "ir.v1.fragment",785 `MCP source storage missing for ${source.id}`,
673 },786 );
674 importMetadata: {
675 ...createCatalogImportMetadata({
676 source,
677 pluginKey: "mcp",
678 }),
679 importerVersion: "ir.v1.mcp",
680 sourceConfigHash: "missing",
681 },
682 sourceHash: null,
683 });
684 }787 }
685 788
686 const connector = yield* createStoredMcpConnector({789 const connector = yield* createStoredMcpConnector({
@@ -853,6 +956,163 @@ export const mcpSdkPlugin = ( · 166 unmodified lines
853 }),956 }),
854 },957 },
855 },958 },
959 tools: [
960 {
961 name: "discoverSource",
962 description: "Probe an MCP endpoint before creating a source.",
963 inputSchema: McpDiscoverInputSchema,
964 outputSchema: McpDiscoverResultSchema,
965 execute: ({ args }) =>
966 Effect.gen(function* () {
967 const normalizedUrl = normalizeSourceDiscoveryUrl(args.endpoint);
968 const discovered = yield* detectMcpSource({
969 normalizedUrl,
970 headers: probeHeadersFromAuth(args.probeAuth ?? null),
971 });
972 return discovered satisfies SourceDiscoveryResult | null;
973 }),
974 },
975 {
976 name: "getSourceConfig",
977 description: "Load the saved configuration for an MCP source.",
978 inputSchema: McpSourceIdInputSchema,
979 outputSchema: McpSourceConfigPayloadSchema,
980 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
981 },
982 {
983 name: "createSource",
984 description: "Create an MCP source.",
985 inputSchema: McpConnectInputSchema,
986 outputSchema: SourceSchema,
987 execute: ({ args, source }) => source.createSource(args),
988 },
989 {
990 name: "updateSource",
991 description: "Update an MCP source configuration.",
992 inputSchema: McpUpdateSourceInputSchema,
993 outputSchema: SourceSchema,
994 execute: ({ args, source }) => source.updateSource(args),
995 },
996 {
997 name: "removeSource",
998 description: "Remove an MCP source and its stored plugin data.",
999 inputSchema: McpSourceIdInputSchema,
1000 outputSchema: Schema.Boolean,
1001 execute: ({ args, source }) => source.removeSource(args.sourceId),
1002 },
1003 {
1004 name: "startOAuth",
1005 description: "Start an MCP OAuth flow for a source connection.",
1006 inputSchema: McpStartOAuthInputSchema,
1007 outputSchema: McpStartOAuthResultSchema,
1008 execute: ({ args }) =>
1009 Effect.gen(function* () {
1010 const endpoint = resolveMcpEndpoint({
1011 endpoint: args.endpoint.trim(),
1012 queryParams: args.queryParams,
1013 });
1014 const sessionId = `mcp_oauth_${crypto.randomUUID()}`;
1015 const started = yield* startMcpOAuthAuthorization({
1016 endpoint,
1017 redirectUrl: args.redirectUrl,
1018 state: sessionId,
1019 });
1020
1021 yield* options.oauthSessions.put({
1022 sessionId,
1023 value: decodeSession({
1024 endpoint,
1025 redirectUrl: args.redirectUrl,
1026 codeVerifier: started.codeVerifier,
1027 resourceMetadataUrl: started.resourceMetadataUrl,
1028 authorizationServerUrl: started.authorizationServerUrl,
1029 resourceMetadata: started.resourceMetadata,
1030 authorizationServerMetadata: started.authorizationServerMetadata,
1031 clientInformation: started.clientInformation,
1032 }),
1033 });
1034
1035 return {
1036 sessionId,
1037 authorizationUrl: started.authorizationUrl,
1038 };
1039 }),
1040 },
1041 {
1042 name: "completeOAuth",
1043 description: "Complete an MCP OAuth callback and return the connected auth payload.",
1044 inputSchema: McpCompleteOAuthInputSchema,
1045 outputSchema: McpOAuthPopupResultSchema,
1046 execute: ({ args }) =>
1047 Effect.gen(function* () {
1048 if (args.error) {
1049 return yield* runtimeEffectError(
1050 "plugins/mcp/sdk",
1051 args.errorDescription || args.error || "MCP OAuth failed",
1052 );
1053 }
1054 if (!args.code) {
1055 return yield* runtimeEffectError(
1056 "plugins/mcp/sdk",
1057 "Missing MCP OAuth code.",
1058 );
1059 }
1060
1061 const session = yield* options.oauthSessions.get(args.state);
1062 if (session === null) {
1063 return yield* runtimeEffectError(
1064 "plugins/mcp/sdk",
1065 `MCP OAuth session not found: ${args.state}`,
1066 );
1067 }
1068
1069 const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
1070 session,
1071 code: args.code,
1072 });
1073 const storeSecretMaterial = yield* SecretMaterialStorerService;
1074 const accessTokenRef = yield* storeSecretMaterial({
1075 purpose: "oauth_access_token",
1076 value: exchanged.tokens.access_token,
1077 name: "MCP Access Token",
1078 });
1079 const refreshTokenRef = exchanged.tokens.refresh_token
1080 ? yield* storeSecretMaterial({
1081 purpose: "oauth_refresh_token",
1082 value: exchanged.tokens.refresh_token,
1083 name: "MCP Refresh Token",
1084 })
1085 : null;
1086
1087 if (options.oauthSessions.remove) {
1088 yield* options.oauthSessions.remove(args.state);
1089 }
1090
1091 return {
1092 type: "executor:oauth-result" as const,
1093 ok: true as const,
1094 sessionId: args.state,
1095 auth: {
1096 kind: "oauth2" as const,
1097 redirectUri: session.redirectUrl,
1098 accessTokenRef,
1099 refreshTokenRef,
1100 tokenType: exchanged.tokens.token_type ?? "Bearer",
1101 expiresAt:
1102 typeof exchanged.tokens.expires_in === "number"
1103 ? Date.now() + exchanged.tokens.expires_in * 1000
1104 : null,
1105 scope: exchanged.tokens.scope ?? null,
1106 resourceMetadataUrl: session.resourceMetadataUrl,
1107 authorizationServerUrl: session.authorizationServerUrl,
1108 resourceMetadata: session.resourceMetadata,
1109 authorizationServerMetadata: session.authorizationServerMetadata,
1110 clientInformation: session.clientInformation,
1111 },
1112 };
1113 }),
1114 },
1115 ] as const,
856 extendExecutor: ({ source, executor }) => {1116 extendExecutor: ({ source, executor }) => {
857 const provideRuntime = <A>(1117 const provideRuntime = <A>(
858 effect: Effect.Effect<A, Error, any>,1118 effect: Effect.Effect<A, Error, any>,
plugins/openapi/sdk/index.ts+165−15
@@ -1,21 +1,36 @@
1import { createHash } from "node:crypto";1import { createHash } from "node:crypto";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
4 5
5import {6import {
6 createCatalogImportMetadata,7 createCatalogImportMetadata,
7 createSourceCatalogSyncResult,8 createSourceCatalogSyncResult,
8} from "@executor/source-core";9} from "@executor/source-core";
9import type { Source } from "@executor/platform-sdk/schema";10import type {
11 LocalExecutorConfig,
12 Source,
13} from "@executor/platform-sdk/schema";
14import {
15 SourceSchema,
16} from "@executor/platform-sdk/schema";
10import {17import {
11 defineExecutorSourcePlugin,18 defineExecutorSourcePlugin,
12} from "@executor/platform-sdk/plugins";19} from "@executor/platform-sdk/plugins";
13import {20import {
21 createPluginLocalConfigEntrySchema,
22 decodeCurrentOrLegacyLocalConfigAuth,
23 pluginLocalConfigSourceFromConfig,
14 runtimeEffectError,24 runtimeEffectError,
15 SecretMaterialResolverService,25 SecretMaterialResolverService,
16} from "@executor/platform-sdk/runtime";26} from "@executor/platform-sdk/runtime";
17import {27import {
18 OpenApiConnectionAuthSchema,28 OpenApiConnectionAuthSchema,
29 OpenApiConnectInputSchema,
30 OpenApiPreviewRequestSchema,
31 OpenApiPreviewResponseSchema,
32 OpenApiSourceConfigPayloadSchema,
33 OpenApiUpdateSourceInputSchema,
19 deriveOpenApiNamespace,34 deriveOpenApiNamespace,
20 previewOpenApiDocument,35 previewOpenApiDocument,
21 type OpenApiConnectInput,36 type OpenApiConnectInput,
@@ -46,6 +61,10 @@ import { · 24 unmodified lines
46 type OpenApiToolProviderData,61 type OpenApiToolProviderData,
47} from "./types";62} from "./types";
48 63
64const OpenApiSourceIdInputSchema = Schema.Struct({
65 sourceId: Schema.String,
66});
67
49const stableSourceHash = (value: OpenApiStoredSourceData): string =>68const stableSourceHash = (value: OpenApiStoredSourceData): string =>
50 createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);69 createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);
51 70
@@ -130,6 +149,40 @@ const normalizeStoredSourceData = ( · 78 unmodified lines
130 auth: normalizeOpenApiAuth(stored.auth as Record<string, unknown>),149 auth: normalizeOpenApiAuth(stored.auth as Record<string, unknown>),
131});150});
132 151
152const OpenApiLocalSourceConfigSchema = Schema.Struct({
153 specUrl: Schema.String,
154 baseUrl: Schema.NullOr(Schema.String),
155 auth: OpenApiConnectionAuthSchema,
156 defaultHeaders: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
157});
158
159const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(
160 createPluginLocalConfigEntrySchema({
161 kind: "openapi",
162 config: OpenApiLocalSourceConfigSchema,
163 }),
164);
165
166const decodeOpenApiLegacyLocalConfigOption = Schema.decodeUnknownOption(
167 Schema.Struct({
168 kind: Schema.Literal("openapi"),
169 name: Schema.optional(Schema.String),
170 namespace: Schema.optional(Schema.String),
171 enabled: Schema.optional(Schema.Boolean),
172 connection: Schema.Struct({
173 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
174 auth: Schema.optional(Schema.Unknown),
175 }),
176 binding: Schema.Struct({
177 specUrl: Schema.String,
178 defaultHeaders: Schema.optional(
179 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
180 ),
181 }),
182 config: Schema.optional(Schema.Unknown),
183 }),
184);
185
133const createStoredSourceData = (186const createStoredSourceData = (
134 input: OpenApiConnectInput,187 input: OpenApiConnectInput,
135): OpenApiStoredSourceData => ({188): OpenApiStoredSourceData => ({
@@ -158,6 +211,47 @@ const configFromStoredSourceData = ( · 22 unmodified lines
158 auth: stored.auth,211 auth: stored.auth,
159});212});
160 213
214const openApiStoredSourceDataFromLocalConfig = (input: {
215 config: unknown;
216 loadedConfig: LocalExecutorConfig | null;
217}): OpenApiStoredSourceData => {
218 const current = decodeOpenApiCurrentLocalConfigOption(input.config);
219 if (Option.isSome(current)) {
220 return normalizeStoredSourceData({
221 ...current.value.config,
222 etag: null,
223 lastSyncAt: null,
224 });
225 }
226
227 const legacy = decodeOpenApiLegacyLocalConfigOption(input.config);
228 if (Option.isSome(legacy)) {
229 return normalizeStoredSourceData({
230 specUrl: legacy.value.binding.specUrl.trim(),
231 baseUrl: legacy.value.connection.endpoint?.trim() || null,
232 auth: decodeCurrentOrLegacyLocalConfigAuth({
233 auth: legacy.value.connection.auth,
234 authSchema: OpenApiConnectionAuthSchema,
235 loadedConfig: input.loadedConfig,
236 onLegacySecretRef: (tokenSecretRef) => ({
237 kind: "bearer",
238 tokenSecretRef,
239 headerName: null,
240 prefix: null,
241 }),
242 fallback: {
243 kind: "none",
244 },
245 }),
246 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
247 etag: null,
248 lastSyncAt: null,
249 });
250 }
251
252 throw new Error("Unsupported OpenAPI local source config.");
253};
254
161const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);255const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);
162 256
163const asRecord = (value: unknown): Record<string, unknown> =>257const asRecord = (value: unknown): Record<string, unknown> =>
@@ -491,6 +585,23 @@ export const openApiSdkPlugin = ( · 327 unmodified lines
491 toConfig: ({ source, stored }) =>585 toConfig: ({ source, stored }) =>
492 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),586 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),
493 },587 },
588 localConfig: {
589 toConfigSource: ({ source, stored }) =>
590 pluginLocalConfigSourceFromConfig({
591 source,
592 config: {
593 specUrl: stored.specUrl,
594 baseUrl: stored.baseUrl,
595 auth: stored.auth,
596 defaultHeaders: stored.defaultHeaders,
597 },
598 }),
599 recoverStored: ({ config, loadedConfig }) =>
600 openApiStoredSourceDataFromLocalConfig({
601 config,
602 loadedConfig,
603 }),
604 },
494 catalog: {605 catalog: {
495 kind: "imported",606 kind: "imported",
496 identity: ({ source }) => ({607 identity: ({ source }) => ({
@@ -500,20 +611,10 @@ export const openApiSdkPlugin = ( · 3 unmodified lines
500 sync: ({ source, stored }) =>611 sync: ({ source, stored }) =>
501 Effect.gen(function* () {612 Effect.gen(function* () {
502 if (stored === null) {613 if (stored === null) {
503 return createSourceCatalogSyncResult({614 return yield* runtimeEffectError(
504 fragment: {615 "plugins/openapi/sdk",
505 version: "ir.v1.fragment",616 `OpenAPI source storage missing for ${source.id}`,
506 },617 );
507 importMetadata: {
508 ...createCatalogImportMetadata({
509 source,
510 pluginKey: "openapi",
511 }),
512 importerVersion: "ir.v1.openapi",
513 sourceConfigHash: "missing",
514 },
515 sourceHash: null,
516 });
517 }618 }
518 619
519 const normalizedStored = normalizeStoredSourceData(stored);620 const normalizedStored = normalizeStoredSourceData(stored);
@@ -713,6 +814,55 @@ export const openApiSdkPlugin = ( · 193 unmodified lines
713 }),814 }),
714 },815 },
715 },816 },
817 tools: [
818 {
819 name: "previewDocument",
820 description: "Preview an OpenAPI document before creating a source.",
821 inputSchema: OpenApiPreviewRequestSchema,
822 outputSchema: OpenApiPreviewResponseSchema,
823 execute: ({ args }) =>
824 Effect.tryPromise({
825 try: () => previewOpenApiDocument(args),
826 catch: (cause) =>
827 cause instanceof Error ? cause : new Error(String(cause)),
828 }),
829 },
830 {
831 name: "getSourceConfig",
832 description: "Load the saved configuration for an OpenAPI source.",
833 inputSchema: OpenApiSourceIdInputSchema,
834 outputSchema: OpenApiSourceConfigPayloadSchema,
835 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
836 },
837 {
838 name: "createSource",
839 description: "Create an OpenAPI source.",
840 inputSchema: OpenApiConnectInputSchema,
841 outputSchema: SourceSchema,
842 execute: ({ args, source }) => source.createSource(args),
843 },
844 {
845 name: "updateSource",
846 description: "Update an OpenAPI source configuration.",
847 inputSchema: OpenApiUpdateSourceInputSchema,
848 outputSchema: SourceSchema,
849 execute: ({ args, source }) => source.updateSource(args),
850 },
851 {
852 name: "refreshSource",
853 description: "Refresh an OpenAPI source and resync its catalog.",
854 inputSchema: OpenApiSourceIdInputSchema,
855 outputSchema: SourceSchema,
856 execute: ({ args, source }) => source.refreshSource(args.sourceId),
857 },
858 {
859 name: "removeSource",
860 description: "Remove an OpenAPI source and its stored plugin data.",
861 inputSchema: OpenApiSourceIdInputSchema,
862 outputSchema: Schema.Boolean,
863 execute: ({ args, source }) => source.removeSource(args.sourceId),
864 },
865 ] as const,
716 extendExecutor: ({ source, executor }) => {866 extendExecutor: ({ source, executor }) => {
717 const provideRuntime = <A>(867 const provideRuntime = <A>(
718 effect: Effect.Effect<A, Error, any>,868 effect: Effect.Effect<A, Error, any>,
ralph.ts+0−22
@@ -1,22 +0,0 @@
1import { $ } from "bun";
2
3let prompt = `
4You're running in a while loop with new sesions each time, you will have to check to see what the progress so far that has been made is.
5
6We are migrating from an old, very messy model to a plugin system for OpenAPI, GraphQL, MCP, and Google Discovery. You'll see that we've got the OpenAPI implemented. If you look at the Git diff on this branch, you'll see a bunch of deleted files and functionality around the rest of the product. We have to bring back all of the functionality that was removed for GraphQL and MCP of an API or Discovery. The thing is that it has to be now localised to those plugins, so you can read the plugin and the functionality will go away. Some examples of that are:
7
8- how Google Discovery has the or credentials and that or flow; that all gets implemented in the Google Discovery plugin
9- the MCP OAuth stuff gets implemented in the MCP plugin
10
11Plugins can own storage; they can own API routes. They're meant to extend the product. If the product does not match what the plugins need, then we can extend the core, but the core should not be plugin-specific. What you need to do is work until this is fully migrated to this new model. Once it is fully migrated, output <STOP-TOKEN-124124> 3 times so you stop working.
12
13Feel free to autonomously test the app yourself via calling the API or running the local CLI
14
15You can take notes in agent-notes/
16`
17let output = ''
18
19do {
20 output = await $`codex exec --yolo "${prompt}"`.then(result => String(result.stdout));
21} while (// stop token has to show up more than 1 time
22 output.split("\n").filter(line => line.includes("<STOP-TOKEN-124124>")).length < 2);
skills-lock.json+0−10
@@ -1,10 +0,0 @@
1{
2 "version": 1,
3 "skills": {
4 "mintlify": {
5 "source": "mintlify.com",
6 "sourceType": "well-known",
7 "computedHash": "fc61ab6703f692dc2c215e74327a8998d02987ca9571084440be20e7ecad6b46"
8 }
9 }
10}