History

Add plugin source management tools

Rhys Sullivan committed 7 months ago

Commit 6dbf7b21 parent 2570fc732 files · +2875 −219Browse files

Unified · Split

32 files
  1. .executor/executor.jsonc +41 −0
  2. apps/executor/src/cli/main.ts +20 −6
  3. apps/web/src/components/source-favicon.tsx +88 −10
  4. apps/web/src/components/source-plugins-reset-state.tsx +1 −1
  5. apps/web/src/lib/source-favicon.ts +158 −0
  6. apps/web/src/plugins/index.ts +8 −2
  7. apps/web/src/plugins/pages.tsx +2 −2
  8. packages/clients/react/src/plugins/components/source-tool-workbench.tsx +17 −8
  9. packages/hosts/mcp/src/index.ts +8 −5
  10. packages/platform/sdk/src/executor-effect.ts +8 −9
  11. packages/platform/sdk/src/plugins.ts +313 −36
  12. packages/platform/sdk/src/runtime/catalog/source/reconcile.ts +26 −1
  13. packages/platform/sdk/src/runtime/catalog/source/sync.ts +11 −1
  14. packages/platform/sdk/src/runtime/execution/ir-execution.ts +8 −1
  15. packages/platform/sdk/src/runtime/execution/scope/environment.ts +7 −0
  16. packages/platform/sdk/src/runtime/execution/scope/tool-invoker.ts +7 −0
  17. packages/platform/sdk/src/runtime/index.ts +37 −3
  18. packages/platform/sdk/src/runtime/source-artifacts.ts +9 −1
  19. packages/platform/sdk/src/runtime/sources/executor-tools.test.ts +290 −0
  20. packages/platform/sdk/src/runtime/sources/executor-tools.ts +349 −33
  21. packages/platform/sdk/src/runtime/sources/source-definitions.ts +52 −16
  22. packages/platform/sdk/src/runtime/sources/source-plugins/index.ts +33 −17
  23. packages/platform/sdk/src/runtime/sources/source-store/config.ts +29 −5
  24. packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts +1 −0
  25. packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts +123 −0
  26. packages/platform/sdk/src/schema/models/local-config.ts +8 −0
  27. packages/platform/server/src/index.test.ts +225 −1
  28. plugins/google-discovery/react/index.ts +4 −1
  29. plugins/google-discovery/sdk/index.ts +427 −15
  30. plugins/graphql/sdk/index.ts +125 −15
  31. plugins/mcp/sdk/index.ts +275 −15
  32. plugins/openapi/sdk/index.ts +165 −15
