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 @@
1−import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from '@effect/platform';
2−import { startOpenApiTestServer } from '@executor/effect-test-utils';
3−import * as Effect from 'effect/Effect';
4−import * as Layer from 'effect/Layer';
5−import * as Schema from 'effect/Schema';
6−
7−const ownerParam = HttpApiSchema.param('owner', Schema.String);
8−const repoParam = HttpApiSchema.param('repo', Schema.String);
9−class 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−) {}
14−class DemoApi extends HttpApi.make('demo').add(DemoReposApi) {}
15−const 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−);
21−const 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−};
30−main().catch((e) => { console.error(e); process.exit(1); });
AGENTS.md+0−1
@@ -1 +0,0 @@
1−CLAUDE.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 @@
1−Executor:
2−
3−Project goal: Build an execution environment for agents to interact with APIs, MCPs, GraphQL, and other services via writing TypeScript.
4−
5−The 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−
7−the 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−
9−the 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−
11−let's look at some examples.
12−
13−let'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−
17−so `tools.discover("github issues list")`
18−
19−that 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−
21−another 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−
23−a 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−
25−So 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−
27−Current state:
28−
29−We are on v3 of this codebase which is a completely fresh start, the original codebase is in legacy/, the v2 is in legacy2/.
30−
31−Architecture I like:
32−
33−Database:
34−Local-file-backed control plane for local use, with room for future hosted backends
35−
36−Server:
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−
44−We 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−
46−Doing MCP here w/ MCP elicitation allows us to not give special treatment to the executor app while also giving a nice UX
47−
48−Ideally the MCP server shares a lot of logic between API
49−
50−Clients:
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−
58−Standalone:
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−
63−Runtimes:
64−
65−- Deno
66−- In process
67−- Cloudflare worker runtime
68−
69−Other:
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−
75−Rough 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−
77−References:
78−https://blog.cloudflare.com/code-mode/ - concept we are implementing
79−https://mcp.axiom.co/mcp - MCP server that requires auth
80−https://modelcontextprotocol.io/specification/draft/client/elicitation - How we are going to be handling interactions from the user
81−legacy/ original implementation
82−legacy2
TRACING.md+0−43
@@ -1,43 +0,0 @@
1−# Local Tracing
2−
3−Start Jaeger locally:
4−
5−```bash
6−bun run trace:up
7−```
8−
9−Run the web/server dev process with tracing enabled:
10−
11−```bash
12−EXECUTOR_TRACE_ENABLED=1 \
13−EXECUTOR_TRACE_SERVICE_NAME=executor-local \
14−EXECUTOR_TRACE_OTLP_ENDPOINT=http://127.0.0.1:4317 \
15−bun run dev
16−```
17−
18−Then open Jaeger:
19−
20−- UI: `http://127.0.0.1:16686/search?service=executor-local`
21−
22−You can also query traces over HTTP:
23−
24−```bash
25−curl "http://127.0.0.1:16686/api/traces?service=executor-local&limit=20"
26−```
27−
28−Useful 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−
39−Stop Jaeger:
40−
41−```bash
42−bun run trace:down
43−```
apps/executor/src/cli/main.ts+20−6
@@ -36,9 +36,12 @@ import { · 35 unmodified lines
3636 openApiSdkPlugin,
3737 } from "@executor/plugin-openapi-sdk";
3838 import {
39− getExecutorSourcesAddHelpLines,
39+ getExecutorInternalToolHelpLines,
4040 RuntimeExecutionResolverService,
4141 } from "@executor/platform-sdk/runtime";
42+import type {
43+ ExecutorSdkPluginRegistry,
44+} from "@executor/platform-sdk/plugins";
4245 import {
4346 createExecutorEffect,
4447 type ExecutorEffect as Executor,
@@ -228,7 +231,10 @@ const toEffectCliArgv = (args: readonly string[]): string[] => [ · 183 unmodified lines
228231 ...args,
229232 ];
230233
231−const buildWorkflowText = (namespaces: readonly string[] = []): string =>
234+const buildWorkflowText = (
235+ namespaces: readonly string[] = [],
236+ pluginRegistry?: ExecutorSdkPluginRegistry,
237+): string =>
232238 [
233239 "Execute TypeScript in sandbox; call tools via discovery workflow.",
234240 ...(namespaces.length > 0
@@ -242,12 +248,15 @@ const buildWorkflowText = (namespaces: readonly string[] = []): string => · 7 unmodified lines
242248 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
243249 "3) Call selected tools.<path>(input).",
244250 "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."]),
246254 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",
247255 "Do not use fetch; use tools.* only.",
248256 ].join("\n");
249257
250−const getDefaultRunWorkflow = () => buildWorkflowText();
258+const getDefaultRunWorkflow = (pluginRegistry?: ExecutorSdkPluginRegistry) =>
259+ buildWorkflowText([], pluginRegistry);
251260
252261 const indentBlock = (value: string, prefix: string = " "): string =>
253262 value
@@ -290,9 +299,10 @@ const closeExecutor = (executor: Executor) => · 36 unmodified lines
290299
291300 const buildRunWorkflowText = (
292301 catalog?: ToolCatalog,
302+ pluginRegistry?: ExecutorSdkPluginRegistry,
293303 ): Effect.Effect<string, Error, never> => {
294304 if (!catalog) {
295− return Effect.succeed(getDefaultRunWorkflow());
305+ return Effect.succeed(getDefaultRunWorkflow(pluginRegistry));
296306 }
297307
298308 return catalog.listNamespaces({ limit: 200 }).pipe(
@@ -301,6 +311,7 @@ const buildRunWorkflowText = ( · 2 unmodified lines
301311 namespaces.length > 0
302312 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)
303313 : ["none discovered yet"],
314+ pluginRegistry,
304315 )
305316 ),
306317 Effect.mapError(toError),
@@ -360,7 +371,10 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 53 unmodified lines
360371 Effect.mapError(toError),
361372 );
362373
363− return yield* buildRunWorkflowText(environment.catalog);
374+ return yield* buildRunWorkflowText(
375+ environment.catalog,
376+ executor.runtime.pluginRegistry,
377+ );
364378 }),
365379 closeExecutor,
366380 ).pipe(
apps/web/src/components/source-favicon.tsx+88−10
@@ -1,9 +1,12 @@
11 import { useState } from "react";
22 import type { Source } from "@executor/react";
3+import { getGoogleDiscoveryIconUrlForService } from "@executor/plugin-google-discovery-react";
34 import { getSourceFrontendIconUrl } from "../plugins";
5+import type { SourcePreset } from "../plugins/source-presets";
6+import { getSourceFaviconUrl } from "../lib/source-favicon";
47 import { cn } from "../lib/utils";
58
6−type SourceKind = Source["kind"] | string;
9+type SourceKind = Source["kind"] | SourcePreset["kind"] | string;
710
811 export function SourceFavicon({
912 source,
@@ -14,12 +17,55 @@ export function SourceFavicon({ · 4 unmodified lines
1417 className?: string;
1518 size?: number;
1619 }) {
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+
30+export 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+
53+function 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+}) {
1864 const [failedUrl, setFailedUrl] = useState<string | null>(null);
1965 const isFailed = Boolean(faviconUrl && failedUrl === faviconUrl);
2066
2167 if (!faviconUrl || isFailed) {
22− return <DefaultSourceIcon kind={source.kind} className={className} />;
68+ return <DefaultSourceIcon kind={kind} className={className} />;
2369 }
2470
2571 return (
@@ -38,7 +84,7 @@ export function SourceFavicon({ · 12 unmodified lines
3884 }
3985
4086 export function DefaultSourceIcon({
41− kind: _kind,
87+ kind,
4288 className,
4389 }: {
4490 kind: SourceKind;
@@ -46,10 +92,42 @@ export function DefaultSourceIcon({ · 1 unmodified line
4692 }) {
4793 const base = cn("shrink-0", className);
4894
49− return (
50− <svg viewBox="0 0 16 16" fill="none" className={base}>
51− <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.2" />
52− <path d="M5 6h6M5 8h4M5 10h3" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
53− </svg>
54− );
95+ switch (kind) {
96+ case "mcp":
97+ return (
98+ <svg viewBox="0 0 16 16" fill="none" className={base}>
99+ <rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
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+ }
55133 }
apps/web/src/components/source-plugins-reset-state.tsx+1−1
@@ -10,7 +10,7 @@ export function SourcePluginsResetState( · 9 unmodified lines
1010 input: SourcePluginsResetStateProps,
1111 ) {
1212 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]">
1414 <div className="mx-auto max-w-3xl px-6 py-10 lg:px-10 lg:py-14">
1515 <div className="rounded-2xl border border-border bg-card p-8">
1616 <h2 className="font-display text-xl tracking-tight text-foreground">
apps/web/src/lib/source-favicon.ts+158−0
@@ -0,0 +1,158 @@
1+import { parse as parseDomain } from "tldts";
2+
3+const RAW_HOSTS = new Set([
4+ "raw.githubusercontent.com",
5+ "cdn.jsdelivr.net",
6+ "raw.github.com",
7+ "unpkg.com",
8+]);
9+
10+const 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+
24+const 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+
66+const parseUrl = (value: string): URL | null => {
67+ try {
68+ return new URL(value);
69+ } catch {
70+ return null;
71+ }
72+};
73+
74+const 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+
80+const isMeaninglessToken = (token: string): boolean =>
81+ token.length <= 1
82+ || IGNORED_BRAND_TOKENS.has(token)
83+ || /^v?\d+$/.test(token);
84+
85+const 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+
95+const 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+
104+const 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+
135+export 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+
153+export 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
1111 import {
1212 GraphqlReactPlugin,
1313 } from "@executor/plugin-graphql-react";
14−import {
15− LocalToolsReactPlugin,
16−} from "@executor/plugin-local-tools-react";
1714 import {
1815 McpReactPlugin,
1916 } from "@executor/plugin-mcp-react";
2017 import {
2118 OpenApiReactPlugin,
2219 } from "@executor/plugin-openapi-react";
23−import {
24− OnePasswordReactPlugin,
25−} from "@executor/plugin-onepassword-react";
2620 import {
2721 createExecutorPluginPaths,
2822 createSourcePluginPaths,
2923 registerExecutorFrontendPlugins,
3024 type ExecutorFrontendPlugin,
3125 } from "@executor/react/plugins";
26+import { getFallbackSourceFaviconUrl } from "../lib/source-favicon";
3227
3328 const frontendPlugins = [
3429 ExecutionHistoryReactPlugin,
35− LocalToolsReactPlugin,
3630 McpReactPlugin,
3731 GraphqlReactPlugin,
3832 GoogleDiscoveryReactPlugin,
3933 OpenApiReactPlugin,
40− OnePasswordReactPlugin,
4134 ] as const satisfies readonly ExecutorFrontendPlugin[];
4235
4336 const frontendPluginRegistry = registerExecutorFrontendPlugins(frontendPlugins);
@@ -85,10 +78,15 @@ export const getSourceFrontendPaths = (kind: string) => { · 41 unmodified lines
8578 return plugin ? createSourcePluginPaths(plugin.key) : null;
8679 };
8780
81+const isGoogleDiscoverySource = (kind: string): boolean =>
82+ kind === "google_discovery"
83+ || kind === "google-discovery";
84+
8885 export const getSourceFrontendIconUrl = (source: Source) =>
89− source.kind === "google-discovery"
86+ (isGoogleDiscoverySource(source.kind)
9087 ? getGoogleDiscoveryIconUrl(source)
91− : null;
88+ : null)
89+ ?? getFallbackSourceFaviconUrl(source);
9290
9391 export const getSecretStoreFrontendPlugin = (kind: string) =>
9492 registeredFrontendPlugins.find((plugin) => plugin.secretStore?.kind === kind) ?? null;
apps/web/src/plugins/pages.tsx+2−2
@@ -13,7 +13,7 @@ import { · 12 unmodified lines
1313 type FrontendPluginRouteSearch,
1414 } from "@executor/react/plugins";
1515
16−import { DefaultSourceIcon } from "../components/source-favicon";
16+import { SourcePresetFavicon } from "../components/source-favicon";
1717 import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
1818 import { SourcePluginsResetState } from "../components/source-plugins-reset-state";
1919 import {
@@ -202,7 +202,7 @@ export function SourcePluginsIndexPage() { · 182 unmodified lines
202202 >
203203 <div className="mb-2 flex items-center gap-2.5">
204204 <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" />
206206 </div>
207207 <div className="min-w-0">
208208 <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
304304 variant="ghost"
305305 size="icon"
306306 onClick={() => setOpen((value) => !value)}
307+ aria-expanded={open}
307308 className="shrink-0 size-auto rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"
308309 style={{ marginLeft: paddingLeft }}
309310 >
310311 <IconChevron
311− className={cn("shrink-0 transition-transform duration-150", open && "rotate-90")}
312− style={{ width: 8, height: 8 }}
312+ className="shrink-0 transition-transform duration-150"
313+ style={{
314+ width: 8,
315+ height: 8,
316+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
317+ transformOrigin: "50% 50%",
318+ }}
313319 />
314320 </Button>
315321 <SourceToolListItem
@@ -328,6 +334,7 @@ const SourceToolTreeNodeView = (props: { · 12 unmodified lines
328334 type="button"
329335 variant="ghost"
330336 onClick={() => setOpen((value) => !value)}
337+ aria-expanded={open}
331338 className={cn(
332339 "group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40",
333340 open ? "text-foreground/80" : "text-muted-foreground/60",
@@ -335,11 +342,13 @@ const SourceToolTreeNodeView = (props: { · 1 unmodified line
335342 style={{ paddingLeft }}
336343 >
337344 <IconChevron
338− className={cn(
339− "shrink-0 text-muted-foreground/30 transition-transform duration-150",
340− open && "rotate-90",
341− )}
342− style={{ width: 8, height: 8 }}
345+ className="shrink-0 text-muted-foreground/30 transition-transform duration-150"
346+ style={{
347+ width: 8,
348+ height: 8,
349+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
350+ transformOrigin: "50% 50%",
351+ }}
343352 />
344353 <IconFolder
345354 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}
@@ -632,7 +641,7 @@ export const SourceToolModelWorkbench = ( · 286 unmodified lines
632641 </div>
633642 </div>
634643
635− <div className="flex-1 overflow-y-auto">
644+ <div className="flex-1 overflow-y-auto w-[300px]">
636645 {filteredTools.length === 0 ? (
637646 <div className="p-4 text-center text-[13px] text-muted-foreground/50">
638647 {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
22 ExecutionEnvelope,
33 } from "@executor/platform-sdk/schema";
44 import {
5− getExecutorSourcesAddHelpLines,
5+ getExecutorInternalToolHelpLines,
66 RuntimeSourceCatalogStoreService,
77 createExecution,
88 getExecution,
@@ -152,6 +152,7 @@ type CatalogLike = { · 143 unmodified lines
152152 const executeDescriptionToolsPerSource = 5;
153153
154154 const buildExecuteWorkflowText = (
155+ runtime: ExecutorRuntime,
155156 sourceToolExamples: ReadonlyArray<{
156157 sourceName: string;
157158 toolPaths: readonly string[];
@@ -173,7 +174,7 @@ const buildExecuteWorkflowText = ( · 15 unmodified lines
173174 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
174175 "3) Call selected tools.<path>(input).",
175176 "4) Use source plugins to inspect or add API sources.",
176− ...getExecutorSourcesAddHelpLines(),
177+ ...getExecutorInternalToolHelpLines(runtime.pluginRegistry),
177178 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",
178179 "Do not use fetch; use tools.* only.",
179180 ].join("\n");
@@ -201,11 +202,13 @@ const loadExecuteDescription = (runtime: ExecutorRuntime): Promise<string> => · 21 unmodified lines
201202 .filter((catalog) => catalog.toolPaths.length > 0);
202203
203204 if (sourceToolExamples.length === 0) {
204− return buildExecuteWorkflowText();
205+ return buildExecuteWorkflowText(runtime);
205206 }
206207
207− return buildExecuteWorkflowText(sourceToolExamples);
208− }).pipe(Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText()))),
208+ return buildExecuteWorkflowText(runtime, sourceToolExamples);
209+ }).pipe(
210+ Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText(runtime))),
211+ ),
209212 );
210213
211214 const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {
packages/platform/sdk/src/executor-effect.ts+8−9
@@ -83,15 +83,13 @@ import { · 82 unmodified lines
8383 listExecutions,
8484 resumeExecution,
8585 } from "./runtime/execution/service";
86−import type {
87− ExecutorSdkPluginHost,
88− ExecutorSdkPlugin,
89− ExecutorSdkPluginExtensions,
90− PluginCleanup,
91−} from "./plugins";
9286 import {
93− configureExecutorSourcePlugins,
94−} from "./runtime/sources/source-plugins";
87+ registerExecutorSdkPlugins,
88+ type ExecutorSdkPluginHost,
89+ type ExecutorSdkPlugin,
90+ type ExecutorSdkPluginExtensions,
91+ type PluginCleanup,
92+} from "./plugins";
9593
9694 type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<
9795 Effect.Effect.Success<T>,
@@ -320,13 +318,14 @@ export const createExecutorEffect = < · 222 unmodified lines
320318 >(
321319 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,
322320 ): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {
323− configureExecutorSourcePlugins(options.plugins ?? []);
321+ const pluginRegistry = registerExecutorSdkPlugins(options.plugins ?? []);
324322
325323 return Effect.flatMap(
326324 options.backend.createRuntime({
327325 executionResolver: options.executionResolver,
328326 resolveSecretMaterial: options.resolveSecretMaterial,
329327 getLocalServerBaseUrl: options.getLocalServerBaseUrl,
328+ pluginRegistry,
330329 }),
331330 (runtime) =>
332331 Effect.gen(function* () {
packages/platform/sdk/src/plugins.ts+321−41
@@ -6,12 +6,14 @@ import type { · 5 unmodified lines
66 } from "@executor/source-core";
77 import type { ExecutorEffect } from "./executor-effect";
88 import type { ExecutorScopeContext } from "./scope";
9−import type {
10− Source as ExecutorSource,
11− SecretMaterial,
12− SecretMaterialPurpose,
13− SecretRef,
14− SecretStore,
9+import {
10+ type LocalConfigSource,
11+ type LocalExecutorConfig,
12+ type Source as ExecutorSource,
13+ type SecretMaterial,
14+ type SecretMaterialPurpose,
15+ type SecretRef,
16+ type SecretStore,
1517 } from "./schema";
1618 import type {
1719 SourceInvokeInput,
@@ -19,12 +21,13 @@ import type { · 1 unmodified line
1921 } from "@executor/source-core";
2022 import type * as Schema from "effect/Schema";
2123 import { runtimeEffectError } from "./runtime/effect-errors";
24+import { ScopeConfigStore } from "./runtime/scope/storage";
2225
2326 export type PluginCleanup = {
2427 close: () => void | Promise<void>;
2528 };
2629
27−export type ExecutorSdkPluginHost = {};
30+export type ExecutorSdkPluginHost = ExecutorSourcePluginInternalHost;
2831
2932 export type ExecutorSdkPluginContext = {
3033 executor: ExecutorEffect & Record<string, unknown>;
@@ -41,6 +44,7 @@ export type ExecutorSdkPluginStartContext< · 10 unmodified lines
4144 type ExecutorSdkPluginInternals = {
4245 sources?: readonly ExecutorSourceContribution<any>[];
4346 secretStores?: readonly ExecutorSecretStoreContribution<any>[];
47+ managementTools?: readonly ExecutorManagementToolContribution<any, any>[];
4448 };
4549
4650 const executorSdkPluginInternalsSymbol = Symbol.for(
@@ -184,6 +188,45 @@ export type ExecutorSecretStorePluginApi< · 137 unmodified lines
184188 ) => Effect.Effect<boolean, Error, any>;
185189 };
186190
191+export 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+
205+export 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+
187230 export type ExecutorSourcePluginDefinition<
188231 TAddInput,
189232 TConnectInput,
@@ -227,6 +270,17 @@ export type ExecutorSourcePluginDefinition< · 37 unmodified lines
227270 stored: TStored | null;
228271 }) => Effect.Effect<void, Error, any>;
229272 };
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+ };
230284 catalog: {
231285 kind: SourceCatalogKind;
232286 identity?: (input: {
@@ -397,6 +451,14 @@ export type ExecutorSourcePluginInput< · 164 unmodified lines
397451 TStored,
398452 TUpdateInput
399453 >;
454+ tools?: readonly ExecutorSourcePluginManagementToolDefinition<
455+ string,
456+ any,
457+ any,
458+ TConnectInput,
459+ TSourceConfig,
460+ TUpdateInput
461+ >[];
400462 extendExecutor?: (input: ExecutorSdkPluginContext & {
401463 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
402464 }) => TExtension;
@@ -467,6 +529,121 @@ const loadSourceOfKind = ( · 64 unmodified lines
467529 return source;
468530 });
469531
532+const cloneJson = <T>(value: T): T =>
533+ JSON.parse(JSON.stringify(value)) as T;
534+
535+const 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+
542+const 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+
587+const 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+
470647 const createExecutorSourcePluginApi = <
471648 TAddInput,
472649 TConnectInput,
@@ -497,10 +674,7 @@ const createExecutorSourcePluginApi = < · 24 unmodified lines
497674 definition,
498675 host,
499676 });
500− const stored = yield* definition.storage.get({
501− scopeId: source.scopeId,
502− sourceId: source.id,
503− });
677+ const stored = yield* loadStoredOrRecover(definition, source);
504678 if (stored === null) {
505679 return yield* runtimeEffectError(
506680 "plugins",
@@ -525,6 +699,10 @@ const createExecutorSourcePluginApi = < · 18 unmodified lines
525699 sourceId: source.id,
526700 value: created.stored,
527701 });
702+ yield* persistSourceLocalConfig(definition, {
703+ source,
704+ stored: created.stored,
705+ });
528706
529707 return yield* host.sources.refreshCatalog(source.id);
530708 }),
@@ -545,6 +723,10 @@ const createExecutorSourcePluginApi = < · 14 unmodified lines
545723 sourceId: saved.id,
546724 value: updated.stored,
547725 });
726+ yield* persistSourceLocalConfig(definition, {
727+ source: saved,
728+ stored: updated.stored,
729+ });
548730
549731 return yield* host.sources.refreshCatalog(saved.id);
550732 }),
@@ -716,6 +898,7 @@ const createExecutorSecretStorePluginApi = < · 165 unmodified lines
716898 });
717899
718900 type ExecutorSourceContribution<TInput = unknown> = {
901+ pluginKey: string;
719902 kind: string;
720903 displayName: string;
721904 inputSchema: Schema.Schema<TInput, any, never>;
@@ -816,7 +999,26 @@ type ExecutorSecretStoreContribution<TInput = unknown> = { · 94 unmodified lines
816999 }, Error, any>;
8171000 };
8181001
1002+export 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+
8191020 const createExecutorSourceContribution = <
1021+ TKey extends string,
8201022 TAddInput,
8211023 TConnectInput,
8221024 TSourceConfig,
@@ -826,46 +1028,44 @@ const createExecutorSourceContribution = < · 3 unmodified lines
8261028 config: TSourceConfig;
8271029 },
8281030 >(
829− definition: ExecutorSourcePluginDefinition<
830− TAddInput,
831− TConnectInput,
832− TSourceConfig,
833− TStored,
834− TUpdateInput
835− >,
1031+ input: {
1032+ key: TKey;
1033+ source: ExecutorSourcePluginDefinition<
1034+ TAddInput,
1035+ TConnectInput,
1036+ TSourceConfig,
1037+ TStored,
1038+ TUpdateInput
1039+ >;
1040+ },
8361041 ): ExecutorSourceContribution<TAddInput> => ({
837− kind: definition.kind,
838− displayName: definition.displayName,
839− inputSchema: definition.add.inputSchema,
840− inputSignatureWidth: definition.add.inputSignatureWidth,
841− helpText: definition.add.helpText,
842− catalogKind: definition.catalog.kind,
843− catalogIdentity: definition.catalog.identity,
1042+ pluginKey: input.key,
1043+ kind: input.source.kind,
1044+ displayName: input.source.displayName,
1045+ inputSchema: input.source.add.inputSchema,
1046+ inputSignatureWidth: input.source.add.inputSignatureWidth,
1047+ helpText: input.source.add.helpText,
1048+ catalogKind: input.source.catalog.kind,
1049+ catalogIdentity: input.source.catalog.identity,
8441050 createSource: ({ args, host }) =>
845− createExecutorSourcePluginApi(definition, host).createSource(
846− definition.add.toConnectInput(args),
1051+ createExecutorSourcePluginApi(input.source, host).createSource(
1052+ input.source.add.toConnectInput(args),
8471053 ),
8481054 syncCatalog: ({ source }) =>
8491055 Effect.flatMap(
850− definition.storage.get({
851− scopeId: source.scopeId,
852− sourceId: source.id,
853− }),
1056+ loadStoredOrRecover(input.source, source),
8541057 (stored) =>
855− definition.catalog.sync({
1058+ input.source.catalog.sync({
8561059 source,
8571060 stored,
8581061 }),
8591062 ),
860− invoke: (input) =>
1063+ invoke: (invokeInput) =>
8611064 Effect.flatMap(
862− definition.storage.get({
863− scopeId: input.source.scopeId,
864− sourceId: input.source.id,
865− }),
1065+ loadStoredOrRecover(input.source, invokeInput.source),
8661066 (stored) =>
867− definition.catalog.invoke({
868− ...input,
1067+ input.source.catalog.invoke({
1068+ ...invokeInput,
8691069 stored,
8701070 }),
8711071 ),
@@ -1064,6 +1264,53 @@ const createExecutorSecretStoreContribution = < · 192 unmodified lines
10641264 ),
10651265 });
10661266
1267+const 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+
10671314 export const defineExecutorSourcePlugin = <
10681315 const TKey extends string,
10691316 TAddInput,
@@ -1110,7 +1357,17 @@ export const defineExecutorSourcePlugin = < · 40 unmodified lines
11101357 })
11111358 : undefined,
11121359 [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+ ),
11141371 },
11151372 }))(input.extendExecutor, input.start);
11161373
@@ -1179,6 +1436,10 @@ export const registerExecutorSdkPlugins = ( · 62 unmodified lines
11791436 const pluginKeys = new Set<string>();
11801437 const sources = new Map<string, ExecutorSourceContribution<any>>();
11811438 const secretStores = new Map<string, ExecutorSecretStoreContribution<any>>();
1439+ const managementTools = new Map<
1440+ string,
1441+ ExecutorManagementToolContribution<any, any>
1442+ >();
11821443
11831444 for (const plugin of plugins) {
11841445 if (pluginKeys.has(plugin.key)) {
@@ -1208,6 +1469,14 @@ export const registerExecutorSdkPlugins = ( · 23 unmodified lines
12081469
12091470 secretStores.set(secretStore.kind, secretStore);
12101471 }
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+ }
12111480 }
12121481
12131482 const getSourceContribution = (kind: string) => {
@@ -1235,13 +1504,24 @@ export const registerExecutorSdkPlugins = ( · 21 unmodified lines
12351504 store: Pick<SecretStore, "kind">,
12361505 ) => getSecretStoreContribution(store.kind);
12371506
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+
12381516 return {
12391517 plugins,
12401518 sources: [...sources.values()],
12411519 secretStores: [...secretStores.values()],
1520+ managementTools: [...managementTools.values()],
12421521 getSourceContribution,
12431522 getSourceContributionForSource,
12441523 getSecretStoreContribution,
12451524 getSecretStoreContributionForStore,
1246− };
1525+ getManagementTool,
1526+ } satisfies ExecutorSdkPluginRegistry;
12471527 };
packages/platform/sdk/src/runtime/catalog/source/reconcile.ts+26−1
@@ -3,6 +3,8 @@ import type { · 2 unmodified lines
33 Source,
44 } from "#schema";
55 import * as Effect from "effect/Effect";
6+import * as Option from "effect/Option";
7+import * as Schema from "effect/Schema";
68
79 import {
810 RuntimeLocalScopeService,
@@ -17,10 +19,33 @@ import { · 8 unmodified lines
1719 RuntimeSourceCatalogSyncService,
1820 } from "./sync";
1921
22+const decodeArtifactImportMetadataOption = Schema.decodeUnknownOption(
23+ Schema.parseJson(
24+ Schema.Struct({
25+ sourceConfigHash: Schema.String,
26+ }),
27+ ),
28+);
29+
2030 const shouldReconcileSource = (source: Source): boolean =>
2131 source.enabled
2232 && source.status === "connected";
2333
34+const 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+
2449 export const reconcileMissingSourceCatalogArtifacts = (input: {
2550 scopeId: ScopeId;
2651 actorScopeId?: ScopeId | null;
@@ -49,7 +74,7 @@ export const reconcileMissingSourceCatalogArtifacts = (input: { · 22 unmodified lines
4974 const artifact = yield* sourceArtifactStore.read({
5075 sourceId: source.id,
5176 });
52− if (artifact !== null) {
77+ if (!artifactNeedsRefresh(artifact)) {
5378 continue;
5479 }
5580
packages/platform/sdk/src/runtime/catalog/source/sync.ts+11−1
@@ -22,6 +22,7 @@ import { · 21 unmodified lines
2222 } from "../../scope-state";
2323 import {
2424 getSourceContributionForSource,
25+ ExecutorPluginRegistryService,
2526 } from "../../sources/source-plugins";
2627 import {
2728 snapshotFromSourceCatalogSyncResult,
@@ -39,6 +40,7 @@ const shouldIndexSource = (source: Source): boolean => · 11 unmodified lines
3940 && source.status === "connected";
4041
4142 type RuntimeSourceCatalogSyncDeps = {
43+ pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
4244 runtimeLocalScope: RuntimeLocalScopeState;
4345 scopeStateStore: ScopeStateStoreShape;
4446 sourceArtifactStore: SourceArtifactStoreShape;
@@ -46,6 +48,7 @@ type RuntimeSourceCatalogSyncDeps = { · 1 unmodified line
4648 };
4749
4850 type SourceCatalogSyncServices =
51+ | ExecutorPluginRegistryService
4952 | RuntimeLocalScopeService
5053 | ScopeStateStore
5154 | SourceArtifactStore
@@ -112,7 +115,10 @@ const syncSourceCatalogWithDeps = ( · 60 unmodified lines
112115 return;
113116 }
114117
115− const definition = getSourceContributionForSource(input.source);
118+ const definition = getSourceContributionForSource(
119+ deps.pluginRegistry,
120+ input.source,
121+ );
116122 const irModel = yield* definition.syncCatalog({
117123 source: input.source,
118124 });
@@ -165,12 +171,14 @@ export const syncSourceCatalog = (input: { · 46 unmodified lines
165171 }): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
166172 Effect.gen(function* () {
167173 const runtimeLocalScope = yield* RuntimeLocalScopeService;
174+ const pluginRegistry = yield* ExecutorPluginRegistryService;
168175 const scopeStateStore = yield* ScopeStateStore;
169176 const sourceArtifactStore = yield* SourceArtifactStore;
170177 const sourceTypeDeclarationsRefresher =
171178 yield* SourceTypeDeclarationsRefresherService;
172179 return yield* syncSourceCatalogWithDeps(
173180 {
181+ pluginRegistry,
174182 runtimeLocalScope,
175183 scopeStateStore,
176184 sourceArtifactStore,
@@ -187,11 +195,13 @@ export const RuntimeSourceCatalogSyncLive = Layer.effect( · 10 unmodified lines
187195 RuntimeSourceCatalogSyncService,
188196 Effect.gen(function* () {
189197 const runtimeLocalScope = yield* RuntimeLocalScopeService;
198+ const pluginRegistry = yield* ExecutorPluginRegistryService;
190199 const scopeStateStore = yield* ScopeStateStore;
191200 const sourceArtifactStore = yield* SourceArtifactStore;
192201 const sourceTypeDeclarationsRefresher =
193202 yield* SourceTypeDeclarationsRefresherService;
194203 const deps: RuntimeSourceCatalogSyncDeps = {
204+ pluginRegistry,
195205 runtimeLocalScope,
196206 scopeStateStore,
197207 sourceArtifactStore,
packages/platform/sdk/src/runtime/execution/ir-execution.ts+8−1
@@ -13,6 +13,9 @@ import type { · 12 unmodified lines
1313 import {
1414 getSourceContribution,
1515 } from "../sources/source-plugins";
16+import type {
17+ ExecutorSdkPluginRegistry,
18+} from "../../plugins";
1619 import {
1720 runtimeEffectError,
1821 } from "../effect-errors";
@@ -49,6 +52,7 @@ export const invocationDescriptorFromTool = (input: { · 30 unmodified lines
4952 });
5053
5154 export const invokeIrTool = (input: {
55+ pluginRegistry: ExecutorSdkPluginRegistry;
5256 scopeId: Source["scopeId"];
5357 actorScopeId: ScopeId;
5458 tool: LoadedSourceCatalogToolIndexEntry;
@@ -56,7 +60,10 @@ export const invokeIrTool = (input: { · 1 unmodified line
5660 onElicitation?: OnElicitation;
5761 context?: Record<string, unknown>;
5862 }) => {
59− const definition = getSourceContribution(input.tool.executable.pluginKey);
63+ const definition = getSourceContribution(
64+ input.pluginRegistry,
65+ input.tool.executable.pluginKey,
66+ );
6067 if (definition.kind !== input.tool.source.kind) {
6168 return Effect.fail(
6269 runtimeEffectError("execution/ir-execution",
packages/platform/sdk/src/runtime/execution/scope/environment.ts+7−0
@@ -47,6 +47,9 @@ import { · 46 unmodified lines
4747 import {
4848 RuntimeSourceStoreService,
4949 } from "../../sources/source-store";
50+import {
51+ ExecutorPluginRegistryService,
52+} from "../../sources/source-plugins";
5053 import {
5154 SecretMaterialDeleterService,
5255 SecretMaterialResolverService,
@@ -71,6 +74,7 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 18 unmodified lines
7174
7275 export const createScopeExecutionEnvironmentResolver =
7376 (input: {
77+ pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
7478 executorStateStore: Effect.Effect.Success<typeof ExecutorStateStore>;
7579 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
7680 sourceCatalogSyncService: Effect.Effect.Success<
@@ -103,6 +107,7 @@ export const createScopeExecutionEnvironmentResolver = · 26 unmodified lines
103107 ? createEmptyLocalToolRuntime()
104108 : yield* input.localToolRuntimeLoader.load();
105109 const { catalog, toolInvoker } = createScopeToolInvoker({
110+ pluginRegistry: input.pluginRegistry,
106111 scopeId,
107112 actorScopeId,
108113 executorStateStore: input.executorStateStore,
@@ -147,6 +152,7 @@ export const RuntimeExecutionResolverLive = ( · 38 unmodified lines
147152 : Layer.effect(
148153 RuntimeExecutionResolverService,
149154 Effect.gen(function* () {
155+ const pluginRegistry = yield* ExecutorPluginRegistryService;
150156 const executorStateStore = yield* ExecutorStateStore;
151157 const sourceStore = yield* RuntimeSourceStoreService;
152158 const sourceCatalogSyncService =
@@ -163,6 +169,7 @@ export const RuntimeExecutionResolverLive = ( · 10 unmodified lines
163169 const updateSecretMaterial = yield* SecretMaterialUpdaterService;
164170
165171 return createScopeExecutionEnvironmentResolver({
172+ pluginRegistry,
166173 executorStateStore,
167174 sourceStore,
168175 sourceCatalogSyncService,
packages/platform/sdk/src/runtime/execution/scope/tool-invoker.ts+7−0
@@ -45,6 +45,9 @@ import type { · 44 unmodified lines
4545 import {
4646 type RuntimeSourceStore,
4747 } from "../../sources/source-store";
48+import type {
49+ ExecutorSdkPluginRegistry,
50+} from "../../../plugins";
4851 import {
4952 createExecutorToolMap,
5053 } from "../../sources/executor-tools";
@@ -73,6 +76,7 @@ import { · 22 unmodified lines
7376 import * as Layer from "effect/Layer";
7477
7578 export const createScopeToolInvoker = (input: {
79+ pluginRegistry: ExecutorSdkPluginRegistry;
7680 scopeId: Source["scopeId"];
7781 actorScopeId: ScopeId;
7882 executorStateStore: ExecutorStateStoreShape;
@@ -120,6 +124,7 @@ export const createScopeToolInvoker = (input: { · 41 unmodified lines
120124 ) => effect.pipe(Effect.provide(secretMaterialLayer));
121125
122126 const executorTools = createExecutorToolMap({
127+ pluginRegistry: input.pluginRegistry,
123128 scopeId: input.scopeId,
124129 actorScopeId: input.actorScopeId,
125130 executorStateStore: input.executorStateStore,
@@ -130,6 +135,7 @@ export const createScopeToolInvoker = (input: { · 4 unmodified lines
130135 scopeStateStore: input.scopeStateStore,
131136 sourceArtifactStore: input.sourceArtifactStore,
132137 runtimeLocalScope: input.runtimeLocalScope,
138+ secretMaterialServices: input.secretMaterialServices,
133139 });
134140 const sourceCatalog = createScopeSourceCatalog({
135141 scopeId: input.scopeId,
@@ -201,6 +207,7 @@ export const createScopeToolInvoker = (input: { · 65 unmodified lines
201207 });
202208
203209 return yield* invokeIrTool({
210+ pluginRegistry: input.pluginRegistry,
204211 scopeId: input.scopeId,
205212 actorScopeId: input.actorScopeId,
206213 tool: catalogTool,
packages/platform/sdk/src/runtime/index.ts+37−3
@@ -40,6 +40,9 @@ import type { · 39 unmodified lines
4040 import type {
4141 InstanceConfig,
4242 } from "../local/contracts";
43+import type {
44+ ExecutorSdkPluginRegistry,
45+} from "../plugins";
4346 import type {
4447 ExecutorScopeContext,
4548 ExecutorScopeDescriptor,
@@ -87,6 +90,10 @@ import { · 41 unmodified lines
8790 import {
8891 RuntimeSourceStoreLive,
8992 } from "./sources/source-store";
93+import {
94+ emptyExecutorPluginRegistry,
95+ ExecutorPluginRegistryLive,
96+} from "./sources/source-plugins";
9097
9198 export * from "./execution/state";
9299 export * from "./secret-stores";
@@ -106,6 +113,8 @@ export * from "./source-artifacts"; · 13 unmodified lines
106113 export * from "./scope-config";
107114 export * from "./scope-state";
108115 export * from "./scope/config-secrets";
116+export * from "./sources/source-store/config";
117+export * from "./sources/source-store/plugin-local-config";
109118 export {
110119 LocalInstanceConfigService,
111120 SecretMaterialDeleterService,
@@ -139,7 +148,10 @@ export type { · 27 unmodified lines
139148 export {
140149 registeredSourceContributions,
141150 registeredSecretStoreContributions,
151+ registeredManagementToolContributions,
142152 hasRegisteredExternalSourcePlugins,
153+ ExecutorPluginRegistryLive,
154+ ExecutorPluginRegistryService,
143155 getSourceContribution,
144156 getSourceContributionForSource,
145157 getSecretStoreContribution,
@@ -150,6 +162,7 @@ export type ExecutorRuntimeOptions = { · 4 unmodified lines
150162 executionResolver?: ResolveExecutionEnvironment;
151163 resolveSecretMaterial?: ResolveSecretMaterial;
152164 getLocalServerBaseUrl?: () => string | undefined;
165+ pluginRegistry?: ExecutorSdkPluginRegistry;
153166 };
154167
155168 type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;
@@ -185,7 +198,11 @@ export type BoundScopeStateStore = { · 29 unmodified lines
185198 };
186199
187200 export 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;
189206 read: (
190207 sourceId: string,
191208 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;
@@ -296,8 +313,14 @@ const toScopeStateStoreShape = ( · 104 unmodified lines
296313
297314 const toSourceArtifactStoreShape = (
298315 input: BoundSourceArtifactStore,
316+ pluginRegistry: ExecutorSdkPluginRegistry,
299317 ): SourceArtifactStoreShape => ({
300− build: input.build,
318+ build: ({ source, syncResult }) =>
319+ input.build({
320+ source,
321+ syncResult,
322+ pluginRegistry,
323+ }),
301324 read: ({ sourceId }) => input.read(sourceId),
302325 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),
303326 remove: ({ sourceId }) => input.remove(sourceId),
@@ -330,11 +353,16 @@ export const createExecutorRuntimeLayer = ( · 26 unmodified lines
330353 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;
331354 },
332355 ) => {
356+ const pluginRegistry =
357+ input.pluginRegistry ?? emptyExecutorPluginRegistry();
333358 const storageLayer = makeLocalStorageLayer({
334359 installationStore: toInstallationStoreShape(input.storage.installation),
335360 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),
336361 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),
337− sourceArtifactStore: toSourceArtifactStoreShape(input.storage.sourceArtifacts),
362+ sourceArtifactStore: toSourceArtifactStoreShape(
363+ input.storage.sourceArtifacts,
364+ pluginRegistry,
365+ ),
338366 });
339367 const localToolRuntimeLayer = Layer.succeed(
340368 LocalToolRuntimeLoaderService,
@@ -353,6 +381,7 @@ export const createExecutorRuntimeLayer = ( · 12 unmodified lines
353381
354382 const baseLayer = Layer.mergeAll(
355383 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),
384+ ExecutorPluginRegistryLive(pluginRegistry),
356385 RuntimeLocalScopeLive(input.localScopeState),
357386 storageLayer,
358387 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),
@@ -410,6 +439,7 @@ export type ExecutorRuntime = { · 51 unmodified lines
410439 scope: ExecutorScopeContext;
411440 storage: RuntimeStorageServices;
412441 localInstallation: LocalInstallation;
442+ pluginRegistry: ExecutorSdkPluginRegistry;
413443 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;
414444 runtimeLayer: ExecutorRuntimeLayer;
415445 close: () => Promise<void>;
@@ -425,6 +455,8 @@ export const createExecutorRuntimeFromServices = (input: { · 9 unmodified lines
425455 services: ExecutorRuntimeServices;
426456 } & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>
427457 (Effect.gen(function* () {
458+ const pluginRegistry =
459+ input.pluginRegistry ?? emptyExecutorPluginRegistry();
428460 const localInstallation = yield* input.services.storage.installation
429461 .getOrProvision()
430462 .pipe(Effect.mapError(toRuntimeBootstrapError));
@@ -457,6 +489,7 @@ export const createExecutorRuntimeFromServices = (input: { · 26 unmodified lines
457489 ),
458490 sourceArtifactStore: toSourceArtifactStoreShape(
459491 input.services.storage.sourceArtifacts,
492+ pluginRegistry,
460493 ),
461494 }),
462495 ),
@@ -499,6 +532,7 @@ export const createExecutorRuntimeFromServices = (input: { · 36 unmodified lines
499532 scope: runtimeWorkspace,
500533 storage: input.services.storage,
501534 localInstallation,
535+ pluginRegistry,
502536 managedRuntime,
503537 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,
504538 close: async () => {
packages/platform/sdk/src/runtime/source-artifacts.ts+9−1
@@ -27,6 +27,9 @@ import { · 26 unmodified lines
2727 createSourceCatalogRevisionRecord,
2828 stableSourceCatalogId,
2929 } from "./sources/source-definitions";
30+import type {
31+ ExecutorSdkPluginRegistry,
32+} from "../plugins";
3033
3134 const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const;
3235 const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const;
@@ -200,14 +203,19 @@ export const decodeStoredLocalSourceArtifact = ( · 167 unmodified lines
200203 export const buildLocalSourceArtifact = (input: {
201204 source: Source;
202205 syncResult: SourceCatalogSyncResult;
206+ pluginRegistry?: ExecutorSdkPluginRegistry;
203207 }): LocalSourceArtifact => {
204− const catalogId: SourceCatalogId = stableSourceCatalogId(input.source);
208+ const catalogId: SourceCatalogId = stableSourceCatalogId(
209+ input.source,
210+ input.pluginRegistry,
211+ );
205212 const generatedAt = Date.now();
206213 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);
207214 const importHash = importMetadataHash(snapshot);
208215 const hash = snapshotHash(snapshot);
209216 const revision = createSourceCatalogRevisionRecord({
210217 source: input.source,
218+ pluginRegistry: input.pluginRegistry,
211219 catalogId,
212220 revisionNumber: 1,
213221 importMetadataJson: JSON.stringify(snapshot.import),
packages/platform/sdk/src/runtime/sources/executor-tools.test.ts+290−0
@@ -0,0 +1,290 @@
1+import {
2+ createServer,
3+} from "node:http";
4+import type {
5+ AddressInfo,
6+} from "node:net";
7+import {
8+ FileSystem,
9+} from "@effect/platform";
10+import {
11+ NodeFileSystem,
12+} from "@effect/platform-node";
13+import {
14+ describe,
15+ expect,
16+ it,
17+} from "@effect/vitest";
18+import * as Effect from "effect/Effect";
19+
20+import {
21+ createLocalExecutorEffect,
22+} from "@executor/platform-sdk-file/effect";
23+import {
24+ ExecutionIdSchema,
25+} from "../../schema";
26+import {
27+ RuntimeExecutionResolverService,
28+} from "../execution/scope/environment";
29+import {
30+ openApiSdkPlugin,
31+} from "../../../../../../plugins/openapi/sdk";
32+
33+const 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+
58+const 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+
102+const 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+
118+describe("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 @@
11 import { toTool, type ToolMap } from "@executor/codemode-core";
22 import {
33 type ScopeId,
4+ SourceIdSchema,
45 SourceSchema,
56 type Source,
67 } from "#schema";
@@ -9,12 +10,16 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines
910 import * as Option from "effect/Option";
1011 import * as Schema from "effect/Schema";
1112
13+import type {
14+ ExecutorSdkPluginRegistry,
15+} from "../../plugins";
1216 import {
13− createManagedSourceRecord,
1417 getSource,
18+ listSources,
1519 refreshManagedSourceCatalog,
1620 removeSource,
1721 saveManagedSourceRecord,
22+ createManagedSourceRecord,
1823 } from "../../sources/operations";
1924 import {
2025 deriveSchemaJson,
@@ -31,23 +36,48 @@ import { · 10 unmodified lines
3136 provideOptionalRuntimeLocalScope,
3237 type RuntimeLocalScopeState,
3338 } from "../scope/runtime-context";
39+import {
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";
3449 import {
3550 type InstallationStoreShape,
3651 makeLocalStorageLayer,
37− type SourceArtifactStoreShape,
3852 type ScopeConfigStoreShape,
3953 type ScopeStateStoreShape,
54+ type SourceArtifactStoreShape,
4055 } from "../scope/storage";
56+import {
57+ registeredManagementToolContributions,
58+ registeredSourceContributions,
59+} from "./source-plugins";
4160 import {
4261 type RuntimeSourceStore,
4362 RuntimeSourceStoreService,
4463 } from "./source-store";
45−import {
46− registeredSourceContributions,
47−} from "./source-plugins";
4864
49−const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => {
50− const sources = registeredSourceContributions();
65+const SourceArraySchema = Schema.Array(SourceSchema);
66+const SourceIdInputSchema = Schema.Struct({
67+ sourceId: SourceIdSchema,
68+});
69+const SourceRemoveResultSchema = Schema.Struct({
70+ removed: Schema.Boolean,
71+});
72+
73+type RegisteredSourceContribution =
74+ ExecutorSdkPluginRegistry["sources"][number];
75+type RegisteredManagementToolContribution =
76+ ExecutorSdkPluginRegistry["managementTools"][number];
77+
78+const createExecutorSourcesAddSchema = (
79+ sources: ReadonlyArray<RegisteredSourceContribution>,
80+): Schema.Schema<any, any, never> => {
5181 if (sources.length === 0) {
5282 return Schema.Unknown;
5383 }
@@ -61,27 +91,78 @@ const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => { · 7 unmodified lines
6191 Schema.Schema<any, any, never>,
6292 Schema.Schema<any, any, never>,
6393 ...Array<Schema.Schema<any, any, never>>,
64− ])
94+ ]),
6595 );
6696 };
6797
68−export const getExecutorSourcesAddInputHint = (): string =>
69− deriveSchemaTypeSignature(createExecutorSourcesAddSchema(), 340);
98+export const getExecutorSourcesAddInputHint = (
99+ pluginRegistry: ExecutorSdkPluginRegistry,
100+): string =>
101+ deriveSchemaTypeSignature(
102+ createExecutorSourcesAddSchema(
103+ registeredSourceContributions(pluginRegistry),
104+ ),
105+ 340,
106+ );
70107
71108 export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature(
72109 SourceSchema,
73110 260,
74111 );
75112
76−export const getExecutorSourcesAddInputSchemaJson = (): Record<string, unknown> =>
77− deriveSchemaJson(createExecutorSourcesAddSchema()) ?? {};
113+export const getExecutorSourcesAddInputSchemaJson = (
114+ pluginRegistry: ExecutorSdkPluginRegistry,
115+): Record<string, unknown> =>
116+ deriveSchemaJson(
117+ createExecutorSourcesAddSchema(
118+ registeredSourceContributions(pluginRegistry),
119+ ),
120+ ) ?? {};
78121
79122 export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(
80123 SourceSchema,
81124 ) ?? {};
82125
83−export const getExecutorSourcesAddHelpLines = (): readonly string[] => {
84− const sources = registeredSourceContributions();
126+const coreSourceManagementHelpLines = (): readonly string[] => [
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+
133+export 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+
162+export const getExecutorSourcesAddHelpLines = (
163+ pluginRegistry: ExecutorSdkPluginRegistry,
164+): readonly string[] => {
165+ const sources = registeredSourceContributions(pluginRegistry);
85166 if (sources.length === 0) {
86167 return ["No source plugins are registered in this build."] as const;
87168 }
@@ -98,15 +179,18 @@ export const getExecutorSourcesAddHelpLines = (): readonly string[] => { · 10 unmodified lines
98179 ];
99180 };
100181
101−export const buildExecutorSourcesAddDescription = (): string => {
102− const sources = registeredSourceContributions();
182+export const buildExecutorSourcesAddDescription = (
183+ pluginRegistry: ExecutorSdkPluginRegistry,
184+): string => {
185+ const sources = registeredSourceContributions(pluginRegistry);
103186 if (sources.length === 0) {
104187 return "No source plugins are registered in this build.";
105188 }
106189
107190 return [
108− "Add a source using one of the registered source plugins.",
109− ...getExecutorSourcesAddHelpLines(),
191+ "Legacy compatibility alias for creating a source.",
192+ "Prefer plugin-specific tools such as executor.openapi.createSource or executor.mcp.createSource.",
193+ ...getExecutorSourcesAddHelpLines(pluginRegistry),
110194 ].join("\n");
111195 };
112196
@@ -153,7 +237,7 @@ const createSourceConnectorHost = (input: { · 40 unmodified lines
153237 },
154238 });
155239
156−const runExecutorSourceEffect = async <A>(
240+const runExecutorToolEffect = async <A>(
157241 effect: Effect.Effect<A, unknown, any>,
158242 input: {
159243 executorStateStore: ExecutorStateStoreShape;
@@ -166,6 +250,12 @@ const runExecutorSourceEffect = async <A>( · 6 unmodified lines
166250 scopeStateStore: ScopeStateStoreShape;
167251 sourceArtifactStore: SourceArtifactStoreShape;
168252 runtimeLocalScope: RuntimeLocalScopeState | null;
253+ secretMaterialServices: {
254+ resolve: ResolveSecretMaterial;
255+ store: StoreSecretMaterial;
256+ delete: DeleteSecretMaterial;
257+ update: UpdateSecretMaterial;
258+ };
169259 },
170260 ): Promise<A> => {
171261 const servicesLayer = Layer.mergeAll(
@@ -181,6 +271,22 @@ const runExecutorSourceEffect = async <A>( · 9 unmodified lines
181271 RuntimeSourceCatalogSyncService,
182272 input.sourceCatalogSyncService,
183273 ),
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+ ),
184290 );
185291
186292 return Effect.runPromise(
@@ -192,11 +298,11 @@ const runExecutorSourceEffect = async <A>( · 5 unmodified lines
192298 };
193299
194300 const resolveSourceContribution = (
195− sources: ReturnType<typeof registeredSourceContributions>,
301+ sources: ReadonlyArray<RegisteredSourceContribution>,
196302 args: unknown,
197303 ):
198304 | {
199− source: ReturnType<typeof registeredSourceContributions>[number];
305+ source: RegisteredSourceContribution;
200306 parsedArgs: unknown;
201307 }
202308 | null => {
@@ -216,7 +322,52 @@ const resolveSourceContribution = ( · 13 unmodified lines
216322 const toSerializableValue = <A>(value: A): A =>
217323 JSON.parse(JSON.stringify(value)) as A;
218324
325+const pluginRemoveToolPath = (
326+ source: RegisteredSourceContribution,
327+): `executor.${string}` =>
328+ `executor.${source.pluginKey}.removeSource`;
329+
330+const pluginRefreshToolPath = (
331+ source: RegisteredSourceContribution,
332+): `executor.${string}` =>
333+ `executor.${source.pluginKey}.refreshSource`;
334+
335+const findManagementTool = (
336+ managementTools: ReadonlyArray<RegisteredManagementToolContribution>,
337+ path: string,
338+): RegisteredManagementToolContribution | null =>
339+ managementTools.find((tool) => tool.path === path) ?? null;
340+
341+const 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+
219369 export const createExecutorToolMap = (input: {
370+ pluginRegistry: ExecutorSdkPluginRegistry;
220371 scopeId: ScopeId;
221372 actorScopeId: ScopeId;
222373 executorStateStore: ExecutorStateStoreShape;
@@ -229,22 +380,182 @@ export const createExecutorToolMap = (input: { · 6 unmodified lines
229380 scopeStateStore: ScopeStateStoreShape;
230381 sourceArtifactStore: SourceArtifactStoreShape;
231382 runtimeLocalScope: RuntimeLocalScopeState | null;
383+ secretMaterialServices: {
384+ resolve: ResolveSecretMaterial;
385+ store: StoreSecretMaterial;
386+ delete: DeleteSecretMaterial;
387+ update: UpdateSecretMaterial;
388+ };
232389 }): ToolMap => {
233− const sources = registeredSourceContributions();
234− if (sources.length === 0) {
235− return {};
236− }
237−
390+ const sources = registeredSourceContributions(input.pluginRegistry);
391+ const managementTools = registeredManagementToolContributions(
392+ input.pluginRegistry,
393+ );
238394 const host = createSourceConnectorHost({
239395 scopeId: input.scopeId,
240396 actorScopeId: input.actorScopeId,
241397 });
242398
243− return {
244− "executor.sources.add": toTool({
399+ const runtime = {
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({
245454 tool: {
246− description: buildExecutorSourcesAddDescription(),
247− inputSchema: Schema.standardSchemaV1(createExecutorSourcesAddSchema()),
455+ description: "Refresh catalog artifacts for a source by id.",
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+ ),
248559 outputSchema: Schema.standardSchemaV1(SourceSchema),
249560 execute: async (args: unknown): Promise<Source> => {
250561 const matched = resolveSourceContribution(sources, args);
@@ -254,17 +565,22 @@ export const createExecutorToolMap = (input: { · 3 unmodified lines
254565 );
255566 }
256567
257− const createdSource = await runExecutorSourceEffect(
568+ const createdSource = await runExecutorToolEffect(
258569 matched.source.createSource({
259570 args: matched.parsedArgs,
260571 host,
261572 }),
262− input,
573+ runtime,
263574 );
264575
265576 return toSerializableValue(createdSource);
266577 },
267578 },
268− }),
579+ });
580+ }
581+
582+ return {
583+ ...coreTools,
584+ ...pluginToolMap,
269585 };
270586 };
packages/platform/sdk/src/runtime/sources/source-definitions.ts+52−16
@@ -16,7 +16,9 @@ import { · 15 unmodified lines
1616 } from "#schema";
1717 import * as Effect from "effect/Effect";
1818
19−import { getSourceContributionForSource } from "./source-plugins";
19+import type {
20+ ExecutorSdkPluginRegistry,
21+} from "../../plugins";
2022
2123 const trimOrNull = (value: string | null | undefined): string | null => {
2224 if (value === null || value === undefined) {
@@ -29,29 +31,56 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 6 unmodified lines
2931
3032 type SourceCatalogSourceConfig = Record<string, unknown>;
3133
32−const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig =>
33− getSourceContributionForSource(source).catalogIdentity?.({
34+const getSourceContributionOption = (
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+
49+const sourceConfigFromSource = (
50+ source: Source,
51+ pluginRegistry?: ExecutorSdkPluginRegistry,
52+): SourceCatalogSourceConfig =>
53+ getSourceContributionOption(source, pluginRegistry)?.catalogIdentity?.({
3454 source,
3555 }) ?? {
3656 kind: source.kind,
3757 namespace: source.namespace,
3858 };
3959
40−const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind =>
41− getSourceContributionForSource(source).catalogKind;
60+const sourceCatalogKindFromSource = (
61+ source: Source,
62+ pluginRegistry?: ExecutorSdkPluginRegistry,
63+): SourceCatalogKind =>
64+ getSourceContributionOption(source, pluginRegistry)?.catalogKind ?? "imported";
4265
43−const sourceCatalogPluginKeyFromSource = (source: Source): SourceCatalogPluginKey =>
44− getSourceContributionForSource(source).kind;
66+const sourceCatalogPluginKeyFromSource = (
67+ source: Source,
68+ pluginRegistry?: ExecutorSdkPluginRegistry,
69+): SourceCatalogPluginKey =>
70+ getSourceContributionOption(source, pluginRegistry)?.kind ?? source.kind;
4571
4672 const stableHash = (value: string): string =>
4773 sha256Hex(value).slice(0, 24);
4874
49−const sourceCatalogSignature = (source: Source): string =>
75+const sourceCatalogSignature = (
76+ source: Source,
77+ pluginRegistry?: ExecutorSdkPluginRegistry,
78+): string =>
5079 JSON.stringify({
51− catalogKind: sourceCatalogKindFromSource(source),
52− pluginKey: sourceCatalogPluginKeyFromSource(source),
80+ catalogKind: sourceCatalogKindFromSource(source, pluginRegistry),
81+ pluginKey: sourceCatalogPluginKeyFromSource(source, pluginRegistry),
5382 sourceId: source.id,
54− sourceConfig: sourceConfigFromSource(source),
83+ sourceConfig: sourceConfigFromSource(source, pluginRegistry),
5584 });
5685
5786 export const sourceConfigSignature = (source: Source): string =>
@@ -64,8 +93,13 @@ export const sourceConfigSignature = (source: Source): string => · 6 unmodified lines
6493 updatedAt: source.updatedAt,
6594 });
6695
67−export const stableSourceCatalogId = (source: Source): SourceCatalogId =>
68− SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`);
96+export const stableSourceCatalogId = (
97+ source: Source,
98+ pluginRegistry?: ExecutorSdkPluginRegistry,
99+): SourceCatalogId =>
100+ SourceCatalogIdSchema.make(
101+ `src_catalog_${stableHash(sourceCatalogSignature(source, pluginRegistry))}`,
102+ );
69103
70104 export const stableSourceCatalogRevisionId = (
71105 source: Source,
@@ -109,12 +143,13 @@ export const normalizeSourceForSave = (input: { · 37 unmodified lines
109143
110144 export const createSourceCatalogRecord = (input: {
111145 source: Source;
146+ pluginRegistry?: ExecutorSdkPluginRegistry;
112147 catalogId?: SourceCatalogId | null;
113148 latestRevisionId: SourceCatalogRevisionId;
114149 }): StoredSourceCatalogRecord => ({
115− id: input.catalogId ?? stableSourceCatalogId(input.source),
116− kind: sourceCatalogKindFromSource(input.source),
117− pluginKey: sourceCatalogPluginKeyFromSource(input.source),
150+ id: input.catalogId ?? stableSourceCatalogId(input.source, input.pluginRegistry),
151+ kind: sourceCatalogKindFromSource(input.source, input.pluginRegistry),
152+ pluginKey: sourceCatalogPluginKeyFromSource(input.source, input.pluginRegistry),
118153 name: input.source.name,
119154 summary: null,
120155 visibility: "scope",
@@ -125,6 +160,7 @@ export const createSourceCatalogRecord = (input: { · 4 unmodified lines
125160
126161 export const createSourceCatalogRevisionRecord = (input: {
127162 source: Source;
163+ pluginRegistry?: ExecutorSdkPluginRegistry;
128164 catalogId: SourceCatalogId;
129165 catalogRevisionId?: SourceCatalogRevisionId | null;
130166 revisionNumber: number;
packages/platform/sdk/src/runtime/sources/source-plugins/index.ts+45−21
@@ -1,35 +1,59 @@
1+import * as Context from "effect/Context";
2+import * as Layer from "effect/Layer";
3+
14 import {
25 registerExecutorSdkPlugins,
36 type ExecutorSdkPlugin,
7+ type ExecutorSdkPluginRegistry,
48 } from "../../../plugins";
59
6−let configuredSourcePlugins: readonly ExecutorSdkPlugin<any, any>[] = [];
7−let registry = registerExecutorSdkPlugins(configuredSourcePlugins);
10+export class ExecutorPluginRegistryService extends Context.Tag(
11+ "#runtime/ExecutorPluginRegistryService",
12+)<ExecutorPluginRegistryService, ExecutorSdkPluginRegistry>() {}
13+
14+export const createExecutorPluginRegistry = (
15+ plugins: readonly ExecutorSdkPlugin<any, any>[] = [],
16+): ExecutorSdkPluginRegistry => registerExecutorSdkPlugins(plugins);
17+
18+export const emptyExecutorPluginRegistry = (): ExecutorSdkPluginRegistry =>
19+ createExecutorPluginRegistry();
20+
21+export const ExecutorPluginRegistryLive = (
22+ registry: ExecutorSdkPluginRegistry,
23+) => Layer.succeed(ExecutorPluginRegistryService, registry);
824
9−const refreshRegistry = () => {
10− registry = registerExecutorSdkPlugins(configuredSourcePlugins);
11−};
25+export const registeredSourceContributions = (
26+ registry: ExecutorSdkPluginRegistry,
27+) => registry.sources;
1228
13−export const configureExecutorSourcePlugins = (
14− plugins: readonly ExecutorSdkPlugin<any, any>[],
15−): void => {
16− configuredSourcePlugins = plugins;
17− refreshRegistry();
18−};
29+export const registeredSecretStoreContributions = (
30+ registry: ExecutorSdkPluginRegistry,
31+) => registry.secretStores;
1932
20−export const registeredSourceContributions = () => registry.sources;
21−export const registeredSecretStoreContributions = () => registry.secretStores;
33+export const registeredManagementToolContributions = (
34+ registry: ExecutorSdkPluginRegistry,
35+) => registry.managementTools;
36+
37+export const getSourceContribution = (
38+ registry: ExecutorSdkPluginRegistry,
39+ kind: string,
40+) => registry.getSourceContribution(kind);
2241
23−export const getSourceContribution = (kind: string) =>
24− registry.getSourceContribution(kind);
2542 export const getSourceContributionForSource = (
26− source: Parameters<typeof registry.getSourceContributionForSource>[0],
43+ registry: ExecutorSdkPluginRegistry,
44+ source: Parameters<ExecutorSdkPluginRegistry["getSourceContributionForSource"]>[0],
2745 ) => registry.getSourceContributionForSource(source);
28−export const getSecretStoreContribution = (kind: string) =>
29− registry.getSecretStoreContribution(kind);
46+
47+export const getSecretStoreContribution = (
48+ registry: ExecutorSdkPluginRegistry,
49+ kind: string,
50+) => registry.getSecretStoreContribution(kind);
51+
3052 export const getSecretStoreContributionForStore = (
31− store: Parameters<typeof registry.getSecretStoreContributionForStore>[0],
53+ registry: ExecutorSdkPluginRegistry,
54+ store: Parameters<ExecutorSdkPluginRegistry["getSecretStoreContributionForStore"]>[0],
3255 ) => registry.getSecretStoreContributionForStore(store);
3356
34−export const hasRegisteredExternalSourcePlugins = () =>
35− configuredSourcePlugins.length > 0;
57+export const hasRegisteredExternalSourcePlugins = (
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
3838 return SourceIdSchema.make(candidate);
3939 };
4040
41+export 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+
4153 export const configSourceFromLocalSource = (input: {
4254 source: Source;
55+ existingConfig?: LocalConfigSource | null;
4356 }): LocalConfigSource => {
4457 return {
58+ ...configSourceBaseFromLocalSource({
59+ source: input.source,
60+ }),
4561 kind: input.source.kind as LocalConfigSource["kind"],
46− ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
47− ? { name: input.source.name }
62+ ...(input.existingConfig?.config !== undefined
63+ ? {
64+ config: cloneJson(input.existingConfig.config),
65+ }
66+ : {}),
67+ ...(input.existingConfig?.connection !== undefined
68+ ? {
69+ connection: cloneJson(input.existingConfig.connection),
70+ }
4871 : {}),
49− ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)
50− ? { namespace: input.source.namespace ?? undefined }
72+ ...(input.existingConfig?.binding !== undefined
73+ ? {
74+ binding: cloneJson(input.existingConfig.binding),
75+ }
5176 : {}),
52− ...(input.source.enabled === false ? { enabled: false } : {}),
5377 } as LocalConfigSource;
5478 };
packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts+1−0
@@ -95,6 +95,7 @@ export const persistSourceWithDeps = ( · 94 unmodified lines
9595 };
9696 sources[nextSource.id] = configSourceFromLocalSource({
9797 source: nextSource,
98+ existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,
9899 });
99100 yield* localScope.scopeConfigStore.writeProject({
100101 config: {
packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts+123−0
@@ -0,0 +1,123 @@
1+import type {
2+ LocalConfigSecretInput,
3+ LocalConfigSource,
4+ LocalExecutorConfig,
5+ SecretRef,
6+ Source,
7+} from "#schema";
8+import {
9+ LocalConfigSecretInputSchema,
10+} from "#schema";
11+import * as Option from "effect/Option";
12+import * as Schema from "effect/Schema";
13+
14+import {
15+ toConfigSecretProviderId,
16+} from "../../scope/config-secrets";
17+import {
18+ cloneJson,
19+ configSourceBaseFromLocalSource,
20+ trimOrNull,
21+} from "./config";
22+
23+export 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+
42+export 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+
53+const decodeLocalConfigSecretInputOption = Schema.decodeUnknownOption(
54+ LocalConfigSecretInputSchema,
55+);
56+
57+const 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+
68+export 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+
96+export 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
5656 LocalConfigExplicitSecretRefSchema,
5757 );
5858
59+export const LocalConfigSourceConnectionSchema = Schema.Struct({
60+ endpoint: Schema.optional(Schema.NullOr(Schema.String)),
61+ auth: Schema.optional(Schema.Unknown),
62+});
63+
5964 const LocalConfigSourceEntryBaseSchema = Schema.Struct({
6065 name: Schema.optional(Schema.String),
6166 namespace: Schema.optional(Schema.String),
6267 enabled: Schema.optional(Schema.Boolean),
68+ config: Schema.optional(Schema.Unknown),
69+ connection: Schema.optional(LocalConfigSourceConnectionSchema),
70+ binding: Schema.optional(Schema.Unknown),
6371 });
6472
6573 export const LocalConfigSourceSchema = Schema.extend(
packages/platform/server/src/index.test.ts+225−1
@@ -35,7 +35,12 @@ import { · 34 unmodified lines
3535 writeLocalSourceArtifact,
3636 writeProjectLocalExecutorConfig,
3737 } from "@executor/platform-sdk-file/effect";
38−import { SourceIdSchema } from "@executor/platform-sdk/schema";
38+import {
39+ type LocalConfigSource,
40+ SourceCatalogIdSchema,
41+ SourceCatalogRevisionIdSchema,
42+ SourceIdSchema,
43+} from "@executor/platform-sdk/schema";
3944 import { googleDiscoveryHttpPlugin } from "@executor/plugin-google-discovery-http";
4045 import { graphqlHttpPlugin } from "@executor/plugin-graphql-http";
4146 import { mcpHttpPlugin } from "@executor/plugin-mcp-http";
@@ -56,6 +61,7 @@ import { · 14 unmodified lines
5661 contentHash,
5762 createCatalogImportMetadata,
5863 createSourceCatalogSyncResult,
64+ snapshotFromSourceCatalogSyncResult,
5965 } from "../../../sources/core/src/index";
6066 import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";
6167
@@ -258,6 +264,103 @@ const writeConfiguredLocalMcpSource = (input: { · 196 unmodified lines
258264 });
259265 }).pipe(Effect.provide(NodeFileSystem.layer));
260266
267+const 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+
290+const 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+
261364 const makeServer = createIsolatedLocalExecutorServer({
262365 port: 0,
263366 localDataDir: ":memory:",
@@ -1715,6 +1818,127 @@ describe("local-executor-server", () => { · 1451 unmodified lines
17151818 15_000,
17161819 );
17171820
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+
17181942 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>
17191943 Effect.gen(function* () {
17201944 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
10−EXECUTOR_TRACE_ENABLED=1 \
11−EXECUTOR_TRACE_SERVICE_NAME=executor-local \
12−EXECUTOR_TRACE_OTLP_ENDPOINT=http://127.0.0.1:4317 \
13−bun run dev
14−```
15−
16−- Query traces via:
17−
18−```bash
19−curl "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
99 GoogleDiscoveryDetailRoute,
1010 GoogleDiscoveryEditRoute,
1111 } from "./components";
12−export { getGoogleDiscoveryIconUrl } from "./icons";
12+export {
13+ getGoogleDiscoveryIconUrl,
14+ getGoogleDiscoveryIconUrlForService,
15+} from "./icons";
1316
1417 export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({
1518 key: GOOGLE_DISCOVERY_PLUGIN_KEY,
plugins/google-discovery/sdk/index.ts+427−15
@@ -1,15 +1,25 @@
11 import * as Effect from "effect/Effect";
2+import * as Option from "effect/Option";
23 import * as Schema from "effect/Schema";
34
45 import {
56 createCatalogImportMetadata,
67 createSourceCatalogSyncResult,
78 } from "@executor/source-core";
8−import type { Source } from "@executor/platform-sdk/schema";
9+import type {
10+ LocalExecutorConfig,
11+ Source,
12+} from "@executor/platform-sdk/schema";
13+import {
14+ SourceSchema,
15+} from "@executor/platform-sdk/schema";
916 import {
1017 defineExecutorSourcePlugin,
1118 } from "@executor/platform-sdk/plugins";
1219 import {
20+ createPluginLocalConfigEntrySchema,
21+ decodeCurrentOrLegacyLocalConfigAuth,
22+ pluginLocalConfigSourceFromConfig,
1323 SecretMaterialDeleterService,
1424 SecretMaterialResolverService,
1525 SecretMaterialStorerService,
@@ -21,11 +31,18 @@ import { · 5 unmodified lines
2131 GOOGLE_DISCOVERY_EXECUTOR_KEY,
2232 GOOGLE_DISCOVERY_SOURCE_KIND,
2333 GoogleDiscoveryBatchSourceInputSchema,
34+ GoogleDiscoveryConnectInputSchema,
2435 GoogleDiscoveryConnectionAuthSchema,
36+ GoogleDiscoveryOAuthPopupResultSchema,
2537 GoogleDiscoveryOAuthSessionSchema,
38+ GoogleDiscoverySourceConfigPayloadSchema,
39+ GoogleDiscoveryStartBatchOAuthInputSchema,
40+ GoogleDiscoveryStartOAuthInputSchema,
41+ GoogleDiscoveryStartOAuthResultSchema,
2642 defaultGoogleDiscoveryUrl,
2743 deriveGoogleDiscoveryNamespace,
2844 GoogleDiscoveryStoredSourceDataSchema,
45+ GoogleDiscoveryUpdateSourceInputSchema,
2946 type GoogleDiscoveryBatchSourceInput,
3047 type GoogleDiscoveryConnectInput,
3148 type GoogleDiscoveryConnectionAuth,
@@ -62,6 +79,16 @@ import { · 30 unmodified lines
6279 refreshOAuth2AccessToken,
6380 } from "./oauth2";
6481
82+const GoogleDiscoverySourceIdInputSchema = Schema.Struct({
83+ sourceId: Schema.String,
84+});
85+const 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+
6592 const decodeStoredSourceData = Schema.decodeUnknownSync(
6693 GoogleDiscoveryStoredSourceDataSchema,
6794 );
@@ -70,6 +97,34 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 2 unmodified lines
7097 );
7198 const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);
7299 const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);
100+const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(
101+ createPluginLocalConfigEntrySchema({
102+ kind: GOOGLE_DISCOVERY_SOURCE_KIND,
103+ config: GoogleDiscoveryStoredSourceDataSchema,
104+ }),
105+);
106+const 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+);
73128
74129 export type GoogleDiscoverySourceStorage = {
75130 get: (input: {
@@ -518,6 +573,44 @@ const googleDiscoveryConnectInputFromAddInput = ( · 442 unmodified lines
518573 auth: input.auth,
519574 });
520575
576+const 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+
521614 export const googleDiscoverySdkPlugin = (options: {
522615 storage: GoogleDiscoverySourceStorage;
523616 oauthSessions: GoogleDiscoveryOAuthSessionStorage;
@@ -575,6 +668,18 @@ export const googleDiscoverySdkPlugin = (options: { · 51 unmodified lines
575668 }
576669 }),
577670 },
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+ },
578683 catalog: {
579684 kind: "imported",
580685 identity: ({ source }) => ({
@@ -584,20 +689,10 @@ export const googleDiscoverySdkPlugin = (options: { · 3 unmodified lines
584689 sync: ({ source, stored }) =>
585690 Effect.gen(function* () {
586691 if (stored === null) {
587− return createSourceCatalogSyncResult({
588− fragment: {
589− version: "ir.v1.fragment",
590− },
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− });
692+ return yield* runtimeEffectError(
693+ "plugins/google-discovery/sdk",
694+ `Google Discovery source storage missing for ${source.id}`,
695+ );
601696 }
602697
603698 const headers = yield* resolveGoogleAuthHeaders({
@@ -725,6 +820,323 @@ export const googleDiscoverySdkPlugin = (options: { · 121 unmodified lines
725820 }),
726821 },
727822 },
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,
7281140 extendExecutor: ({ source, executor }) => {
7291141 const provideRuntime = <A>(
7301142 effect: Effect.Effect<A, Error, any>,
plugins/graphql/sdk/index.ts+125−15
@@ -4,25 +4,38 @@ import { · 3 unmodified lines
44 applyJsonBodyPlacements,
55 } from "@executor/codemode-core";
66 import * as Effect from "effect/Effect";
7+import * as Option from "effect/Option";
78 import * as Schema from "effect/Schema";
89
910 import {
1011 createCatalogImportMetadata,
1112 createSourceCatalogSyncResult,
1213 } from "@executor/source-core";
13−import type { Source } from "@executor/platform-sdk/schema";
14+import type {
15+ LocalExecutorConfig,
16+ Source,
17+} from "@executor/platform-sdk/schema";
18+import {
19+ SourceSchema,
20+} from "@executor/platform-sdk/schema";
1421 import {
1522 defineExecutorSourcePlugin,
1623 } from "@executor/platform-sdk/plugins";
1724 import {
1825 SecretMaterialResolverService,
26+ createPluginLocalConfigEntrySchema,
27+ decodeCurrentOrLegacyLocalConfigAuth,
28+ pluginLocalConfigSourceFromConfig,
1929 provideExecutorRuntime,
2030 runtimeEffectError,
2131 } from "@executor/platform-sdk/runtime";
2232 import {
2333 deriveGraphqlNamespace,
34+ GraphqlConnectInputSchema,
2435 GraphqlConnectionAuthSchema,
36+ GraphqlSourceConfigPayloadSchema,
2537 GraphqlStoredSourceDataSchema,
38+ GraphqlUpdateSourceInputSchema,
2639 type GraphqlConnectInput,
2740 type GraphqlSourceConfigPayload,
2841 type GraphqlStoredSourceData,
@@ -44,6 +57,10 @@ import { · 15 unmodified lines
4457 type GraphqlToolProviderData,
4558 } from "./provider-data";
4659
60+const GraphqlSourceIdInputSchema = Schema.Struct({
61+ sourceId: Schema.String,
62+});
63+
4764 const decodeStoredSourceData = Schema.decodeUnknownSync(GraphqlStoredSourceDataSchema);
4865 const decodeProviderData = Schema.decodeUnknownSync(GraphqlToolProviderDataSchema);
4966
@@ -134,6 +151,32 @@ const asStringRecord = (value: unknown): Record<string, string> => · 84 unmodified lines
134151 ),
135152 );
136153
154+const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(
155+ createPluginLocalConfigEntrySchema({
156+ kind: "graphql",
157+ config: GraphqlStoredSourceDataSchema,
158+ }),
159+);
160+
161+const 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+
137180 const withoutUndefinedEntries = (
138181 record: Readonly<Record<string, unknown>>,
139182 ): Record<string, unknown> =>
@@ -311,6 +354,41 @@ const graphqlConnectInputFromAddInput = ( · 171 unmodified lines
311354 auth: input.auth,
312355 });
313356
357+const 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+
314392 export const graphqlSdkPlugin = (options: {
315393 storage: GraphqlSourceStorage;
316394 }) => defineExecutorSourcePlugin<
@@ -363,6 +441,18 @@ export const graphqlSdkPlugin = (options: { · 46 unmodified lines
363441 toConfig: ({ source, stored }) =>
364442 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),
365443 },
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+ },
366456 catalog: {
367457 kind: "imported",
368458 identity: ({ source }) => ({
@@ -372,20 +462,10 @@ export const graphqlSdkPlugin = (options: { · 3 unmodified lines
372462 sync: ({ source, stored }) =>
373463 Effect.gen(function* () {
374464 if (stored === null) {
375− return createSourceCatalogSyncResult({
376− fragment: {
377− version: "ir.v1.fragment",
378− },
379− importMetadata: {
380− ...createCatalogImportMetadata({
381− source,
382− pluginKey: "graphql",
383− }),
384− importerVersion: "ir.v1.graphql",
385− sourceConfigHash: "missing",
386− },
387− sourceHash: null,
388− });
465+ return yield* runtimeEffectError(
466+ "plugins/graphql/sdk",
467+ `GraphQL source storage missing for ${source.id}`,
468+ );
389469 }
390470
391471 const normalizedStored = normalizeStoredSourceData(stored);
@@ -522,6 +602,36 @@ export const graphqlSdkPlugin = (options: { · 130 unmodified lines
522602 }),
523603 },
524604 },
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,
525635 extendExecutor: ({ source, executor }) => {
526636 const provideRuntime = <A>(
527637 effect: Effect.Effect<A, Error, any>,
plugins/mcp/sdk/index.ts+275−15
@@ -21,13 +21,21 @@ import { · 20 unmodified lines
2121 createSourceCatalogSyncResult,
2222 normalizeSourceDiscoveryUrl,
2323 probeHeadersFromAuth,
24+ SourceTransportSchema,
25+ StringArraySchema,
26+ StringMapSchema,
2427 type SourceDiscoveryResult,
2528 } from "@executor/source-core";
26−import type { Source } from "@executor/platform-sdk/schema";
29+import {
30+ type Source,
31+ SourceSchema,
32+} from "@executor/platform-sdk/schema";
2733 import {
2834 defineExecutorSourcePlugin,
2935 } from "@executor/platform-sdk/plugins";
3036 import {
37+ createPluginLocalConfigEntrySchema,
38+ pluginLocalConfigSourceFromConfig,
3139 SecretMaterialDeleterService,
3240 SecretMaterialResolverService,
3341 SecretMaterialStorerService,
@@ -36,9 +44,17 @@ import { · 2 unmodified lines
3644 runtimeEffectError,
3745 } from "@executor/platform-sdk/runtime";
3846 import {
47+ McpConnectInputSchema,
3948 McpConnectionAuthSchema,
49+ McpDiscoverInputSchema,
50+ McpDiscoverResultSchema,
51+ McpOAuthPopupResultSchema,
4052 McpOAuthSessionSchema,
53+ McpSourceConfigPayloadSchema,
54+ McpStartOAuthInputSchema,
55+ McpStartOAuthResultSchema,
4156 McpStoredSourceDataSchema,
57+ McpUpdateSourceInputSchema,
4258 deriveMcpNamespace,
4359 resolveMcpEndpoint,
4460 type McpConnectInput,
@@ -54,6 +70,7 @@ import { · 9 unmodified lines
5470 type McpUpdateSourceInput,
5571 } from "@executor/plugin-mcp-shared";
5672 import * as Effect from "effect/Effect";
73+import * as Option from "effect/Option";
5774 import * as Schema from "effect/Schema";
5875
5976 import {
@@ -83,6 +100,16 @@ import { · 23 unmodified lines
83100 const decodeStoredSourceData = Schema.decodeUnknownSync(McpStoredSourceDataSchema);
84101 const decodeSession = Schema.decodeUnknownSync(McpOAuthSessionSchema);
85102
103+const McpSourceIdInputSchema = Schema.Struct({
104+ sourceId: Schema.String,
105+});
106+const 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+
86113 const McpExecutableBindingSchema = Schema.Struct({
87114 toolId: Schema.String,
88115 toolName: Schema.String,
@@ -222,6 +249,38 @@ const normalizeStringArray = ( · 133 unmodified lines
222249 return normalized.length > 0 ? normalized : null;
223250 };
224251
252+const decodeMcpAuthOption = Schema.decodeUnknownOption(McpConnectionAuthSchema);
253+const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(
254+ createPluginLocalConfigEntrySchema({
255+ kind: "mcp",
256+ config: McpStoredSourceDataSchema,
257+ }),
258+);
259+const 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+
225284 const normalizeStoredSourceData = (
226285 input: McpConnectInput | McpSourceConfigPayload,
227286 ): Effect.Effect<McpStoredSourceData, Error, never> =>
@@ -332,6 +391,51 @@ const mcpConnectInputFromAddInput = ( · 104 unmodified lines
332391 auth: input.auth,
333392 });
334393
394+const 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+
335439 const mcpCatalogOperationFromManifestEntry = (input: {
336440 entry: McpToolManifestEntry;
337441 server: McpServerMetadata | null | undefined;
@@ -658,6 +762,15 @@ export const mcpSdkPlugin = ( · 320 unmodified lines
658762 }
659763 }),
660764 },
765+ localConfig: {
766+ toConfigSource: ({ source, stored }) =>
767+ pluginLocalConfigSourceFromConfig({
768+ source,
769+ config: stored,
770+ }),
771+ recoverStored: ({ config }) =>
772+ mcpStoredSourceDataFromLocalConfig(config),
773+ },
661774 catalog: {
662775 kind: "imported",
663776 identity: ({ source }) => ({
@@ -667,20 +780,10 @@ export const mcpSdkPlugin = ( · 3 unmodified lines
667780 sync: ({ source, stored }) =>
668781 Effect.gen(function* () {
669782 if (stored === null) {
670− return createSourceCatalogSyncResult({
671− fragment: {
672− version: "ir.v1.fragment",
673− },
674− importMetadata: {
675− ...createCatalogImportMetadata({
676− source,
677− pluginKey: "mcp",
678− }),
679− importerVersion: "ir.v1.mcp",
680− sourceConfigHash: "missing",
681− },
682− sourceHash: null,
683− });
783+ return yield* runtimeEffectError(
784+ "plugins/mcp/sdk",
785+ `MCP source storage missing for ${source.id}`,
786+ );
684787 }
685788
686789 const connector = yield* createStoredMcpConnector({
@@ -853,6 +956,163 @@ export const mcpSdkPlugin = ( · 166 unmodified lines
853956 }),
854957 },
855958 },
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,
8561116 extendExecutor: ({ source, executor }) => {
8571117 const provideRuntime = <A>(
8581118 effect: Effect.Effect<A, Error, any>,
plugins/openapi/sdk/index.ts+165−15
@@ -1,21 +1,36 @@
11 import { createHash } from "node:crypto";
22 import * as Effect from "effect/Effect";
3+import * as Option from "effect/Option";
34 import * as Schema from "effect/Schema";
45
56 import {
67 createCatalogImportMetadata,
78 createSourceCatalogSyncResult,
89 } from "@executor/source-core";
9−import type { Source } from "@executor/platform-sdk/schema";
10+import type {
11+ LocalExecutorConfig,
12+ Source,
13+} from "@executor/platform-sdk/schema";
14+import {
15+ SourceSchema,
16+} from "@executor/platform-sdk/schema";
1017 import {
1118 defineExecutorSourcePlugin,
1219 } from "@executor/platform-sdk/plugins";
1320 import {
21+ createPluginLocalConfigEntrySchema,
22+ decodeCurrentOrLegacyLocalConfigAuth,
23+ pluginLocalConfigSourceFromConfig,
1424 runtimeEffectError,
1525 SecretMaterialResolverService,
1626 } from "@executor/platform-sdk/runtime";
1727 import {
1828 OpenApiConnectionAuthSchema,
29+ OpenApiConnectInputSchema,
30+ OpenApiPreviewRequestSchema,
31+ OpenApiPreviewResponseSchema,
32+ OpenApiSourceConfigPayloadSchema,
33+ OpenApiUpdateSourceInputSchema,
1934 deriveOpenApiNamespace,
2035 previewOpenApiDocument,
2136 type OpenApiConnectInput,
@@ -46,6 +61,10 @@ import { · 24 unmodified lines
4661 type OpenApiToolProviderData,
4762 } from "./types";
4863
64+const OpenApiSourceIdInputSchema = Schema.Struct({
65+ sourceId: Schema.String,
66+});
67+
4968 const stableSourceHash = (value: OpenApiStoredSourceData): string =>
5069 createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);
5170
@@ -130,6 +149,40 @@ const normalizeStoredSourceData = ( · 78 unmodified lines
130149 auth: normalizeOpenApiAuth(stored.auth as Record<string, unknown>),
131150 });
132151
152+const 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+
159+const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(
160+ createPluginLocalConfigEntrySchema({
161+ kind: "openapi",
162+ config: OpenApiLocalSourceConfigSchema,
163+ }),
164+);
165+
166+const 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+
133186 const createStoredSourceData = (
134187 input: OpenApiConnectInput,
135188 ): OpenApiStoredSourceData => ({
@@ -158,6 +211,47 @@ const configFromStoredSourceData = ( · 22 unmodified lines
158211 auth: stored.auth,
159212 });
160213
214+const 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+
161255 const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);
162256
163257 const asRecord = (value: unknown): Record<string, unknown> =>
@@ -491,6 +585,23 @@ export const openApiSdkPlugin = ( · 327 unmodified lines
491585 toConfig: ({ source, stored }) =>
492586 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),
493587 },
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+ },
494605 catalog: {
495606 kind: "imported",
496607 identity: ({ source }) => ({
@@ -500,20 +611,10 @@ export const openApiSdkPlugin = ( · 3 unmodified lines
500611 sync: ({ source, stored }) =>
501612 Effect.gen(function* () {
502613 if (stored === null) {
503− return createSourceCatalogSyncResult({
504− fragment: {
505− version: "ir.v1.fragment",
506− },
507− importMetadata: {
508− ...createCatalogImportMetadata({
509− source,
510− pluginKey: "openapi",
511− }),
512− importerVersion: "ir.v1.openapi",
513− sourceConfigHash: "missing",
514− },
515− sourceHash: null,
516− });
614+ return yield* runtimeEffectError(
615+ "plugins/openapi/sdk",
616+ `OpenAPI source storage missing for ${source.id}`,
617+ );
517618 }
518619
519620 const normalizedStored = normalizeStoredSourceData(stored);
@@ -713,6 +814,55 @@ export const openApiSdkPlugin = ( · 193 unmodified lines
713814 }),
714815 },
715816 },
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,
716866 extendExecutor: ({ source, executor }) => {
717867 const provideRuntime = <A>(
718868 effect: Effect.Effect<A, Error, any>,
ralph.ts+0−22
@@ -1,22 +0,0 @@
1−import { $ } from "bun";
2−
3−let prompt = `
4−You'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−
6−We 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−
11−Plugins 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−
13−Feel free to autonomously test the app yourself via calling the API or running the local CLI
14−
15−You can take notes in agent-notes/
16−`
17−let output = ''
18−
19−do {
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−}