.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+}
apps/executor/src/cli/main.ts+20−6
@@ -27,9 +27,12 @@ import { · 26 unmodified lines
2727 openApiSdkPlugin,
2828 } from "@executor/plugin-openapi-sdk";
2929 import {
30− getExecutorSourcesAddHelpLines,
30+ getExecutorInternalToolHelpLines,
3131 RuntimeExecutionResolverService,
3232 } from "@executor/platform-sdk/runtime";
33+import type {
34+ ExecutorSdkPluginRegistry,
35+} from "@executor/platform-sdk/plugins";
3336 import {
3437 createExecutorEffect,
3538 type ExecutorEffect as Executor,
@@ -217,7 +220,10 @@ const toEffectCliArgv = (args: readonly string[]): string[] => [ · 181 unmodified lines
217220 ...args,
218221 ];
219222
220−const buildWorkflowText = (namespaces: readonly string[] = []): string =>
223+const buildWorkflowText = (
224+ namespaces: readonly string[] = [],
225+ pluginRegistry?: ExecutorSdkPluginRegistry,
226+): string =>
221227 [
222228 "Execute TypeScript in sandbox; call tools via discovery workflow.",
223229 ...(namespaces.length > 0
@@ -231,12 +237,15 @@ const buildWorkflowText = (namespaces: readonly string[] = []): string => · 7 unmodified lines
231237 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
232238 "3) Call selected tools.<path>(input).",
233239 "4) Use source plugins to inspect or add API sources.",
234− ...getExecutorSourcesAddHelpLines(),
240+ ...(pluginRegistry
241+ ? getExecutorInternalToolHelpLines(pluginRegistry)
242+ : ["Use executor.* tools to inspect or manage API sources."]),
235243 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",
236244 "Do not use fetch; use tools.* only.",
237245 ].join("\n");
238246
239−const getDefaultRunWorkflow = () => buildWorkflowText();
247+const getDefaultRunWorkflow = (pluginRegistry?: ExecutorSdkPluginRegistry) =>
248+ buildWorkflowText([], pluginRegistry);
240249
241250 const indentBlock = (value: string, prefix: string = " "): string =>
242251 value
@@ -279,9 +288,10 @@ const closeExecutor = (executor: Executor) => · 36 unmodified lines
279288
280289 const buildRunWorkflowText = (
281290 catalog?: ToolCatalog,
291+ pluginRegistry?: ExecutorSdkPluginRegistry,
282292 ): Effect.Effect<string, Error, never> => {
283293 if (!catalog) {
284− return Effect.succeed(getDefaultRunWorkflow());
294+ return Effect.succeed(getDefaultRunWorkflow(pluginRegistry));
285295 }
286296
287297 return catalog.listNamespaces({ limit: 200 }).pipe(
@@ -290,6 +300,7 @@ const buildRunWorkflowText = ( · 2 unmodified lines
290300 namespaces.length > 0
291301 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)
292302 : ["none discovered yet"],
303+ pluginRegistry,
293304 )
294305 ),
295306 Effect.mapError(toError),
@@ -345,7 +356,10 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 49 unmodified lines
345356 Effect.mapError(toError),
346357 );
347358
348− return yield* buildRunWorkflowText(environment.catalog);
359+ return yield* buildRunWorkflowText(
360+ environment.catalog,
361+ executor.runtime.pluginRegistry,
362+ );
349363 }),
350364 closeExecutor,
351365 ).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 max-w-[300px]">
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−2
@@ -23,6 +23,7 @@ import { · 22 unmodified lines
2323 registerExecutorFrontendPlugins,
2424 type ExecutorFrontendPlugin,
2525 } from "@executor/react/plugins";
26+import { getFallbackSourceFaviconUrl } from "../lib/source-favicon";
2627
2728 const frontendPlugins = [
2829 ExecutionHistoryReactPlugin,
@@ -77,7 +78,12 @@ export const getSourceFrontendPaths = (kind: string) => { · 48 unmodified lines
7778 return plugin ? createSourcePluginPaths(plugin.key) : null;
7879 };
7980
81+const isGoogleDiscoverySource = (kind: string): boolean =>
82+ kind === "google_discovery"
83+ || kind === "google-discovery";
84+
8085 export const getSourceFrontendIconUrl = (source: Source) =>
81− source.kind === "google-discovery"
86+ (isGoogleDiscoverySource(source.kind)
8287 ? getGoogleDiscoveryIconUrl(source)
83− : null;
88+ : null)
89+ ?? getFallbackSourceFaviconUrl(source);
apps/web/src/plugins/pages.tsx+2−2
@@ -9,7 +9,7 @@ import { · 8 unmodified lines
99 type FrontendPluginRouteSearch,
1010 } from "@executor/react/plugins";
1111
12−import { DefaultSourceIcon } from "../components/source-favicon";
12+import { SourcePresetFavicon } from "../components/source-favicon";
1313 import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
1414 import { SourcePluginsResetState } from "../components/source-plugins-reset-state";
1515 import {
@@ -201,7 +201,7 @@ export function SourcePluginsIndexPage() { · 185 unmodified lines
201201 >
202202 <div className="mb-2 flex items-center gap-2.5">
203203 <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
204− <DefaultSourceIcon kind={preset.kind} className="size-4" />
204+ <SourcePresetFavicon preset={preset} className="size-4" />
205205 </div>
206206 <div className="min-w-0">
207207 <div className="truncate text-sm font-semibold text-foreground">
packages/clients/react/src/plugins/components/source-tool-workbench.tsx+17−8
@@ -298,12 +298,18 @@ const SourceToolTreeNodeView = (props: { · 297 unmodified lines
298298 <button
299299 type="button"
300300 onClick={() => setOpen((value) => !value)}
301+ aria-expanded={open}
301302 className="shrink-0 rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"
302303 style={{ marginLeft: paddingLeft }}
303304 >
304305 <IconChevron
305− className={cn("shrink-0 transition-transform duration-150", open && "rotate-90")}
306− style={{ width: 8, height: 8 }}
306+ className="shrink-0 transition-transform duration-150"
307+ style={{
308+ width: 8,
309+ height: 8,
310+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
311+ transformOrigin: "50% 50%",
312+ }}
307313 />
308314 </button>
309315 <SourceToolListItem
@@ -321,6 +327,7 @@ const SourceToolTreeNodeView = (props: { · 11 unmodified lines
321327 <button
322328 type="button"
323329 onClick={() => setOpen((value) => !value)}
330+ aria-expanded={open}
324331 className={cn(
325332 "group flex w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] transition-colors hover:bg-accent/40",
326333 open ? "text-foreground/80" : "text-muted-foreground/60",
@@ -328,11 +335,13 @@ const SourceToolTreeNodeView = (props: { · 1 unmodified line
328335 style={{ paddingLeft }}
329336 >
330337 <IconChevron
331− className={cn(
332− "shrink-0 text-muted-foreground/30 transition-transform duration-150",
333− open && "rotate-90",
334− )}
335− style={{ width: 8, height: 8 }}
338+ className="shrink-0 text-muted-foreground/30 transition-transform duration-150"
339+ style={{
340+ width: 8,
341+ height: 8,
342+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
343+ transformOrigin: "50% 50%",
344+ }}
336345 />
337346 <IconFolder
338347 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}
@@ -622,7 +631,7 @@ export const SourceToolModelWorkbench = ( · 283 unmodified lines
622631 </div>
623632 </div>
624633
625− <div className="flex-1 overflow-y-auto max-w-[300px]">
634+ <div className="flex-1 overflow-y-auto w-[300px]">
626635 {filteredTools.length === 0 ? (
627636 <div className="p-4 text-center text-[13px] text-muted-foreground/50">
628637 {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
@@ -61,15 +61,13 @@ import { · 60 unmodified lines
6161 listExecutions,
6262 resumeExecution,
6363 } from "./runtime/execution/service";
64−import type {
65− ExecutorSdkPluginHost,
66− ExecutorSdkPlugin,
67− ExecutorSdkPluginExtensions,
68− PluginCleanup,
69−} from "./plugins";
7064 import {
71− configureExecutorSourcePlugins,
72−} from "./runtime/sources/source-plugins";
65+ registerExecutorSdkPlugins,
66+ type ExecutorSdkPluginHost,
67+ type ExecutorSdkPlugin,
68+ type ExecutorSdkPluginExtensions,
69+ type PluginCleanup,
70+} from "./plugins";
7371
7472 type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<
7573 Effect.Effect.Success<T>,
@@ -263,13 +261,14 @@ export const createExecutorEffect = < · 187 unmodified lines
263261 >(
264262 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,
265263 ): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {
266− configureExecutorSourcePlugins(options.plugins ?? []);
264+ const pluginRegistry = registerExecutorSdkPlugins(options.plugins ?? []);
267265
268266 return Effect.flatMap(
269267 options.backend.createRuntime({
270268 executionResolver: options.executionResolver,
271269 resolveSecretMaterial: options.resolveSecretMaterial,
272270 getLocalServerBaseUrl: options.getLocalServerBaseUrl,
271+ pluginRegistry,
273272 }),
274273 (runtime) =>
275274 Effect.gen(function* () {
packages/platform/sdk/src/plugins.ts+313−36
@@ -6,19 +6,24 @@ 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 { Source as ExecutorSource } from "./schema";
9+import {
10+ type LocalConfigSource,
11+ type LocalExecutorConfig,
12+ type Source as ExecutorSource,
13+} from "./schema";
1014 import type {
1115 SourceInvokeInput,
1216 SourceInvokeResult,
1317 } from "@executor/source-core";
1418 import type * as Schema from "effect/Schema";
1519 import { runtimeEffectError } from "./runtime/effect-errors";
20+import { ScopeConfigStore } from "./runtime/scope/storage";
1621
1722 export type PluginCleanup = {
1823 close: () => void | Promise<void>;
1924 };
2025
21−export type ExecutorSdkPluginHost = {};
26+export type ExecutorSdkPluginHost = ExecutorSourcePluginInternalHost;
2227
2328 export type ExecutorSdkPluginContext = {
2429 executor: ExecutorEffect & Record<string, unknown>;
@@ -34,6 +39,7 @@ export type ExecutorSdkPluginStartContext< · 9 unmodified lines
3439
3540 type ExecutorSdkPluginInternals = {
3641 sources?: readonly ExecutorSourceContribution<any>[];
42+ managementTools?: readonly ExecutorManagementToolContribution<any, any>[];
3743 };
3844
3945 const executorSdkPluginInternalsSymbol = Symbol.for(
@@ -122,6 +128,45 @@ export type ExecutorSourcePluginApi< · 82 unmodified lines
122128 ) => Effect.Effect<boolean, Error, any>;
123129 };
124130
131+export type ExecutorManagementToolContribution<
132+ TInput = unknown,
133+ TOutput = unknown,
134+> = {
135+ path: `executor.${string}`;
136+ description: string;
137+ inputSchema: Schema.Schema<TInput, any, never>;
138+ outputSchema: Schema.Schema<TOutput, any, never>;
139+ execute: (input: {
140+ args: TInput;
141+ host: ExecutorSdkPluginHost;
142+ }) => Effect.Effect<TOutput, Error, any>;
143+};
144+
145+export type ExecutorSourcePluginManagementToolDefinition<
146+ TName extends string = string,
147+ TInput = unknown,
148+ TOutput = unknown,
149+ TConnectInput = unknown,
150+ TSourceConfig = unknown,
151+ TUpdateInput extends {
152+ sourceId: string;
153+ config: TSourceConfig;
154+ } = {
155+ sourceId: string;
156+ config: TSourceConfig;
157+ },
158+> = {
159+ name: TName;
160+ description: string;
161+ inputSchema: Schema.Schema<TInput, any, never>;
162+ outputSchema: Schema.Schema<TOutput, any, never>;
163+ execute: (input: {
164+ args: TInput;
165+ source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
166+ host: ExecutorSourcePluginInternalHost;
167+ }) => Effect.Effect<TOutput, Error, any>;
168+};
169+
125170 export type ExecutorSourcePluginDefinition<
126171 TAddInput,
127172 TConnectInput,
@@ -165,6 +210,17 @@ export type ExecutorSourcePluginDefinition< · 37 unmodified lines
165210 stored: TStored | null;
166211 }) => Effect.Effect<void, Error, any>;
167212 };
213+ localConfig?: {
214+ toConfigSource: (input: {
215+ source: ExecutorSource;
216+ stored: TStored;
217+ }) => LocalConfigSource;
218+ recoverStored: (input: {
219+ source: ExecutorSource;
220+ config: LocalConfigSource;
221+ loadedConfig: LocalExecutorConfig | null;
222+ }) => Effect.Effect<TStored, Error, any> | TStored;
223+ };
168224 catalog: {
169225 kind: SourceCatalogKind;
170226 identity?: (input: {
@@ -206,6 +262,14 @@ export type ExecutorSourcePluginInput< · 35 unmodified lines
206262 TStored,
207263 TUpdateInput
208264 >;
265+ tools?: readonly ExecutorSourcePluginManagementToolDefinition<
266+ string,
267+ any,
268+ any,
269+ TConnectInput,
270+ TSourceConfig,
271+ TUpdateInput
272+ >[];
209273 extendExecutor?: (input: ExecutorSdkPluginContext & {
210274 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
211275 }) => TExtension;
@@ -239,6 +303,121 @@ const loadSourceOfKind = ( · 27 unmodified lines
239303 return source;
240304 });
241305
306+const cloneJson = <T>(value: T): T =>
307+ JSON.parse(JSON.stringify(value)) as T;
308+
309+const fromMaybeEffect = <A>(
310+ value: Effect.Effect<A, Error, any> | A,
311+): Effect.Effect<A, Error, any> =>
312+ Effect.isEffect(value)
313+ ? value
314+ : Effect.succeed(value);
315+
316+const persistSourceLocalConfig = <
317+ TAddInput,
318+ TConnectInput,
319+ TSourceConfig,
320+ TStored,
321+ TUpdateInput extends {
322+ sourceId: string;
323+ config: TSourceConfig;
324+ },
325+>(
326+ definition: ExecutorSourcePluginDefinition<
327+ TAddInput,
328+ TConnectInput,
329+ TSourceConfig,
330+ TStored,
331+ TUpdateInput
332+ >,
333+ input: {
334+ source: ExecutorSource;
335+ stored: TStored;
336+ },
337+): Effect.Effect<void, Error, ScopeConfigStore> =>
338+ definition.localConfig
339+ ? Effect.gen(function* () {
340+ const localConfig = definition.localConfig!;
341+ const scopeConfigStore = yield* ScopeConfigStore;
342+ const loadedConfig = yield* scopeConfigStore.load();
343+ const projectConfig = cloneJson(loadedConfig.projectConfig ?? {});
344+ const sources = {
345+ ...projectConfig.sources,
346+ [input.source.id]: localConfig.toConfigSource({
347+ source: input.source,
348+ stored: input.stored,
349+ }),
350+ };
351+
352+ yield* scopeConfigStore.writeProject({
353+ config: {
354+ ...projectConfig,
355+ sources,
356+ },
357+ });
358+ })
359+ : Effect.void;
360+
361+const loadStoredOrRecover = <
362+ TAddInput,
363+ TConnectInput,
364+ TSourceConfig,
365+ TStored,
366+ TUpdateInput extends {
367+ sourceId: string;
368+ config: TSourceConfig;
369+ },
370+>(
371+ definition: ExecutorSourcePluginDefinition<
372+ TAddInput,
373+ TConnectInput,
374+ TSourceConfig,
375+ TStored,
376+ TUpdateInput
377+ >,
378+ source: ExecutorSource,
379+): Effect.Effect<TStored | null, Error, ScopeConfigStore> =>
380+ Effect.gen(function* () {
381+ const localConfig = definition.localConfig;
382+ const stored = yield* definition.storage.get({
383+ scopeId: source.scopeId,
384+ sourceId: source.id,
385+ });
386+ if (stored !== null || localConfig === undefined) {
387+ return stored;
388+ }
389+
390+ const scopeConfigStore = yield* ScopeConfigStore;
391+ const loadedConfig = yield* scopeConfigStore.load();
392+ const config = loadedConfig.config?.sources?.[source.id] ?? null;
393+ if (config === null) {
394+ return null;
395+ }
396+
397+ if (config.kind !== definition.kind) {
398+ return yield* runtimeEffectError(
399+ "plugins",
400+ `Source ${source.id} config kind ${config.kind} does not match ${definition.kind}.`,
401+ );
402+ }
403+
404+ const recovered = yield* fromMaybeEffect(
405+ localConfig.recoverStored({
406+ source,
407+ config,
408+ loadedConfig: loadedConfig.config,
409+ }),
410+ );
411+
412+ yield* definition.storage.put({
413+ scopeId: source.scopeId,
414+ sourceId: source.id,
415+ value: recovered,
416+ });
417+
418+ return recovered;
419+ });
420+
242421 const createExecutorSourcePluginApi = <
243422 TAddInput,
244423 TConnectInput,
@@ -269,10 +448,7 @@ const createExecutorSourcePluginApi = < · 24 unmodified lines
269448 definition,
270449 host,
271450 });
272− const stored = yield* definition.storage.get({
273− scopeId: source.scopeId,
274− sourceId: source.id,
275− });
451+ const stored = yield* loadStoredOrRecover(definition, source);
276452 if (stored === null) {
277453 return yield* runtimeEffectError(
278454 "plugins",
@@ -297,6 +473,10 @@ const createExecutorSourcePluginApi = < · 18 unmodified lines
297473 sourceId: source.id,
298474 value: created.stored,
299475 });
476+ yield* persistSourceLocalConfig(definition, {
477+ source,
478+ stored: created.stored,
479+ });
300480
301481 return yield* host.sources.refreshCatalog(source.id);
302482 }),
@@ -317,6 +497,10 @@ const createExecutorSourcePluginApi = < · 14 unmodified lines
317497 sourceId: saved.id,
318498 value: updated.stored,
319499 });
500+ yield* persistSourceLocalConfig(definition, {
501+ source: saved,
502+ stored: updated.stored,
503+ });
320504
321505 return yield* host.sources.refreshCatalog(saved.id);
322506 }),
@@ -359,6 +543,7 @@ const createExecutorSourcePluginApi = < · 36 unmodified lines
359543 });
360544
361545 type ExecutorSourceContribution<TInput = unknown> = {
546+ pluginKey: string;
362547 kind: string;
363548 displayName: string;
364549 inputSchema: Schema.Schema<TInput, any, never>;
@@ -382,7 +567,21 @@ type ExecutorSourceContribution<TInput = unknown> = { · 17 unmodified lines
382567 ) => Effect.Effect<SourceInvokeResult, Error, any>;
383568 };
384569
570+export type ExecutorSdkPluginRegistry = {
571+ plugins: readonly ExecutorSdkPlugin<any, any>[];
572+ sources: readonly ExecutorSourceContribution<any>[];
573+ managementTools: readonly ExecutorManagementToolContribution<any, any>[];
574+ getSourceContribution: (kind: string) => ExecutorSourceContribution<any>;
575+ getSourceContributionForSource: (
576+ source: Pick<ExecutorSource, "kind">,
577+ ) => ExecutorSourceContribution<any>;
578+ getManagementTool: (
579+ path: string,
580+ ) => ExecutorManagementToolContribution<any, any>;
581+};
582+
385583 const createExecutorSourceContribution = <
584+ TKey extends string,
386585 TAddInput,
387586 TConnectInput,
388587 TSourceConfig,
@@ -392,51 +591,96 @@ const createExecutorSourceContribution = < · 3 unmodified lines
392591 config: TSourceConfig;
393592 },
394593 >(
395− definition: ExecutorSourcePluginDefinition<
396− TAddInput,
397− TConnectInput,
398− TSourceConfig,
399− TStored,
400− TUpdateInput
401− >,
594+ input: {
595+ key: TKey;
596+ source: ExecutorSourcePluginDefinition<
597+ TAddInput,
598+ TConnectInput,
599+ TSourceConfig,
600+ TStored,
601+ TUpdateInput
602+ >;
603+ },
402604 ): ExecutorSourceContribution<TAddInput> => ({
403− kind: definition.kind,
404− displayName: definition.displayName,
405− inputSchema: definition.add.inputSchema,
406− inputSignatureWidth: definition.add.inputSignatureWidth,
407− helpText: definition.add.helpText,
408− catalogKind: definition.catalog.kind,
409− catalogIdentity: definition.catalog.identity,
605+ pluginKey: input.key,
606+ kind: input.source.kind,
607+ displayName: input.source.displayName,
608+ inputSchema: input.source.add.inputSchema,
609+ inputSignatureWidth: input.source.add.inputSignatureWidth,
610+ helpText: input.source.add.helpText,
611+ catalogKind: input.source.catalog.kind,
612+ catalogIdentity: input.source.catalog.identity,
410613 createSource: ({ args, host }) =>
411− createExecutorSourcePluginApi(definition, host).createSource(
412− definition.add.toConnectInput(args),
614+ createExecutorSourcePluginApi(input.source, host).createSource(
615+ input.source.add.toConnectInput(args),
413616 ),
414617 syncCatalog: ({ source }) =>
415618 Effect.flatMap(
416− definition.storage.get({
417− scopeId: source.scopeId,
418− sourceId: source.id,
419− }),
619+ loadStoredOrRecover(input.source, source),
420620 (stored) =>
421− definition.catalog.sync({
621+ input.source.catalog.sync({
422622 source,
423623 stored,
424624 }),
425625 ),
426− invoke: (input) =>
626+ invoke: (invokeInput) =>
427627 Effect.flatMap(
428− definition.storage.get({
429− scopeId: input.source.scopeId,
430− sourceId: input.source.id,
431− }),
628+ loadStoredOrRecover(input.source, invokeInput.source),
432629 (stored) =>
433− definition.catalog.invoke({
434− ...input,
630+ input.source.catalog.invoke({
631+ ...invokeInput,
435632 stored,
436633 }),
437634 ),
438635 });
439636
637+const createExecutorSourceManagementToolContribution = <
638+ TKey extends string,
639+ TName extends string,
640+ TInput,
641+ TOutput,
642+ TConnectInput,
643+ TSourceConfig,
644+ TStored,
645+ TUpdateInput extends {
646+ sourceId: string;
647+ config: TSourceConfig;
648+ },
649+>(
650+ input: {
651+ key: TKey;
652+ source: ExecutorSourcePluginDefinition<
653+ any,
654+ TConnectInput,
655+ TSourceConfig,
656+ TStored,
657+ TUpdateInput
658+ >;
659+ tool: ExecutorSourcePluginManagementToolDefinition<
660+ TName,
661+ TInput,
662+ TOutput,
663+ TConnectInput,
664+ TSourceConfig,
665+ TUpdateInput
666+ >;
667+ },
668+): ExecutorManagementToolContribution<TInput, TOutput> => ({
669+ path: `executor.${input.key}.${input.tool.name}`,
670+ description: input.tool.description,
671+ inputSchema: input.tool.inputSchema,
672+ outputSchema: input.tool.outputSchema,
673+ execute: ({ args, host }) =>
674+ input.tool.execute({
675+ args,
676+ host: host as ExecutorSourcePluginInternalHost,
677+ source: createExecutorSourcePluginApi(
678+ input.source,
679+ host as ExecutorSourcePluginInternalHost,
680+ ),
681+ }),
682+});
683+
440684 export const defineExecutorSourcePlugin = <
441685 const TKey extends string,
442686 TAddInput,
@@ -483,7 +727,17 @@ export const defineExecutorSourcePlugin = < · 40 unmodified lines
483727 })
484728 : undefined,
485729 [executorSdkPluginInternalsSymbol]: {
486− sources: [createExecutorSourceContribution(input.source)],
730+ sources: [createExecutorSourceContribution({
731+ key: input.key,
732+ source: input.source,
733+ })],
734+ managementTools: (input.tools ?? []).map((tool) =>
735+ createExecutorSourceManagementToolContribution({
736+ key: input.key,
737+ source: input.source,
738+ tool,
739+ })
740+ ),
487741 },
488742 }))(input.extendExecutor, input.start);
489743
@@ -501,6 +755,10 @@ export const registerExecutorSdkPlugins = ( · 11 unmodified lines
501755 ) => {
502756 const pluginKeys = new Set<string>();
503757 const sources = new Map<string, ExecutorSourceContribution<any>>();
758+ const managementTools = new Map<
759+ string,
760+ ExecutorManagementToolContribution<any, any>
761+ >();
504762
505763 for (const plugin of plugins) {
506764 if (pluginKeys.has(plugin.key)) {
@@ -520,6 +778,14 @@ export const registerExecutorSdkPlugins = ( · 13 unmodified lines
520778
521779 sources.set(source.kind, source);
522780 }
781+
782+ for (const tool of internals?.managementTools ?? []) {
783+ if (managementTools.has(tool.path)) {
784+ throw new Error(`Duplicate executor management tool: ${tool.path}`);
785+ }
786+
787+ managementTools.set(tool.path, tool);
788+ }
523789 }
524790
525791 const getSourceContribution = (kind: string) => {
@@ -534,10 +800,21 @@ export const registerExecutorSdkPlugins = ( · 8 unmodified lines
534800 const getSourceContributionForSource = (source: Pick<ExecutorSource, "kind">) =>
535801 getSourceContribution(source.kind);
536802
803+ const getManagementTool = (path: string) => {
804+ const tool = managementTools.get(path);
805+ if (!tool) {
806+ throw new Error(`Unsupported executor management tool: ${path}`);
807+ }
808+
809+ return tool;
810+ };
811+
537812 return {
538813 plugins,
539814 sources: [...sources.values()],
815+ managementTools: [...managementTools.values()],
540816 getSourceContribution,
541817 getSourceContributionForSource,
542− };
818+ getManagementTool,
819+ } satisfies ExecutorSdkPluginRegistry;
543820 };
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";
@@ -70,6 +73,7 @@ import { · 19 unmodified lines
7073 import * as Layer from "effect/Layer";
7174
7275 export const createScopeToolInvoker = (input: {
76+ pluginRegistry: ExecutorSdkPluginRegistry;
7377 scopeId: Source["scopeId"];
7478 actorScopeId: ScopeId;
7579 executorStateStore: ExecutorStateStoreShape;
@@ -117,6 +121,7 @@ export const createScopeToolInvoker = (input: { · 41 unmodified lines
117121 ) => effect.pipe(Effect.provide(secretMaterialLayer));
118122
119123 const executorTools = createExecutorToolMap({
124+ pluginRegistry: input.pluginRegistry,
120125 scopeId: input.scopeId,
121126 actorScopeId: input.actorScopeId,
122127 executorStateStore: input.executorStateStore,
@@ -127,6 +132,7 @@ export const createScopeToolInvoker = (input: { · 4 unmodified lines
127132 scopeStateStore: input.scopeStateStore,
128133 sourceArtifactStore: input.sourceArtifactStore,
129134 runtimeLocalScope: input.runtimeLocalScope,
135+ secretMaterialServices: input.secretMaterialServices,
130136 });
131137 const sourceCatalog = createScopeSourceCatalog({
132138 scopeId: input.scopeId,
@@ -196,6 +202,7 @@ export const createScopeToolInvoker = (input: { · 63 unmodified lines
196202 });
197203
198204 return yield* invokeIrTool({
205+ pluginRegistry: input.pluginRegistry,
199206 scopeId: input.scopeId,
200207 actorScopeId: input.actorScopeId,
201208 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 "./sources/executor-tools";
@@ -105,6 +112,8 @@ export * from "./source-artifacts"; · 12 unmodified lines
105112 export * from "./scope-config";
106113 export * from "./scope-state";
107114 export * from "./scope/config-secrets";
115+export * from "./sources/source-store/config";
116+export * from "./sources/source-store/plugin-local-config";
108117 export {
109118 LocalInstanceConfigService,
110119 SecretMaterialDeleterService,
@@ -137,7 +146,10 @@ export type { · 26 unmodified lines
137146 } from "./local-tool-runtime";
138147 export {
139148 registeredSourceContributions,
149+ registeredManagementToolContributions,
140150 hasRegisteredExternalSourcePlugins,
151+ ExecutorPluginRegistryLive,
152+ ExecutorPluginRegistryService,
141153 getSourceContribution,
142154 getSourceContributionForSource,
143155 } from "./sources/source-plugins";
@@ -146,6 +158,7 @@ export type ExecutorRuntimeOptions = { · 2 unmodified lines
146158 executionResolver?: ResolveExecutionEnvironment;
147159 resolveSecretMaterial?: ResolveSecretMaterial;
148160 getLocalServerBaseUrl?: () => string | undefined;
161+ pluginRegistry?: ExecutorSdkPluginRegistry;
149162 };
150163
151164 type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;
@@ -181,7 +194,11 @@ export type BoundScopeStateStore = { · 29 unmodified lines
181194 };
182195
183196 export type BoundSourceArtifactStore = {
184− build: SourceArtifactStoreShape["build"];
197+ build: (input: {
198+ source: Parameters<SourceArtifactStoreShape["build"]>[0]["source"];
199+ syncResult: Parameters<SourceArtifactStoreShape["build"]>[0]["syncResult"];
200+ pluginRegistry?: ExecutorSdkPluginRegistry;
201+ }) => LocalSourceArtifact;
185202 read: (
186203 sourceId: string,
187204 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;
@@ -289,8 +306,14 @@ const toScopeStateStoreShape = ( · 101 unmodified lines
289306
290307 const toSourceArtifactStoreShape = (
291308 input: BoundSourceArtifactStore,
309+ pluginRegistry: ExecutorSdkPluginRegistry,
292310 ): SourceArtifactStoreShape => ({
293− build: input.build,
311+ build: ({ source, syncResult }) =>
312+ input.build({
313+ source,
314+ syncResult,
315+ pluginRegistry,
316+ }),
294317 read: ({ sourceId }) => input.read(sourceId),
295318 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),
296319 remove: ({ sourceId }) => input.remove(sourceId),
@@ -322,11 +345,16 @@ export const createExecutorRuntimeLayer = ( · 25 unmodified lines
322345 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;
323346 },
324347 ) => {
348+ const pluginRegistry =
349+ input.pluginRegistry ?? emptyExecutorPluginRegistry();
325350 const storageLayer = makeLocalStorageLayer({
326351 installationStore: toInstallationStoreShape(input.storage.installation),
327352 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),
328353 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),
329− sourceArtifactStore: toSourceArtifactStoreShape(input.storage.sourceArtifacts),
354+ sourceArtifactStore: toSourceArtifactStoreShape(
355+ input.storage.sourceArtifacts,
356+ pluginRegistry,
357+ ),
330358 });
331359 const localToolRuntimeLayer = Layer.succeed(
332360 LocalToolRuntimeLoaderService,
@@ -345,6 +373,7 @@ export const createExecutorRuntimeLayer = ( · 12 unmodified lines
345373
346374 const baseLayer = Layer.mergeAll(
347375 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),
376+ ExecutorPluginRegistryLive(pluginRegistry),
348377 RuntimeLocalScopeLive(input.localScopeState),
349378 storageLayer,
350379 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),
@@ -402,6 +431,7 @@ export type ExecutorRuntime = { · 51 unmodified lines
402431 scope: ExecutorScopeContext;
403432 storage: RuntimeStorageServices;
404433 localInstallation: LocalInstallation;
434+ pluginRegistry: ExecutorSdkPluginRegistry;
405435 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;
406436 runtimeLayer: ExecutorRuntimeLayer;
407437 close: () => Promise<void>;
@@ -417,6 +447,8 @@ export const createExecutorRuntimeFromServices = (input: { · 9 unmodified lines
417447 services: ExecutorRuntimeServices;
418448 } & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>
419449 (Effect.gen(function* () {
450+ const pluginRegistry =
451+ input.pluginRegistry ?? emptyExecutorPluginRegistry();
420452 const localInstallation = yield* input.services.storage.installation
421453 .getOrProvision()
422454 .pipe(Effect.mapError(toRuntimeBootstrapError));
@@ -449,6 +481,7 @@ export const createExecutorRuntimeFromServices = (input: { · 26 unmodified lines
449481 ),
450482 sourceArtifactStore: toSourceArtifactStoreShape(
451483 input.services.storage.sourceArtifacts,
484+ pluginRegistry,
452485 ),
453486 }),
454487 ),
@@ -491,6 +524,7 @@ export const createExecutorRuntimeFromServices = (input: { · 36 unmodified lines
491524 scope: runtimeWorkspace,
492525 storage: input.services.storage,
493526 localInstallation,
527+ pluginRegistry,
494528 managedRuntime,
495529 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,
496530 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+33−17
@@ -1,29 +1,45 @@
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 registeredManagementToolContributions = (
30+ registry: ExecutorSdkPluginRegistry,
31+) => registry.managementTools;
1932
20−export const registeredSourceContributions = () => registry.sources;
33+export const getSourceContribution = (
34+ registry: ExecutorSdkPluginRegistry,
35+ kind: string,
36+) => registry.getSourceContribution(kind);
2137
22−export const getSourceContribution = (kind: string) =>
23− registry.getSourceContribution(kind);
2438 export const getSourceContributionForSource = (
25− source: Parameters<typeof registry.getSourceContributionForSource>[0],
39+ registry: ExecutorSdkPluginRegistry,
40+ source: Parameters<ExecutorSdkPluginRegistry["getSourceContributionForSource"]>[0],
2641 ) => registry.getSourceContributionForSource(source);
2742
28−export const hasRegisteredExternalSourcePlugins = () =>
29− configuredSourcePlugins.length > 0;
43+export const hasRegisteredExternalSourcePlugins = (
44+ registry: ExecutorSdkPluginRegistry,
45+) => 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";
@@ -55,6 +60,7 @@ import { · 13 unmodified lines
5560 contentHash,
5661 createCatalogImportMetadata,
5762 createSourceCatalogSyncResult,
63+ snapshotFromSourceCatalogSyncResult,
5864 } from "../../../sources/core/src/index";
5965 import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";
6066
@@ -256,6 +262,103 @@ const writeConfiguredLocalMcpSource = (input: { · 195 unmodified lines
256262 });
257263 }).pipe(Effect.provide(NodeFileSystem.layer));
258264
265+const writeLegacyConfiguredWorkspaceSources = (input: {
266+ workspaceRoot: string;
267+ sources: Record<string, LocalConfigSource>;
268+}) =>
269+ Effect.gen(function* () {
270+ const context = yield* resolveLocalWorkspaceContext({
271+ workspaceRoot: input.workspaceRoot,
272+ });
273+ const installation = yield* getOrProvisionLocalInstallation({ context });
274+
275+ yield* writeProjectLocalExecutorConfig({
276+ context,
277+ config: {
278+ sources: input.sources,
279+ },
280+ });
281+
282+ return {
283+ context,
284+ installation,
285+ };
286+ }).pipe(Effect.provide(NodeFileSystem.layer));
287+
288+const writeMissingSourceCatalogArtifact = (input: {
289+ context: Effect.Effect.Success<ReturnType<typeof writeLegacyConfiguredWorkspaceSources>>["context"];
290+ installation: Effect.Effect.Success<ReturnType<typeof writeLegacyConfiguredWorkspaceSources>>["installation"];
291+ source: {
292+ id: string;
293+ name: string;
294+ kind: "mcp" | "openapi" | "graphql" | "google_discovery";
295+ namespace: string;
296+ };
297+ pluginKey: string;
298+ importerVersion: string;
299+}) =>
300+ Effect.gen(function* () {
301+ const now = Date.now();
302+ const source = {
303+ id: SourceIdSchema.make(input.source.id),
304+ scopeId: input.installation.scopeId,
305+ name: input.source.name,
306+ kind: input.source.kind,
307+ status: "connected" as const,
308+ enabled: true,
309+ namespace: input.source.namespace,
310+ createdAt: now,
311+ updatedAt: now,
312+ };
313+ const syncResult = createSourceCatalogSyncResult({
314+ fragment: {
315+ version: "ir.v1.fragment",
316+ },
317+ importMetadata: {
318+ ...createCatalogImportMetadata({
319+ source,
320+ pluginKey: input.pluginKey,
321+ }),
322+ importerVersion: input.importerVersion,
323+ sourceConfigHash: "missing",
324+ },
325+ sourceHash: null,
326+ });
327+ const snapshot = snapshotFromSourceCatalogSyncResult(syncResult);
328+ const catalogId = SourceCatalogIdSchema.make(`test_catalog_${input.source.id}`);
329+ const importMetadataJson = JSON.stringify(snapshot.import);
330+
331+ yield* writeLocalSourceArtifact({
332+ context: input.context,
333+ sourceId: source.id,
334+ artifact: {
335+ version: 4,
336+ sourceId: source.id,
337+ catalogId,
338+ generatedAt: now,
339+ revision: {
340+ id: SourceCatalogRevisionIdSchema.make(`test_catalog_rev_${input.source.id}`),
341+ catalogId,
342+ revisionNumber: 1,
343+ sourceConfigJson: JSON.stringify({
344+ sourceId: source.id,
345+ kind: source.kind,
346+ name: source.name,
347+ namespace: source.namespace,
348+ enabled: source.enabled,
349+ updatedAt: source.updatedAt,
350+ }),
351+ importMetadataJson,
352+ importMetadataHash: contentHash(importMetadataJson),
353+ snapshotHash: contentHash(JSON.stringify(snapshot)),
354+ createdAt: now,
355+ updatedAt: now,
356+ },
357+ snapshot,
358+ },
359+ });
360+ }).pipe(Effect.provide(NodeFileSystem.layer));
361+
259362 const makeServer = createIsolatedLocalExecutorServer({
260363 port: 0,
261364 localDataDir: ":memory:",
@@ -1702,6 +1805,127 @@ describe("local-executor-server", () => { · 1440 unmodified lines
17021805 15_000,
17031806 );
17041807
1808+ it.scoped("recovers config-backed MCP and OpenAPI sources from config-only state", () =>
1809+ Effect.gen(function* () {
1810+ const workspaceRoot = yield* makeTempWorkspaceRoot();
1811+ const localDataDir = yield* makeTempWorkspaceRoot();
1812+ const demoServer = yield* Effect.acquireRelease(
1813+ Effect.promise(() => startMcpElicitationDemoServer()),
1814+ (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
1815+ );
1816+ const openApiServer = yield* Effect.acquireRelease(
1817+ Effect.promise(() => startOpenApiDemoServer()),
1818+ (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
1819+ );
1820+
1821+ const workspace = yield* writeLegacyConfiguredWorkspaceSources({
1822+ workspaceRoot,
1823+ sources: {
1824+ deepwiki: {
1825+ kind: "mcp",
1826+ name: "DeepWiki MCP",
1827+ connection: {
1828+ endpoint: demoServer.endpoint,
1829+ },
1830+ binding: {
1831+ transport: "streamable-http",
1832+ queryParams: null,
1833+ headers: null,
1834+ command: null,
1835+ args: null,
1836+ env: null,
1837+ cwd: null,
1838+ },
1839+ },
1840+ vercel: {
1841+ kind: "openapi",
1842+ name: "Vercel API",
1843+ connection: {
1844+ endpoint: openApiServer.baseUrl,
1845+ },
1846+ binding: {
1847+ specUrl: openApiServer.specUrl,
1848+ defaultHeaders: null,
1849+ },
1850+ },
1851+ },
1852+ });
1853+
1854+ yield* writeMissingSourceCatalogArtifact({
1855+ context: workspace.context,
1856+ installation: workspace.installation,
1857+ source: {
1858+ id: "deepwiki",
1859+ name: "DeepWiki MCP",
1860+ kind: "mcp",
1861+ namespace: "deepwiki",
1862+ },
1863+ pluginKey: "mcp",
1864+ importerVersion: "ir.v1.mcp",
1865+ });
1866+ yield* writeMissingSourceCatalogArtifact({
1867+ context: workspace.context,
1868+ installation: workspace.installation,
1869+ source: {
1870+ id: "vercel",
1871+ name: "Vercel API",
1872+ kind: "openapi",
1873+ namespace: "vercel",
1874+ },
1875+ pluginKey: "openapi",
1876+ importerVersion: "ir.v1.openapi",
1877+ });
1878+
1879+ const server = yield* createLocalExecutorServer({
1880+ workspaceRoot,
1881+ localDataDir,
1882+ port: 0,
1883+ });
1884+ const client = yield* makeApiClient(server.baseUrl);
1885+
1886+ const mcpConfig = yield* client.mcp.getSourceConfig({
1887+ path: {
1888+ workspaceId: workspace.installation.scopeId,
1889+ sourceId: "deepwiki" as never,
1890+ },
1891+ });
1892+ expect(mcpConfig.endpoint).toBe(demoServer.endpoint);
1893+ expect(mcpConfig.transport).toBe("streamable-http");
1894+
1895+ const openApiConfig = yield* client.openapi.getSourceConfig({
1896+ path: {
1897+ workspaceId: workspace.installation.scopeId,
1898+ sourceId: "vercel" as never,
1899+ },
1900+ });
1901+ expect(openApiConfig.specUrl).toBe(openApiServer.specUrl);
1902+ expect(openApiConfig.baseUrl).toBe(openApiServer.baseUrl);
1903+
1904+ const mcpInspectionResponse = yield* Effect.promise(() =>
1905+ fetch(
1906+ `${server.baseUrl}/v1/workspaces/${workspace.installation.scopeId}/sources/deepwiki/inspection`,
1907+ ),
1908+ );
1909+ assertTrue(mcpInspectionResponse.ok);
1910+ const mcpInspection = yield* Effect.promise(
1911+ () => mcpInspectionResponse.json() as Promise<{ toolCount: number }>,
1912+ );
1913+ expect(mcpInspection.toolCount).toBeGreaterThan(0);
1914+
1915+ const openApiInspectionResponse = yield* Effect.promise(() =>
1916+ fetch(
1917+ `${server.baseUrl}/v1/workspaces/${workspace.installation.scopeId}/sources/vercel/inspection`,
1918+ ),
1919+ );
1920+ assertTrue(openApiInspectionResponse.ok);
1921+ const openApiInspection = yield* Effect.promise(
1922+ () => openApiInspectionResponse.json() as Promise<{ toolCount: number }>,
1923+ );
1924+ expect(openApiInspection.toolCount).toBeGreaterThan(0);
1925+ }),
1926+ 20_000,
1927+ );
1928+
17051929 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>
17061930 Effect.gen(function* () {
17071931 const localDataDir = yield* makeTempWorkspaceRoot();
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>,