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
27 openApiSdkPlugin,27 openApiSdkPlugin,
28} from "@executor/plugin-openapi-sdk";28} from "@executor/plugin-openapi-sdk";
29import {29import {
30 getExecutorSourcesAddHelpLines,30 getExecutorInternalToolHelpLines,
31 RuntimeExecutionResolverService,31 RuntimeExecutionResolverService,
32} from "@executor/platform-sdk/runtime";32} from "@executor/platform-sdk/runtime";
33import type {
34 ExecutorSdkPluginRegistry,
35} from "@executor/platform-sdk/plugins";
33import {36import {
34 createExecutorEffect,37 createExecutorEffect,
35 type ExecutorEffect as Executor,38 type ExecutorEffect as Executor,
@@ -217,7 +220,10 @@ const toEffectCliArgv = (args: readonly string[]): string[] => [ · 181 unmodified lines
217 ...args,220 ...args,
218];221];
219 222
220const buildWorkflowText = (namespaces: readonly string[] = []): string =>223const buildWorkflowText = (
224 namespaces: readonly string[] = [],
225 pluginRegistry?: ExecutorSdkPluginRegistry,
226): string =>
221 [227 [
222 "Execute TypeScript in sandbox; call tools via discovery workflow.",228 "Execute TypeScript in sandbox; call tools via discovery workflow.",
223 ...(namespaces.length > 0229 ...(namespaces.length > 0
@@ -231,12 +237,15 @@ const buildWorkflowText = (namespaces: readonly string[] = []): string => · 7 unmodified lines
231 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",237 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
232 "3) Call selected tools.<path>(input).",238 "3) Call selected tools.<path>(input).",
233 "4) Use source plugins to inspect or add API sources.",239 "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."]),
235 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",243 "5) If execution pauses for interaction, resume it with `executor resume --execution-id ...`.",
236 "Do not use fetch; use tools.* only.",244 "Do not use fetch; use tools.* only.",
237 ].join("\n");245 ].join("\n");
238 246
239const getDefaultRunWorkflow = () => buildWorkflowText();247const getDefaultRunWorkflow = (pluginRegistry?: ExecutorSdkPluginRegistry) =>
248 buildWorkflowText([], pluginRegistry);
240 249
241const indentBlock = (value: string, prefix: string = " "): string =>250const indentBlock = (value: string, prefix: string = " "): string =>
242 value251 value
@@ -279,9 +288,10 @@ const closeExecutor = (executor: Executor) => · 36 unmodified lines
279 288
280const buildRunWorkflowText = (289const buildRunWorkflowText = (
281 catalog?: ToolCatalog,290 catalog?: ToolCatalog,
291 pluginRegistry?: ExecutorSdkPluginRegistry,
282): Effect.Effect<string, Error, never> => {292): Effect.Effect<string, Error, never> => {
283 if (!catalog) {293 if (!catalog) {
284 return Effect.succeed(getDefaultRunWorkflow());294 return Effect.succeed(getDefaultRunWorkflow(pluginRegistry));
285 }295 }
286 296
287 return catalog.listNamespaces({ limit: 200 }).pipe(297 return catalog.listNamespaces({ limit: 200 }).pipe(
@@ -290,6 +300,7 @@ const buildRunWorkflowText = ( · 2 unmodified lines
290 namespaces.length > 0300 namespaces.length > 0
291 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)301 ? namespaces.map((namespace) => namespace.displayName ?? namespace.namespace)
292 : ["none discovered yet"],302 : ["none discovered yet"],
303 pluginRegistry,
293 )304 )
294 ),305 ),
295 Effect.mapError(toError),306 Effect.mapError(toError),
@@ -345,7 +356,10 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 49 unmodified lines
345 Effect.mapError(toError),356 Effect.mapError(toError),
346 );357 );
347 358
348 return yield* buildRunWorkflowText(environment.catalog);359 return yield* buildRunWorkflowText(
360 environment.catalog,
361 executor.runtime.pluginRegistry,
362 );
349 }),363 }),
350 closeExecutor,364 closeExecutor,
351 ).pipe(365 ).pipe(
apps/web/src/components/source-favicon.tsx+88−10
@@ -1,9 +1,12 @@
1import { useState } from "react";1import { useState } from "react";
2import type { Source } from "@executor/react";2import type { Source } from "@executor/react";
3import { getGoogleDiscoveryIconUrlForService } from "@executor/plugin-google-discovery-react";
3import { getSourceFrontendIconUrl } from "../plugins";4import { getSourceFrontendIconUrl } from "../plugins";
5import type { SourcePreset } from "../plugins/source-presets";
6import { getSourceFaviconUrl } from "../lib/source-favicon";
4import { cn } from "../lib/utils";7import { cn } from "../lib/utils";
5 8
6type SourceKind = Source["kind"] | string;9type SourceKind = Source["kind"] | SourcePreset["kind"] | string;
7 10
8export function SourceFavicon({11export function SourceFavicon({
9 source,12 source,
@@ -14,12 +17,55 @@ export function SourceFavicon({ · 4 unmodified lines
14 className?: string;17 className?: string;
15 size?: number;18 size?: number;
16}) {19}) {
17 const faviconUrl = getSourceFrontendIconUrl(source);20 return (
21 <ResolvedSourceIcon
22 kind={source.kind}
23 faviconUrl={getSourceFrontendIconUrl(source)}
24 className={className}
25 size={size}
26 />
27 );
28}
29
30export function SourcePresetFavicon({
31 preset,
32 className,
33 size = 16,
34}: {
35 preset: SourcePreset;
36 className?: string;
37 size?: number;
38}) {
39 const faviconUrl = preset.kind === "google_discovery"
40 ? getGoogleDiscoveryIconUrlForService(preset.service)
41 : getSourceFaviconUrl(preset.previewUrl);
42
43 return (
44 <ResolvedSourceIcon
45 kind={preset.kind}
46 faviconUrl={faviconUrl ?? getSourceFaviconUrl(preset.name)}
47 className={className}
48 size={size}
49 />
50 );
51}
52
53function ResolvedSourceIcon({
54 kind,
55 faviconUrl,
56 className,
57 size = 16,
58}: {
59 kind: SourceKind;
60 faviconUrl: string | null;
61 className?: string;
62 size?: number;
63}) {
18 const [failedUrl, setFailedUrl] = useState<string | null>(null);64 const [failedUrl, setFailedUrl] = useState<string | null>(null);
19 const isFailed = Boolean(faviconUrl && failedUrl === faviconUrl);65 const isFailed = Boolean(faviconUrl && failedUrl === faviconUrl);
20 66
21 if (!faviconUrl || isFailed) {67 if (!faviconUrl || isFailed) {
22 return <DefaultSourceIcon kind={source.kind} className={className} />;68 return <DefaultSourceIcon kind={kind} className={className} />;
23 }69 }
24 70
25 return (71 return (
@@ -38,7 +84,7 @@ export function SourceFavicon({ · 12 unmodified lines
38}84}
39 85
40export function DefaultSourceIcon({86export function DefaultSourceIcon({
41 kind: _kind,87 kind,
42 className,88 className,
43}: {89}: {
44 kind: SourceKind;90 kind: SourceKind;
@@ -46,10 +92,42 @@ export function DefaultSourceIcon({ · 1 unmodified line
46}) {92}) {
47 const base = cn("shrink-0", className);93 const base = cn("shrink-0", className);
48 94
49 return (95 switch (kind) {
50 <svg viewBox="0 0 16 16" fill="none" className={base}>96 case "mcp":
51 <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.2" />97 return (
52 <path d="M5 6h6M5 8h4M5 10h3" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />98 <svg viewBox="0 0 16 16" fill="none" className={base}>
53 </svg>99 <rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
54 );100 <path d="M5 7h1M5 9h4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
101 </svg>
102 );
103 case "graphql":
104 return (
105 <svg viewBox="0 0 16 16" fill="none" className={base}>
106 <path d="M8 2L14 5.5V10.5L8 14L2 10.5V5.5L8 2Z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
107 <circle cx="8" cy="8" r="1.5" fill="currentColor" opacity="0.5" />
108 </svg>
109 );
110 case "google_discovery":
111 return (
112 <svg viewBox="0 0 16 16" fill="none" className={base}>
113 <path d="M8 2.5a5.5 5.5 0 1 0 0 11 5.3 5.3 0 0 0 3.82-1.55" stroke="currentColor" strokeWidth="1.2" />
114 <path d="M13 8H8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
115 <circle cx="12.5" cy="8" r="1.2" fill="currentColor" />
116 </svg>
117 );
118 case "openapi":
119 return (
120 <svg viewBox="0 0 16 16" fill="none" className={base}>
121 <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
122 <path d="M5 6h6M5 8h4M5 10h5" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
123 </svg>
124 );
125 default:
126 return (
127 <svg viewBox="0 0 16 16" fill="none" className={base}>
128 <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.2" />
129 <path d="M5 6h6M5 8h4M5 10h3" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
130 </svg>
131 );
132 }
55}133}
apps/web/src/components/source-plugins-reset-state.tsx+1−1
@@ -10,7 +10,7 @@ export function SourcePluginsResetState( · 9 unmodified lines
10 input: SourcePluginsResetStateProps,10 input: SourcePluginsResetStateProps,
11) {11) {
12 return (12 return (
13 <div className="min-h-0 flex-1 overflow-y-auto max-w-[300px]">13 <div className="min-h-0 flex-1 overflow-y-auto w-[300px]">
14 <div className="mx-auto max-w-3xl px-6 py-10 lg:px-10 lg:py-14">14 <div className="mx-auto max-w-3xl px-6 py-10 lg:px-10 lg:py-14">
15 <div className="rounded-2xl border border-border bg-card p-8">15 <div className="rounded-2xl border border-border bg-card p-8">
16 <h2 className="font-display text-xl tracking-tight text-foreground">16 <h2 className="font-display text-xl tracking-tight text-foreground">
apps/web/src/lib/source-favicon.ts+158−0
@@ -0,0 +1,158 @@
1import { parse as parseDomain } from "tldts";
2
3const RAW_HOSTS = new Set([
4 "raw.githubusercontent.com",
5 "cdn.jsdelivr.net",
6 "raw.github.com",
7 "unpkg.com",
8]);
9
10const BRAND_DOMAINS: Record<string, string> = {
11 anilist: "anilist.co",
12 axiom: "axiom.co",
13 chrome: "developer.chrome.com",
14 deepwiki: "deepwiki.com",
15 github: "github.com",
16 gitlab: "gitlab.com",
17 linear: "linear.app",
18 neon: "neon.tech",
19 openai: "openai.com",
20 stripe: "stripe.com",
21 vercel: "vercel.com",
22};
23
24const IGNORED_BRAND_TOKENS = new Set([
25 "api",
26 "app",
27 "apps",
28 "cli",
29 "cloud",
30 "com",
31 "console",
32 "dev",
33 "docs",
34 "doc",
35 "graphql",
36 "http",
37 "https",
38 "json",
39 "latest",
40 "mcp",
41 "net",
42 "none",
43 "npm",
44 "npx",
45 "openapi",
46 "org",
47 "plugin",
48 "plugins",
49 "raw",
50 "rest",
51 "sdk",
52 "server",
53 "service",
54 "services",
55 "source",
56 "sources",
57 "stdio",
58 "transport",
59 "uv",
60 "uvx",
61 "yaml",
62 "yarn",
63 "yml",
64]);
65
66const parseUrl = (value: string): URL | null => {
67 try {
68 return new URL(value);
69 } catch {
70 return null;
71 }
72};
73
74const toDomainSeedUrl = (value: string): string | null => {
75 const parsed = parseDomain(value);
76 const domain = parsed.domain ?? BRAND_DOMAINS[value] ?? null;
77 return domain ? `https://${domain}` : null;
78};
79
80const isMeaninglessToken = (token: string): boolean =>
81 token.length <= 1
82 || IGNORED_BRAND_TOKENS.has(token)
83 || /^v?\d+$/.test(token);
84
85const inferBrandToken = (value: string): string | null => {
86 const token = value
87 .trim()
88 .toLowerCase()
89 .split(/[^a-z0-9]+/g)
90 .find((candidate) => !isMeaninglessToken(candidate));
91
92 return token && token.length > 0 ? token : null;
93};
94
95const toBrandSeedUrl = (value: string): string | null => {
96 const brand = inferBrandToken(value);
97 if (!brand) {
98 return null;
99 }
100
101 return `https://${BRAND_DOMAINS[brand] ?? `${brand}.com`}`;
102};
103
104const normalizeSeedUrl = (value: string | null | undefined): string | null => {
105 const trimmed = value?.trim();
106 if (!trimmed) {
107 return null;
108 }
109
110 const parsed = parseUrl(trimmed) ?? parseUrl(`https://${trimmed}`);
111 if (parsed) {
112 if (RAW_HOSTS.has(parsed.hostname)) {
113 const segments = parsed.pathname
114 .split("/")
115 .map((segment) => segment.trim())
116 .filter((segment) => segment.length > 0);
117
118 for (const segment of segments) {
119 const withoutExtension = segment.replace(/\.(ya?ml|json)$/i, "");
120 const seeded =
121 toDomainSeedUrl(withoutExtension)
122 ?? toBrandSeedUrl(withoutExtension);
123 if (seeded) {
124 return seeded;
125 }
126 }
127 }
128
129 return toDomainSeedUrl(parsed.hostname) ?? toBrandSeedUrl(parsed.hostname);
130 }
131
132 return toDomainSeedUrl(trimmed) ?? toBrandSeedUrl(trimmed);
133};
134
135export const getSourceFaviconUrl = (
136 value: string | null | undefined,
137): string | null => {
138 const seedUrl = normalizeSeedUrl(value);
139 if (!seedUrl) {
140 return null;
141 }
142
143 try {
144 const hostname = new URL(seedUrl).hostname;
145 const parsed = parseDomain(hostname);
146 const domain = parsed.domain ?? hostname;
147 return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`;
148 } catch {
149 return null;
150 }
151};
152
153export const getFallbackSourceFaviconUrl = (input: {
154 namespace?: string | null;
155 name?: string | null;
156}): string | null =>
157 getSourceFaviconUrl(input.namespace)
158 ?? getSourceFaviconUrl(input.name);
apps/web/src/plugins/index.ts+8−2
@@ -23,6 +23,7 @@ import { · 22 unmodified lines
23 registerExecutorFrontendPlugins,23 registerExecutorFrontendPlugins,
24 type ExecutorFrontendPlugin,24 type ExecutorFrontendPlugin,
25} from "@executor/react/plugins";25} from "@executor/react/plugins";
26import { getFallbackSourceFaviconUrl } from "../lib/source-favicon";
26 27
27const frontendPlugins = [28const frontendPlugins = [
28 ExecutionHistoryReactPlugin,29 ExecutionHistoryReactPlugin,
@@ -77,7 +78,12 @@ export const getSourceFrontendPaths = (kind: string) => { · 48 unmodified lines
77 return plugin ? createSourcePluginPaths(plugin.key) : null;78 return plugin ? createSourcePluginPaths(plugin.key) : null;
78};79};
79 80
81const isGoogleDiscoverySource = (kind: string): boolean =>
82 kind === "google_discovery"
83 || kind === "google-discovery";
84
80export const getSourceFrontendIconUrl = (source: Source) =>85export const getSourceFrontendIconUrl = (source: Source) =>
81 source.kind === "google-discovery"86 (isGoogleDiscoverySource(source.kind)
82 ? getGoogleDiscoveryIconUrl(source)87 ? 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
9 type FrontendPluginRouteSearch,9 type FrontendPluginRouteSearch,
10} from "@executor/react/plugins";10} from "@executor/react/plugins";
11 11
12import { DefaultSourceIcon } from "../components/source-favicon";12import { SourcePresetFavicon } from "../components/source-favicon";
13import { LocalMcpInstallCard } from "../components/local-mcp-install-card";13import { LocalMcpInstallCard } from "../components/local-mcp-install-card";
14import { SourcePluginsResetState } from "../components/source-plugins-reset-state";14import { SourcePluginsResetState } from "../components/source-plugins-reset-state";
15import {15import {
@@ -201,7 +201,7 @@ export function SourcePluginsIndexPage() { · 185 unmodified lines
201 >201 >
202 <div className="mb-2 flex items-center gap-2.5">202 <div className="mb-2 flex items-center gap-2.5">
203 <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">203 <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" />
205 </div>205 </div>
206 <div className="min-w-0">206 <div className="min-w-0">
207 <div className="truncate text-sm font-semibold text-foreground">207 <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
298 <button298 <button
299 type="button"299 type="button"
300 onClick={() => setOpen((value) => !value)}300 onClick={() => setOpen((value) => !value)}
301 aria-expanded={open}
301 className="shrink-0 rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"302 className="shrink-0 rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground"
302 style={{ marginLeft: paddingLeft }}303 style={{ marginLeft: paddingLeft }}
303 >304 >
304 <IconChevron305 <IconChevron
305 className={cn("shrink-0 transition-transform duration-150", open && "rotate-90")}306 className="shrink-0 transition-transform duration-150"
306 style={{ width: 8, height: 8 }}307 style={{
308 width: 8,
309 height: 8,
310 transform: open ? "rotate(90deg)" : "rotate(0deg)",
311 transformOrigin: "50% 50%",
312 }}
307 />313 />
308 </button>314 </button>
309 <SourceToolListItem315 <SourceToolListItem
@@ -321,6 +327,7 @@ const SourceToolTreeNodeView = (props: { · 11 unmodified lines
321 <button327 <button
322 type="button"328 type="button"
323 onClick={() => setOpen((value) => !value)}329 onClick={() => setOpen((value) => !value)}
330 aria-expanded={open}
324 className={cn(331 className={cn(
325 "group flex w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] transition-colors hover:bg-accent/40",332 "group flex w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] transition-colors hover:bg-accent/40",
326 open ? "text-foreground/80" : "text-muted-foreground/60",333 open ? "text-foreground/80" : "text-muted-foreground/60",
@@ -328,11 +335,13 @@ const SourceToolTreeNodeView = (props: { · 1 unmodified line
328 style={{ paddingLeft }}335 style={{ paddingLeft }}
329 >336 >
330 <IconChevron337 <IconChevron
331 className={cn(338 className="shrink-0 text-muted-foreground/30 transition-transform duration-150"
332 "shrink-0 text-muted-foreground/30 transition-transform duration-150",339 style={{
333 open && "rotate-90",340 width: 8,
334 )}341 height: 8,
335 style={{ width: 8, height: 8 }}342 transform: open ? "rotate(90deg)" : "rotate(0deg)",
343 transformOrigin: "50% 50%",
344 }}
336 />345 />
337 <IconFolder346 <IconFolder
338 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}347 className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")}
@@ -622,7 +631,7 @@ export const SourceToolModelWorkbench = ( · 283 unmodified lines
622 </div>631 </div>
623 </div>632 </div>
624 633
625 <div className="flex-1 overflow-y-auto max-w-[300px]">634 <div className="flex-1 overflow-y-auto w-[300px]">
626 {filteredTools.length === 0 ? (635 {filteredTools.length === 0 ? (
627 <div className="p-4 text-center text-[13px] text-muted-foreground/50">636 <div className="p-4 text-center text-[13px] text-muted-foreground/50">
628 {terms.length > 0 ? "No tools match your filter" : "No tools available"}637 {terms.length > 0 ? "No tools match your filter" : "No tools available"}
packages/hosts/mcp/src/index.ts+8−5
@@ -2,7 +2,7 @@ import type { · 1 unmodified line
2 ExecutionEnvelope,2 ExecutionEnvelope,
3} from "@executor/platform-sdk/schema";3} from "@executor/platform-sdk/schema";
4import {4import {
5 getExecutorSourcesAddHelpLines,5 getExecutorInternalToolHelpLines,
6 RuntimeSourceCatalogStoreService,6 RuntimeSourceCatalogStoreService,
7 createExecution,7 createExecution,
8 getExecution,8 getExecution,
@@ -152,6 +152,7 @@ type CatalogLike = { · 143 unmodified lines
152const executeDescriptionToolsPerSource = 5;152const executeDescriptionToolsPerSource = 5;
153 153
154const buildExecuteWorkflowText = (154const buildExecuteWorkflowText = (
155 runtime: ExecutorRuntime,
155 sourceToolExamples: ReadonlyArray<{156 sourceToolExamples: ReadonlyArray<{
156 sourceName: string;157 sourceName: string;
157 toolPaths: readonly string[];158 toolPaths: readonly string[];
@@ -173,7 +174,7 @@ const buildExecuteWorkflowText = ( · 15 unmodified lines
173 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",174 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
174 "3) Call selected tools.<path>(input).",175 "3) Call selected tools.<path>(input).",
175 "4) Use source plugins to inspect or add API sources.",176 "4) Use source plugins to inspect or add API sources.",
176 ...getExecutorSourcesAddHelpLines(),177 ...getExecutorInternalToolHelpLines(runtime.pluginRegistry),
177 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",178 "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.",
178 "Do not use fetch; use tools.* only.",179 "Do not use fetch; use tools.* only.",
179 ].join("\n");180 ].join("\n");
@@ -201,11 +202,13 @@ const loadExecuteDescription = (runtime: ExecutorRuntime): Promise<string> => · 21 unmodified lines
201 .filter((catalog) => catalog.toolPaths.length > 0);202 .filter((catalog) => catalog.toolPaths.length > 0);
202 203
203 if (sourceToolExamples.length === 0) {204 if (sourceToolExamples.length === 0) {
204 return buildExecuteWorkflowText();205 return buildExecuteWorkflowText(runtime);
205 }206 }
206 207
207 return buildExecuteWorkflowText(sourceToolExamples);208 return buildExecuteWorkflowText(runtime, sourceToolExamples);
208 }).pipe(Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText()))),209 }).pipe(
210 Effect.catchAll(() => Effect.succeed(buildExecuteWorkflowText(runtime))),
211 ),
209 );212 );
210 213
211const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {214const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => {
packages/platform/sdk/src/executor-effect.ts+8−9
@@ -61,15 +61,13 @@ import { · 60 unmodified lines
61 listExecutions,61 listExecutions,
62 resumeExecution,62 resumeExecution,
63} from "./runtime/execution/service";63} from "./runtime/execution/service";
64import type {
65 ExecutorSdkPluginHost,
66 ExecutorSdkPlugin,
67 ExecutorSdkPluginExtensions,
68 PluginCleanup,
69} from "./plugins";
70import {64import {
71 configureExecutorSourcePlugins,65 registerExecutorSdkPlugins,
72} from "./runtime/sources/source-plugins";66 type ExecutorSdkPluginHost,
67 type ExecutorSdkPlugin,
68 type ExecutorSdkPluginExtensions,
69 type PluginCleanup,
70} from "./plugins";
73 71
74type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<72type ProvidedEffect<T extends Effect.Effect<any, any, any>> = Effect.Effect<
75 Effect.Effect.Success<T>,73 Effect.Effect.Success<T>,
@@ -263,13 +261,14 @@ export const createExecutorEffect = < · 187 unmodified lines
263>(261>(
264 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,262 options: CreateExecutorEffectOptionsWithPlugins<TPlugins>,
265): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {263): Effect.Effect<ExecutorEffect & ExecutorSdkPluginExtensions<TPlugins>, Error> => {
266 configureExecutorSourcePlugins(options.plugins ?? []);264 const pluginRegistry = registerExecutorSdkPlugins(options.plugins ?? []);
267 265
268 return Effect.flatMap(266 return Effect.flatMap(
269 options.backend.createRuntime({267 options.backend.createRuntime({
270 executionResolver: options.executionResolver,268 executionResolver: options.executionResolver,
271 resolveSecretMaterial: options.resolveSecretMaterial,269 resolveSecretMaterial: options.resolveSecretMaterial,
272 getLocalServerBaseUrl: options.getLocalServerBaseUrl,270 getLocalServerBaseUrl: options.getLocalServerBaseUrl,
271 pluginRegistry,
273 }),272 }),
274 (runtime) =>273 (runtime) =>
275 Effect.gen(function* () {274 Effect.gen(function* () {
packages/platform/sdk/src/plugins.ts+313−36
@@ -6,19 +6,24 @@ import type { · 5 unmodified lines
6} from "@executor/source-core";6} from "@executor/source-core";
7import type { ExecutorEffect } from "./executor-effect";7import type { ExecutorEffect } from "./executor-effect";
8import type { ExecutorScopeContext } from "./scope";8import type { ExecutorScopeContext } from "./scope";
9import type { Source as ExecutorSource } from "./schema";9import {
10 type LocalConfigSource,
11 type LocalExecutorConfig,
12 type Source as ExecutorSource,
13} from "./schema";
10import type {14import type {
11 SourceInvokeInput,15 SourceInvokeInput,
12 SourceInvokeResult,16 SourceInvokeResult,
13} from "@executor/source-core";17} from "@executor/source-core";
14import type * as Schema from "effect/Schema";18import type * as Schema from "effect/Schema";
15import { runtimeEffectError } from "./runtime/effect-errors";19import { runtimeEffectError } from "./runtime/effect-errors";
20import { ScopeConfigStore } from "./runtime/scope/storage";
16 21
17export type PluginCleanup = {22export type PluginCleanup = {
18 close: () => void | Promise<void>;23 close: () => void | Promise<void>;
19};24};
20 25
21export type ExecutorSdkPluginHost = {};26export type ExecutorSdkPluginHost = ExecutorSourcePluginInternalHost;
22 27
23export type ExecutorSdkPluginContext = {28export type ExecutorSdkPluginContext = {
24 executor: ExecutorEffect & Record<string, unknown>;29 executor: ExecutorEffect & Record<string, unknown>;
@@ -34,6 +39,7 @@ export type ExecutorSdkPluginStartContext< · 9 unmodified lines
34 39
35type ExecutorSdkPluginInternals = {40type ExecutorSdkPluginInternals = {
36 sources?: readonly ExecutorSourceContribution<any>[];41 sources?: readonly ExecutorSourceContribution<any>[];
42 managementTools?: readonly ExecutorManagementToolContribution<any, any>[];
37};43};
38 44
39const executorSdkPluginInternalsSymbol = Symbol.for(45const executorSdkPluginInternalsSymbol = Symbol.for(
@@ -122,6 +128,45 @@ export type ExecutorSourcePluginApi< · 82 unmodified lines
122 ) => Effect.Effect<boolean, Error, any>;128 ) => Effect.Effect<boolean, Error, any>;
123};129};
124 130
131export 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
145export 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
125export type ExecutorSourcePluginDefinition<170export type ExecutorSourcePluginDefinition<
126 TAddInput,171 TAddInput,
127 TConnectInput,172 TConnectInput,
@@ -165,6 +210,17 @@ export type ExecutorSourcePluginDefinition< · 37 unmodified lines
165 stored: TStored | null;210 stored: TStored | null;
166 }) => Effect.Effect<void, Error, any>;211 }) => Effect.Effect<void, Error, any>;
167 };212 };
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 };
168 catalog: {224 catalog: {
169 kind: SourceCatalogKind;225 kind: SourceCatalogKind;
170 identity?: (input: {226 identity?: (input: {
@@ -206,6 +262,14 @@ export type ExecutorSourcePluginInput< · 35 unmodified lines
206 TStored,262 TStored,
207 TUpdateInput263 TUpdateInput
208 >;264 >;
265 tools?: readonly ExecutorSourcePluginManagementToolDefinition<
266 string,
267 any,
268 any,
269 TConnectInput,
270 TSourceConfig,
271 TUpdateInput
272 >[];
209 extendExecutor?: (input: ExecutorSdkPluginContext & {273 extendExecutor?: (input: ExecutorSdkPluginContext & {
210 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;274 source: ExecutorSourcePluginApi<TConnectInput, TSourceConfig, TUpdateInput>;
211 }) => TExtension;275 }) => TExtension;
@@ -239,6 +303,121 @@ const loadSourceOfKind = ( · 27 unmodified lines
239 return source;303 return source;
240 });304 });
241 305
306const cloneJson = <T>(value: T): T =>
307 JSON.parse(JSON.stringify(value)) as T;
308
309const 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
316const 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
361const 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
242const createExecutorSourcePluginApi = <421const createExecutorSourcePluginApi = <
243 TAddInput,422 TAddInput,
244 TConnectInput,423 TConnectInput,
@@ -269,10 +448,7 @@ const createExecutorSourcePluginApi = < · 24 unmodified lines
269 definition,448 definition,
270 host,449 host,
271 });450 });
272 const stored = yield* definition.storage.get({451 const stored = yield* loadStoredOrRecover(definition, source);
273 scopeId: source.scopeId,
274 sourceId: source.id,
275 });
276 if (stored === null) {452 if (stored === null) {
277 return yield* runtimeEffectError(453 return yield* runtimeEffectError(
278 "plugins",454 "plugins",
@@ -297,6 +473,10 @@ const createExecutorSourcePluginApi = < · 18 unmodified lines
297 sourceId: source.id,473 sourceId: source.id,
298 value: created.stored,474 value: created.stored,
299 });475 });
476 yield* persistSourceLocalConfig(definition, {
477 source,
478 stored: created.stored,
479 });
300 480
301 return yield* host.sources.refreshCatalog(source.id);481 return yield* host.sources.refreshCatalog(source.id);
302 }),482 }),
@@ -317,6 +497,10 @@ const createExecutorSourcePluginApi = < · 14 unmodified lines
317 sourceId: saved.id,497 sourceId: saved.id,
318 value: updated.stored,498 value: updated.stored,
319 });499 });
500 yield* persistSourceLocalConfig(definition, {
501 source: saved,
502 stored: updated.stored,
503 });
320 504
321 return yield* host.sources.refreshCatalog(saved.id);505 return yield* host.sources.refreshCatalog(saved.id);
322 }),506 }),
@@ -359,6 +543,7 @@ const createExecutorSourcePluginApi = < · 36 unmodified lines
359});543});
360 544
361type ExecutorSourceContribution<TInput = unknown> = {545type ExecutorSourceContribution<TInput = unknown> = {
546 pluginKey: string;
362 kind: string;547 kind: string;
363 displayName: string;548 displayName: string;
364 inputSchema: Schema.Schema<TInput, any, never>;549 inputSchema: Schema.Schema<TInput, any, never>;
@@ -382,7 +567,21 @@ type ExecutorSourceContribution<TInput = unknown> = { · 17 unmodified lines
382 ) => Effect.Effect<SourceInvokeResult, Error, any>;567 ) => Effect.Effect<SourceInvokeResult, Error, any>;
383};568};
384 569
570export 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
385const createExecutorSourceContribution = <583const createExecutorSourceContribution = <
584 TKey extends string,
386 TAddInput,585 TAddInput,
387 TConnectInput,586 TConnectInput,
388 TSourceConfig,587 TSourceConfig,
@@ -392,51 +591,96 @@ const createExecutorSourceContribution = < · 3 unmodified lines
392 config: TSourceConfig;591 config: TSourceConfig;
393 },592 },
394>(593>(
395 definition: ExecutorSourcePluginDefinition<594 input: {
396 TAddInput,595 key: TKey;
397 TConnectInput,596 source: ExecutorSourcePluginDefinition<
398 TSourceConfig,597 TAddInput,
399 TStored,598 TConnectInput,
400 TUpdateInput599 TSourceConfig,
401 >,600 TStored,
601 TUpdateInput
602 >;
603 },
402): ExecutorSourceContribution<TAddInput> => ({604): ExecutorSourceContribution<TAddInput> => ({
403 kind: definition.kind,605 pluginKey: input.key,
404 displayName: definition.displayName,606 kind: input.source.kind,
405 inputSchema: definition.add.inputSchema,607 displayName: input.source.displayName,
406 inputSignatureWidth: definition.add.inputSignatureWidth,608 inputSchema: input.source.add.inputSchema,
407 helpText: definition.add.helpText,609 inputSignatureWidth: input.source.add.inputSignatureWidth,
408 catalogKind: definition.catalog.kind,610 helpText: input.source.add.helpText,
409 catalogIdentity: definition.catalog.identity,611 catalogKind: input.source.catalog.kind,
612 catalogIdentity: input.source.catalog.identity,
410 createSource: ({ args, host }) =>613 createSource: ({ args, host }) =>
411 createExecutorSourcePluginApi(definition, host).createSource(614 createExecutorSourcePluginApi(input.source, host).createSource(
412 definition.add.toConnectInput(args),615 input.source.add.toConnectInput(args),
413 ),616 ),
414 syncCatalog: ({ source }) =>617 syncCatalog: ({ source }) =>
415 Effect.flatMap(618 Effect.flatMap(
416 definition.storage.get({619 loadStoredOrRecover(input.source, source),
417 scopeId: source.scopeId,
418 sourceId: source.id,
419 }),
420 (stored) =>620 (stored) =>
421 definition.catalog.sync({621 input.source.catalog.sync({
422 source,622 source,
423 stored,623 stored,
424 }),624 }),
425 ),625 ),
426 invoke: (input) =>626 invoke: (invokeInput) =>
427 Effect.flatMap(627 Effect.flatMap(
428 definition.storage.get({628 loadStoredOrRecover(input.source, invokeInput.source),
429 scopeId: input.source.scopeId,
430 sourceId: input.source.id,
431 }),
432 (stored) =>629 (stored) =>
433 definition.catalog.invoke({630 input.source.catalog.invoke({
434 ...input,631 ...invokeInput,
435 stored,632 stored,
436 }),633 }),
437 ),634 ),
438});635});
439 636
637const 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
440export const defineExecutorSourcePlugin = <684export const defineExecutorSourcePlugin = <
441 const TKey extends string,685 const TKey extends string,
442 TAddInput,686 TAddInput,
@@ -483,7 +727,17 @@ export const defineExecutorSourcePlugin = < · 40 unmodified lines
483 })727 })
484 : undefined,728 : undefined,
485 [executorSdkPluginInternalsSymbol]: {729 [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 ),
487 },741 },
488 }))(input.extendExecutor, input.start);742 }))(input.extendExecutor, input.start);
489 743
@@ -501,6 +755,10 @@ export const registerExecutorSdkPlugins = ( · 11 unmodified lines
501) => {755) => {
502 const pluginKeys = new Set<string>();756 const pluginKeys = new Set<string>();
503 const sources = new Map<string, ExecutorSourceContribution<any>>();757 const sources = new Map<string, ExecutorSourceContribution<any>>();
758 const managementTools = new Map<
759 string,
760 ExecutorManagementToolContribution<any, any>
761 >();
504 762
505 for (const plugin of plugins) {763 for (const plugin of plugins) {
506 if (pluginKeys.has(plugin.key)) {764 if (pluginKeys.has(plugin.key)) {
@@ -520,6 +778,14 @@ export const registerExecutorSdkPlugins = ( · 13 unmodified lines
520 778
521 sources.set(source.kind, source);779 sources.set(source.kind, source);
522 }780 }
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 }
523 }789 }
524 790
525 const getSourceContribution = (kind: string) => {791 const getSourceContribution = (kind: string) => {
@@ -534,10 +800,21 @@ export const registerExecutorSdkPlugins = ( · 8 unmodified lines
534 const getSourceContributionForSource = (source: Pick<ExecutorSource, "kind">) =>800 const getSourceContributionForSource = (source: Pick<ExecutorSource, "kind">) =>
535 getSourceContribution(source.kind);801 getSourceContribution(source.kind);
536 802
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
537 return {812 return {
538 plugins,813 plugins,
539 sources: [...sources.values()],814 sources: [...sources.values()],
815 managementTools: [...managementTools.values()],
540 getSourceContribution,816 getSourceContribution,
541 getSourceContributionForSource,817 getSourceContributionForSource,
542 };818 getManagementTool,
819 } satisfies ExecutorSdkPluginRegistry;
543};820};
packages/platform/sdk/src/runtime/catalog/source/reconcile.ts+26−1
@@ -3,6 +3,8 @@ import type { · 2 unmodified lines
3 Source,3 Source,
4} from "#schema";4} from "#schema";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
6 8
7import {9import {
8 RuntimeLocalScopeService,10 RuntimeLocalScopeService,
@@ -17,10 +19,33 @@ import { · 8 unmodified lines
17 RuntimeSourceCatalogSyncService,19 RuntimeSourceCatalogSyncService,
18} from "./sync";20} from "./sync";
19 21
22const decodeArtifactImportMetadataOption = Schema.decodeUnknownOption(
23 Schema.parseJson(
24 Schema.Struct({
25 sourceConfigHash: Schema.String,
26 }),
27 ),
28);
29
20const shouldReconcileSource = (source: Source): boolean =>30const shouldReconcileSource = (source: Source): boolean =>
21 source.enabled31 source.enabled
22 && source.status === "connected";32 && source.status === "connected";
23 33
34const artifactNeedsRefresh = (artifact: {
35 revision: {
36 importMetadataJson: string | null;
37 };
38} | null): boolean => {
39 if (artifact === null) {
40 return true;
41 }
42
43 const decoded = decodeArtifactImportMetadataOption(
44 artifact.revision.importMetadataJson,
45 );
46 return Option.isSome(decoded) && decoded.value.sourceConfigHash === "missing";
47};
48
24export const reconcileMissingSourceCatalogArtifacts = (input: {49export const reconcileMissingSourceCatalogArtifacts = (input: {
25 scopeId: ScopeId;50 scopeId: ScopeId;
26 actorScopeId?: ScopeId | null;51 actorScopeId?: ScopeId | null;
@@ -49,7 +74,7 @@ export const reconcileMissingSourceCatalogArtifacts = (input: { · 22 unmodified lines
49 const artifact = yield* sourceArtifactStore.read({74 const artifact = yield* sourceArtifactStore.read({
50 sourceId: source.id,75 sourceId: source.id,
51 });76 });
52 if (artifact !== null) {77 if (!artifactNeedsRefresh(artifact)) {
53 continue;78 continue;
54 }79 }
55 80
packages/platform/sdk/src/runtime/catalog/source/sync.ts+11−1
@@ -22,6 +22,7 @@ import { · 21 unmodified lines
22} from "../../scope-state";22} from "../../scope-state";
23import {23import {
24 getSourceContributionForSource,24 getSourceContributionForSource,
25 ExecutorPluginRegistryService,
25} from "../../sources/source-plugins";26} from "../../sources/source-plugins";
26import {27import {
27 snapshotFromSourceCatalogSyncResult,28 snapshotFromSourceCatalogSyncResult,
@@ -39,6 +40,7 @@ const shouldIndexSource = (source: Source): boolean => · 11 unmodified lines
39 && source.status === "connected";40 && source.status === "connected";
40 41
41type RuntimeSourceCatalogSyncDeps = {42type RuntimeSourceCatalogSyncDeps = {
43 pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
42 runtimeLocalScope: RuntimeLocalScopeState;44 runtimeLocalScope: RuntimeLocalScopeState;
43 scopeStateStore: ScopeStateStoreShape;45 scopeStateStore: ScopeStateStoreShape;
44 sourceArtifactStore: SourceArtifactStoreShape;46 sourceArtifactStore: SourceArtifactStoreShape;
@@ -46,6 +48,7 @@ type RuntimeSourceCatalogSyncDeps = { · 1 unmodified line
46};48};
47 49
48type SourceCatalogSyncServices =50type SourceCatalogSyncServices =
51 | ExecutorPluginRegistryService
49 | RuntimeLocalScopeService52 | RuntimeLocalScopeService
50 | ScopeStateStore53 | ScopeStateStore
51 | SourceArtifactStore54 | SourceArtifactStore
@@ -112,7 +115,10 @@ const syncSourceCatalogWithDeps = ( · 60 unmodified lines
112 return;115 return;
113 }116 }
114 117
115 const definition = getSourceContributionForSource(input.source);118 const definition = getSourceContributionForSource(
119 deps.pluginRegistry,
120 input.source,
121 );
116 const irModel = yield* definition.syncCatalog({122 const irModel = yield* definition.syncCatalog({
117 source: input.source,123 source: input.source,
118 });124 });
@@ -165,12 +171,14 @@ export const syncSourceCatalog = (input: { · 46 unmodified lines
165}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>171}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
166 Effect.gen(function* () {172 Effect.gen(function* () {
167 const runtimeLocalScope = yield* RuntimeLocalScopeService;173 const runtimeLocalScope = yield* RuntimeLocalScopeService;
174 const pluginRegistry = yield* ExecutorPluginRegistryService;
168 const scopeStateStore = yield* ScopeStateStore;175 const scopeStateStore = yield* ScopeStateStore;
169 const sourceArtifactStore = yield* SourceArtifactStore;176 const sourceArtifactStore = yield* SourceArtifactStore;
170 const sourceTypeDeclarationsRefresher =177 const sourceTypeDeclarationsRefresher =
171 yield* SourceTypeDeclarationsRefresherService;178 yield* SourceTypeDeclarationsRefresherService;
172 return yield* syncSourceCatalogWithDeps(179 return yield* syncSourceCatalogWithDeps(
173 {180 {
181 pluginRegistry,
174 runtimeLocalScope,182 runtimeLocalScope,
175 scopeStateStore,183 scopeStateStore,
176 sourceArtifactStore,184 sourceArtifactStore,
@@ -187,11 +195,13 @@ export const RuntimeSourceCatalogSyncLive = Layer.effect( · 10 unmodified lines
187 RuntimeSourceCatalogSyncService,195 RuntimeSourceCatalogSyncService,
188 Effect.gen(function* () {196 Effect.gen(function* () {
189 const runtimeLocalScope = yield* RuntimeLocalScopeService;197 const runtimeLocalScope = yield* RuntimeLocalScopeService;
198 const pluginRegistry = yield* ExecutorPluginRegistryService;
190 const scopeStateStore = yield* ScopeStateStore;199 const scopeStateStore = yield* ScopeStateStore;
191 const sourceArtifactStore = yield* SourceArtifactStore;200 const sourceArtifactStore = yield* SourceArtifactStore;
192 const sourceTypeDeclarationsRefresher =201 const sourceTypeDeclarationsRefresher =
193 yield* SourceTypeDeclarationsRefresherService;202 yield* SourceTypeDeclarationsRefresherService;
194 const deps: RuntimeSourceCatalogSyncDeps = {203 const deps: RuntimeSourceCatalogSyncDeps = {
204 pluginRegistry,
195 runtimeLocalScope,205 runtimeLocalScope,
196 scopeStateStore,206 scopeStateStore,
197 sourceArtifactStore,207 sourceArtifactStore,
packages/platform/sdk/src/runtime/execution/ir-execution.ts+8−1
@@ -13,6 +13,9 @@ import type { · 12 unmodified lines
13import {13import {
14 getSourceContribution,14 getSourceContribution,
15} from "../sources/source-plugins";15} from "../sources/source-plugins";
16import type {
17 ExecutorSdkPluginRegistry,
18} from "../../plugins";
16import {19import {
17 runtimeEffectError,20 runtimeEffectError,
18} from "../effect-errors";21} from "../effect-errors";
@@ -49,6 +52,7 @@ export const invocationDescriptorFromTool = (input: { · 30 unmodified lines
49});52});
50 53
51export const invokeIrTool = (input: {54export const invokeIrTool = (input: {
55 pluginRegistry: ExecutorSdkPluginRegistry;
52 scopeId: Source["scopeId"];56 scopeId: Source["scopeId"];
53 actorScopeId: ScopeId;57 actorScopeId: ScopeId;
54 tool: LoadedSourceCatalogToolIndexEntry;58 tool: LoadedSourceCatalogToolIndexEntry;
@@ -56,7 +60,10 @@ export const invokeIrTool = (input: { · 1 unmodified line
56 onElicitation?: OnElicitation;60 onElicitation?: OnElicitation;
57 context?: Record<string, unknown>;61 context?: Record<string, unknown>;
58}) => {62}) => {
59 const definition = getSourceContribution(input.tool.executable.pluginKey);63 const definition = getSourceContribution(
64 input.pluginRegistry,
65 input.tool.executable.pluginKey,
66 );
60 if (definition.kind !== input.tool.source.kind) {67 if (definition.kind !== input.tool.source.kind) {
61 return Effect.fail(68 return Effect.fail(
62 runtimeEffectError("execution/ir-execution", 69 runtimeEffectError("execution/ir-execution",
packages/platform/sdk/src/runtime/execution/scope/environment.ts+7−0
@@ -47,6 +47,9 @@ import { · 46 unmodified lines
47import {47import {
48 RuntimeSourceStoreService,48 RuntimeSourceStoreService,
49} from "../../sources/source-store";49} from "../../sources/source-store";
50import {
51 ExecutorPluginRegistryService,
52} from "../../sources/source-plugins";
50import {53import {
51 SecretMaterialDeleterService,54 SecretMaterialDeleterService,
52 SecretMaterialResolverService,55 SecretMaterialResolverService,
@@ -71,6 +74,7 @@ const createEmptyLocalToolRuntime = (): LocalToolRuntime => ({ · 18 unmodified lines
71 74
72export const createScopeExecutionEnvironmentResolver =75export const createScopeExecutionEnvironmentResolver =
73 (input: {76 (input: {
77 pluginRegistry: Effect.Effect.Success<typeof ExecutorPluginRegistryService>;
74 executorStateStore: Effect.Effect.Success<typeof ExecutorStateStore>;78 executorStateStore: Effect.Effect.Success<typeof ExecutorStateStore>;
75 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;79 sourceStore: Effect.Effect.Success<typeof RuntimeSourceStoreService>;
76 sourceCatalogSyncService: Effect.Effect.Success<80 sourceCatalogSyncService: Effect.Effect.Success<
@@ -103,6 +107,7 @@ export const createScopeExecutionEnvironmentResolver = · 26 unmodified lines
103 ? createEmptyLocalToolRuntime()107 ? createEmptyLocalToolRuntime()
104 : yield* input.localToolRuntimeLoader.load();108 : yield* input.localToolRuntimeLoader.load();
105 const { catalog, toolInvoker } = createScopeToolInvoker({109 const { catalog, toolInvoker } = createScopeToolInvoker({
110 pluginRegistry: input.pluginRegistry,
106 scopeId,111 scopeId,
107 actorScopeId,112 actorScopeId,
108 executorStateStore: input.executorStateStore,113 executorStateStore: input.executorStateStore,
@@ -147,6 +152,7 @@ export const RuntimeExecutionResolverLive = ( · 38 unmodified lines
147 : Layer.effect(152 : Layer.effect(
148 RuntimeExecutionResolverService,153 RuntimeExecutionResolverService,
149 Effect.gen(function* () {154 Effect.gen(function* () {
155 const pluginRegistry = yield* ExecutorPluginRegistryService;
150 const executorStateStore = yield* ExecutorStateStore;156 const executorStateStore = yield* ExecutorStateStore;
151 const sourceStore = yield* RuntimeSourceStoreService;157 const sourceStore = yield* RuntimeSourceStoreService;
152 const sourceCatalogSyncService =158 const sourceCatalogSyncService =
@@ -163,6 +169,7 @@ export const RuntimeExecutionResolverLive = ( · 10 unmodified lines
163 const updateSecretMaterial = yield* SecretMaterialUpdaterService;169 const updateSecretMaterial = yield* SecretMaterialUpdaterService;
164 170
165 return createScopeExecutionEnvironmentResolver({171 return createScopeExecutionEnvironmentResolver({
172 pluginRegistry,
166 executorStateStore,173 executorStateStore,
167 sourceStore,174 sourceStore,
168 sourceCatalogSyncService,175 sourceCatalogSyncService,
packages/platform/sdk/src/runtime/execution/scope/tool-invoker.ts+7−0
@@ -45,6 +45,9 @@ import type { · 44 unmodified lines
45import {45import {
46 type RuntimeSourceStore,46 type RuntimeSourceStore,
47} from "../../sources/source-store";47} from "../../sources/source-store";
48import type {
49 ExecutorSdkPluginRegistry,
50} from "../../../plugins";
48import {51import {
49 createExecutorToolMap,52 createExecutorToolMap,
50} from "../../sources/executor-tools";53} from "../../sources/executor-tools";
@@ -70,6 +73,7 @@ import { · 19 unmodified lines
70import * as Layer from "effect/Layer";73import * as Layer from "effect/Layer";
71 74
72export const createScopeToolInvoker = (input: {75export const createScopeToolInvoker = (input: {
76 pluginRegistry: ExecutorSdkPluginRegistry;
73 scopeId: Source["scopeId"];77 scopeId: Source["scopeId"];
74 actorScopeId: ScopeId;78 actorScopeId: ScopeId;
75 executorStateStore: ExecutorStateStoreShape;79 executorStateStore: ExecutorStateStoreShape;
@@ -117,6 +121,7 @@ export const createScopeToolInvoker = (input: { · 41 unmodified lines
117 ) => effect.pipe(Effect.provide(secretMaterialLayer));121 ) => effect.pipe(Effect.provide(secretMaterialLayer));
118 122
119 const executorTools = createExecutorToolMap({123 const executorTools = createExecutorToolMap({
124 pluginRegistry: input.pluginRegistry,
120 scopeId: input.scopeId,125 scopeId: input.scopeId,
121 actorScopeId: input.actorScopeId,126 actorScopeId: input.actorScopeId,
122 executorStateStore: input.executorStateStore,127 executorStateStore: input.executorStateStore,
@@ -127,6 +132,7 @@ export const createScopeToolInvoker = (input: { · 4 unmodified lines
127 scopeStateStore: input.scopeStateStore,132 scopeStateStore: input.scopeStateStore,
128 sourceArtifactStore: input.sourceArtifactStore,133 sourceArtifactStore: input.sourceArtifactStore,
129 runtimeLocalScope: input.runtimeLocalScope,134 runtimeLocalScope: input.runtimeLocalScope,
135 secretMaterialServices: input.secretMaterialServices,
130 });136 });
131 const sourceCatalog = createScopeSourceCatalog({137 const sourceCatalog = createScopeSourceCatalog({
132 scopeId: input.scopeId,138 scopeId: input.scopeId,
@@ -196,6 +202,7 @@ export const createScopeToolInvoker = (input: { · 63 unmodified lines
196 });202 });
197 203
198 return yield* invokeIrTool({204 return yield* invokeIrTool({
205 pluginRegistry: input.pluginRegistry,
199 scopeId: input.scopeId,206 scopeId: input.scopeId,
200 actorScopeId: input.actorScopeId,207 actorScopeId: input.actorScopeId,
201 tool: catalogTool,208 tool: catalogTool,
packages/platform/sdk/src/runtime/index.ts+37−3
@@ -40,6 +40,9 @@ import type { · 39 unmodified lines
40import type {40import type {
41 InstanceConfig,41 InstanceConfig,
42} from "../local/contracts";42} from "../local/contracts";
43import type {
44 ExecutorSdkPluginRegistry,
45} from "../plugins";
43import type {46import type {
44 ExecutorScopeContext,47 ExecutorScopeContext,
45 ExecutorScopeDescriptor,48 ExecutorScopeDescriptor,
@@ -87,6 +90,10 @@ import { · 41 unmodified lines
87import {90import {
88 RuntimeSourceStoreLive,91 RuntimeSourceStoreLive,
89} from "./sources/source-store";92} from "./sources/source-store";
93import {
94 emptyExecutorPluginRegistry,
95 ExecutorPluginRegistryLive,
96} from "./sources/source-plugins";
90 97
91export * from "./execution/state";98export * from "./execution/state";
92export * from "./sources/executor-tools";99export * from "./sources/executor-tools";
@@ -105,6 +112,8 @@ export * from "./source-artifacts"; · 12 unmodified lines
105export * from "./scope-config";112export * from "./scope-config";
106export * from "./scope-state";113export * from "./scope-state";
107export * from "./scope/config-secrets";114export * from "./scope/config-secrets";
115export * from "./sources/source-store/config";
116export * from "./sources/source-store/plugin-local-config";
108export {117export {
109 LocalInstanceConfigService,118 LocalInstanceConfigService,
110 SecretMaterialDeleterService,119 SecretMaterialDeleterService,
@@ -137,7 +146,10 @@ export type { · 26 unmodified lines
137} from "./local-tool-runtime";146} from "./local-tool-runtime";
138export {147export {
139 registeredSourceContributions,148 registeredSourceContributions,
149 registeredManagementToolContributions,
140 hasRegisteredExternalSourcePlugins,150 hasRegisteredExternalSourcePlugins,
151 ExecutorPluginRegistryLive,
152 ExecutorPluginRegistryService,
141 getSourceContribution,153 getSourceContribution,
142 getSourceContributionForSource,154 getSourceContributionForSource,
143} from "./sources/source-plugins";155} from "./sources/source-plugins";
@@ -146,6 +158,7 @@ export type ExecutorRuntimeOptions = { · 2 unmodified lines
146 executionResolver?: ResolveExecutionEnvironment;158 executionResolver?: ResolveExecutionEnvironment;
147 resolveSecretMaterial?: ResolveSecretMaterial;159 resolveSecretMaterial?: ResolveSecretMaterial;
148 getLocalServerBaseUrl?: () => string | undefined;160 getLocalServerBaseUrl?: () => string | undefined;
161 pluginRegistry?: ExecutorSdkPluginRegistry;
149};162};
150 163
151type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;164type ResolveExecutionEnvironment = import("./execution/state").ResolveExecutionEnvironment;
@@ -181,7 +194,11 @@ export type BoundScopeStateStore = { · 29 unmodified lines
181};194};
182 195
183export type BoundSourceArtifactStore = {196export 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;
185 read: (202 read: (
186 sourceId: string,203 sourceId: string,
187 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;204 ) => Effect.Effect<LocalSourceArtifact | null, Error, never>;
@@ -289,8 +306,14 @@ const toScopeStateStoreShape = ( · 101 unmodified lines
289 306
290const toSourceArtifactStoreShape = (307const toSourceArtifactStoreShape = (
291 input: BoundSourceArtifactStore,308 input: BoundSourceArtifactStore,
309 pluginRegistry: ExecutorSdkPluginRegistry,
292): SourceArtifactStoreShape => ({310): SourceArtifactStoreShape => ({
293 build: input.build,311 build: ({ source, syncResult }) =>
312 input.build({
313 source,
314 syncResult,
315 pluginRegistry,
316 }),
294 read: ({ sourceId }) => input.read(sourceId),317 read: ({ sourceId }) => input.read(sourceId),
295 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),318 write: ({ sourceId, artifact }) => input.write({ sourceId, artifact }),
296 remove: ({ sourceId }) => input.remove(sourceId),319 remove: ({ sourceId }) => input.remove(sourceId),
@@ -322,11 +345,16 @@ export const createExecutorRuntimeLayer = ( · 25 unmodified lines
322 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;345 liveExecutionManager: ReturnType<typeof createLiveExecutionManager>;
323 },346 },
324) => {347) => {
348 const pluginRegistry =
349 input.pluginRegistry ?? emptyExecutorPluginRegistry();
325 const storageLayer = makeLocalStorageLayer({350 const storageLayer = makeLocalStorageLayer({
326 installationStore: toInstallationStoreShape(input.storage.installation),351 installationStore: toInstallationStoreShape(input.storage.installation),
327 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),352 scopeConfigStore: toScopeConfigStoreShape(input.storage.scopeConfig),
328 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),353 scopeStateStore: toScopeStateStoreShape(input.storage.scopeState),
329 sourceArtifactStore: toSourceArtifactStoreShape(input.storage.sourceArtifacts),354 sourceArtifactStore: toSourceArtifactStoreShape(
355 input.storage.sourceArtifacts,
356 pluginRegistry,
357 ),
330 });358 });
331 const localToolRuntimeLayer = Layer.succeed(359 const localToolRuntimeLayer = Layer.succeed(
332 LocalToolRuntimeLoaderService,360 LocalToolRuntimeLoaderService,
@@ -345,6 +373,7 @@ export const createExecutorRuntimeLayer = ( · 12 unmodified lines
345 373
346 const baseLayer = Layer.mergeAll(374 const baseLayer = Layer.mergeAll(
347 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),375 Layer.succeed(ExecutorStateStore, toExecutorStateStoreShape(input.storage)),
376 ExecutorPluginRegistryLive(pluginRegistry),
348 RuntimeLocalScopeLive(input.localScopeState),377 RuntimeLocalScopeLive(input.localScopeState),
349 storageLayer,378 storageLayer,
350 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),379 Layer.succeed(LiveExecutionManagerService, input.liveExecutionManager),
@@ -402,6 +431,7 @@ export type ExecutorRuntime = { · 51 unmodified lines
402 scope: ExecutorScopeContext;431 scope: ExecutorScopeContext;
403 storage: RuntimeStorageServices;432 storage: RuntimeStorageServices;
404 localInstallation: LocalInstallation;433 localInstallation: LocalInstallation;
434 pluginRegistry: ExecutorSdkPluginRegistry;
405 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;435 managedRuntime: ManagedRuntime.ManagedRuntime<any, never>;
406 runtimeLayer: ExecutorRuntimeLayer;436 runtimeLayer: ExecutorRuntimeLayer;
407 close: () => Promise<void>;437 close: () => Promise<void>;
@@ -417,6 +447,8 @@ export const createExecutorRuntimeFromServices = (input: { · 9 unmodified lines
417 services: ExecutorRuntimeServices;447 services: ExecutorRuntimeServices;
418} & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>448} & ExecutorRuntimeOptions): Effect.Effect<ExecutorRuntime, Error> =>
419 (Effect.gen(function* () {449 (Effect.gen(function* () {
450 const pluginRegistry =
451 input.pluginRegistry ?? emptyExecutorPluginRegistry();
420 const localInstallation = yield* input.services.storage.installation452 const localInstallation = yield* input.services.storage.installation
421 .getOrProvision()453 .getOrProvision()
422 .pipe(Effect.mapError(toRuntimeBootstrapError));454 .pipe(Effect.mapError(toRuntimeBootstrapError));
@@ -449,6 +481,7 @@ export const createExecutorRuntimeFromServices = (input: { · 26 unmodified lines
449 ),481 ),
450 sourceArtifactStore: toSourceArtifactStoreShape(482 sourceArtifactStore: toSourceArtifactStoreShape(
451 input.services.storage.sourceArtifacts,483 input.services.storage.sourceArtifacts,
484 pluginRegistry,
452 ),485 ),
453 }),486 }),
454 ),487 ),
@@ -491,6 +524,7 @@ export const createExecutorRuntimeFromServices = (input: { · 36 unmodified lines
491 scope: runtimeWorkspace,524 scope: runtimeWorkspace,
492 storage: input.services.storage,525 storage: input.services.storage,
493 localInstallation,526 localInstallation,
527 pluginRegistry,
494 managedRuntime,528 managedRuntime,
495 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,529 runtimeLayer: concreteRuntimeLayer as ExecutorRuntimeLayer,
496 close: async () => {530 close: async () => {
packages/platform/sdk/src/runtime/source-artifacts.ts+9−1
@@ -27,6 +27,9 @@ import { · 26 unmodified lines
27 createSourceCatalogRevisionRecord,27 createSourceCatalogRevisionRecord,
28 stableSourceCatalogId,28 stableSourceCatalogId,
29} from "./sources/source-definitions";29} from "./sources/source-definitions";
30import type {
31 ExecutorSdkPluginRegistry,
32} from "../plugins";
30 33
31const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const;34const LEGACY_LOCAL_SOURCE_ARTIFACT_VERSION = 3 as const;
32const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const;35const LOCAL_SOURCE_ARTIFACT_VERSION = 4 as const;
@@ -200,14 +203,19 @@ export const decodeStoredLocalSourceArtifact = ( · 167 unmodified lines
200export const buildLocalSourceArtifact = (input: {203export const buildLocalSourceArtifact = (input: {
201 source: Source;204 source: Source;
202 syncResult: SourceCatalogSyncResult;205 syncResult: SourceCatalogSyncResult;
206 pluginRegistry?: ExecutorSdkPluginRegistry;
203}): LocalSourceArtifact => {207}): LocalSourceArtifact => {
204 const catalogId: SourceCatalogId = stableSourceCatalogId(input.source);208 const catalogId: SourceCatalogId = stableSourceCatalogId(
209 input.source,
210 input.pluginRegistry,
211 );
205 const generatedAt = Date.now();212 const generatedAt = Date.now();
206 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);213 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);
207 const importHash = importMetadataHash(snapshot);214 const importHash = importMetadataHash(snapshot);
208 const hash = snapshotHash(snapshot);215 const hash = snapshotHash(snapshot);
209 const revision = createSourceCatalogRevisionRecord({216 const revision = createSourceCatalogRevisionRecord({
210 source: input.source,217 source: input.source,
218 pluginRegistry: input.pluginRegistry,
211 catalogId,219 catalogId,
212 revisionNumber: 1,220 revisionNumber: 1,
213 importMetadataJson: JSON.stringify(snapshot.import),221 importMetadataJson: JSON.stringify(snapshot.import),
packages/platform/sdk/src/runtime/sources/executor-tools.test.ts+290−0
@@ -0,0 +1,290 @@
1import {
2 createServer,
3} from "node:http";
4import type {
5 AddressInfo,
6} from "node:net";
7import {
8 FileSystem,
9} from "@effect/platform";
10import {
11 NodeFileSystem,
12} from "@effect/platform-node";
13import {
14 describe,
15 expect,
16 it,
17} from "@effect/vitest";
18import * as Effect from "effect/Effect";
19
20import {
21 createLocalExecutorEffect,
22} from "@executor/platform-sdk-file/effect";
23import {
24 ExecutionIdSchema,
25} from "../../schema";
26import {
27 RuntimeExecutionResolverService,
28} from "../execution/scope/environment";
29import {
30 openApiSdkPlugin,
31} from "../../../../../../plugins/openapi/sdk";
32
33const staticOpenApiDocument = JSON.stringify({
34 openapi: "3.1.0",
35 info: {
36 title: "Demo API",
37 version: "1.0.0",
38 },
39 servers: [
40 {
41 url: "https://api.example.test",
42 },
43 ],
44 paths: {
45 "/widgets": {
46 get: {
47 operationId: "listWidgets",
48 responses: {
49 "200": {
50 description: "ok",
51 },
52 },
53 },
54 },
55 },
56});
57
58const startStaticSpecServer = async () => {
59 const server = createServer((request, response) => {
60 if (request.url !== "/openapi.json") {
61 response.writeHead(404);
62 response.end("Not Found");
63 return;
64 }
65
66 response.writeHead(200, {
67 "content-type": "application/json; charset=utf-8",
68 });
69 response.end(staticOpenApiDocument);
70 });
71
72 await new Promise<void>((resolve, reject) => {
73 server.once("error", reject);
74 server.listen(0, "127.0.0.1", () => {
75 server.off("error", reject);
76 resolve();
77 });
78 });
79
80 const address = server.address();
81 if (!address || typeof address === "string") {
82 server.close();
83 throw new Error("Failed to determine OpenAPI test server address.");
84 }
85
86 return {
87 specUrl: `http://127.0.0.1:${(address as AddressInfo).port}/openapi.json`,
88 close: () =>
89 new Promise<void>((resolve, reject) => {
90 server.close((error) => {
91 if (error) {
92 reject(error);
93 return;
94 }
95
96 resolve();
97 });
98 }),
99 };
100};
101
102const resolveExecutionEnvironment = (executor: {
103 scopeId: string;
104 actorScopeId: string;
105 runtime: {
106 runtimeLayer: any;
107 };
108}) =>
109 Effect.gen(function* () {
110 const resolver = yield* RuntimeExecutionResolverService;
111 return yield* resolver({
112 scopeId: executor.scopeId,
113 actorScopeId: executor.actorScopeId,
114 executionId: ExecutionIdSchema.make("exec_executor_tools_test"),
115 });
116 }).pipe(Effect.provide(executor.runtime.runtimeLayer));
117
118describe("executor internal tools", () => {
119 it.scoped("registers plugin-owned executor.openapi tools and delegates generic remove through them", () =>
120 Effect.gen(function* () {
121 const fs = yield* FileSystem.FileSystem;
122 const workspaceRoot = yield* fs.makeTempDirectory({
123 prefix: "executor-internal-tools-",
124 });
125 const stored = new Map<string, Record<string, unknown>>();
126 const specServer = yield* Effect.acquireRelease(
127 Effect.tryPromise({
128 try: () => startStaticSpecServer(),
129 catch: (cause) =>
130 cause instanceof Error ? cause : new Error(String(cause)),
131 }),
132 (server) =>
133 Effect.promise(() => server.close()).pipe(Effect.orDie),
134 );
135 const executor = yield* Effect.acquireRelease(
136 createLocalExecutorEffect({
137 localDataDir: ":memory:",
138 workspaceRoot,
139 plugins: [
140 openApiSdkPlugin({
141 storage: {
142 get: ({ sourceId }) =>
143 Effect.succeed((stored.get(sourceId) as any) ?? null),
144 put: ({ sourceId, value }) =>
145 Effect.sync(() => {
146 stored.set(sourceId, value as any);
147 }),
148 remove: ({ sourceId }) =>
149 Effect.sync(() => {
150 stored.delete(sourceId);
151 }),
152 },
153 }),
154 ] as const,
155 }),
156 (openedExecutor) =>
157 Effect.promise(() => openedExecutor.close()).pipe(
158 Effect.orDie,
159 Effect.zipRight(
160 fs.remove(workspaceRoot, {
161 recursive: true,
162 force: true,
163 }),
164 ),
165 ),
166 );
167
168 const environment = yield* resolveExecutionEnvironment(executor);
169 expect(environment.catalog).toBeDefined();
170
171 const createTool = yield* environment.catalog!.getToolByPath({
172 path: "executor.openapi.createSource",
173 includeSchemas: false,
174 });
175 const previewTool = yield* environment.catalog!.getToolByPath({
176 path: "executor.openapi.previewDocument",
177 includeSchemas: false,
178 });
179
180 expect(createTool?.path).toBe("executor.openapi.createSource");
181 expect(previewTool?.path).toBe("executor.openapi.previewDocument");
182
183 const preview = yield* environment.toolInvoker.invoke({
184 path: "executor.openapi.previewDocument",
185 args: {
186 specUrl: specServer.specUrl,
187 },
188 });
189 expect(preview).toMatchObject({
190 title: "Demo API",
191 operationCount: 1,
192 });
193
194 const created = yield* environment.toolInvoker.invoke({
195 path: "executor.openapi.createSource",
196 args: {
197 name: "Demo API",
198 specUrl: specServer.specUrl,
199 baseUrl: null,
200 auth: {
201 kind: "none",
202 },
203 },
204 });
205
206 expect((created as { kind: string }).kind).toBe("openapi");
207 expect(stored.has((created as { id: string }).id)).toBe(true);
208
209 const removed = yield* environment.toolInvoker.invoke({
210 path: "executor.sources.remove",
211 args: {
212 sourceId: (created as { id: string }).id,
213 },
214 });
215
216 expect(removed).toEqual({
217 removed: true,
218 });
219 expect(stored.has((created as { id: string }).id)).toBe(false);
220 }).pipe(Effect.provide(NodeFileSystem.layer)),
221 );
222
223 it.scoped("keeps executor.sources.add as a compatibility alias", () =>
224 Effect.gen(function* () {
225 const fs = yield* FileSystem.FileSystem;
226 const workspaceRoot = yield* fs.makeTempDirectory({
227 prefix: "executor-add-alias-",
228 });
229 const specServer = yield* Effect.acquireRelease(
230 Effect.tryPromise({
231 try: () => startStaticSpecServer(),
232 catch: (cause) =>
233 cause instanceof Error ? cause : new Error(String(cause)),
234 }),
235 (server) =>
236 Effect.promise(() => server.close()).pipe(Effect.orDie),
237 );
238 const executor = yield* Effect.acquireRelease(
239 createLocalExecutorEffect({
240 localDataDir: ":memory:",
241 workspaceRoot,
242 plugins: [
243 openApiSdkPlugin({
244 storage: {
245 get: () => Effect.succeed(null),
246 put: () => Effect.void,
247 remove: () => Effect.void,
248 },
249 }),
250 ] as const,
251 }),
252 (openedExecutor) =>
253 Effect.promise(() => openedExecutor.close()).pipe(
254 Effect.orDie,
255 Effect.zipRight(
256 fs.remove(workspaceRoot, {
257 recursive: true,
258 force: true,
259 }),
260 ),
261 ),
262 );
263
264 const environment = yield* resolveExecutionEnvironment(executor);
265 const aliasTool = yield* environment.catalog!.getToolByPath({
266 path: "executor.sources.add",
267 includeSchemas: false,
268 });
269 expect(aliasTool?.path).toBe("executor.sources.add");
270
271 const created = yield* environment.toolInvoker.invoke({
272 path: "executor.sources.add",
273 args: {
274 kind: "openapi",
275 name: "Alias API",
276 specUrl: specServer.specUrl,
277 baseUrl: null,
278 auth: {
279 kind: "none",
280 },
281 },
282 });
283
284 expect(created).toMatchObject({
285 kind: "openapi",
286 name: "Alias API",
287 });
288 }).pipe(Effect.provide(NodeFileSystem.layer)),
289 );
290});
packages/platform/sdk/src/runtime/sources/executor-tools.ts+349−33
@@ -1,6 +1,7 @@
1import { toTool, type ToolMap } from "@executor/codemode-core";1import { toTool, type ToolMap } from "@executor/codemode-core";
2import {2import {
3 type ScopeId,3 type ScopeId,
4 SourceIdSchema,
4 SourceSchema,5 SourceSchema,
5 type Source,6 type Source,
6} from "#schema";7} from "#schema";
@@ -9,12 +10,16 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines
9import * as Option from "effect/Option";10import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
11 12
13import type {
14 ExecutorSdkPluginRegistry,
15} from "../../plugins";
12import {16import {
13 createManagedSourceRecord,
14 getSource,17 getSource,
18 listSources,
15 refreshManagedSourceCatalog,19 refreshManagedSourceCatalog,
16 removeSource,20 removeSource,
17 saveManagedSourceRecord,21 saveManagedSourceRecord,
22 createManagedSourceRecord,
18} from "../../sources/operations";23} from "../../sources/operations";
19import {24import {
20 deriveSchemaJson,25 deriveSchemaJson,
@@ -31,23 +36,48 @@ import { · 10 unmodified lines
31 provideOptionalRuntimeLocalScope,36 provideOptionalRuntimeLocalScope,
32 type RuntimeLocalScopeState,37 type RuntimeLocalScopeState,
33} from "../scope/runtime-context";38} from "../scope/runtime-context";
39import {
40 SecretMaterialDeleterService,
41 SecretMaterialResolverService,
42 SecretMaterialStorerService,
43 SecretMaterialUpdaterService,
44 type DeleteSecretMaterial,
45 type ResolveSecretMaterial,
46 type StoreSecretMaterial,
47 type UpdateSecretMaterial,
48} from "../scope/secret-material-providers";
34import {49import {
35 type InstallationStoreShape,50 type InstallationStoreShape,
36 makeLocalStorageLayer,51 makeLocalStorageLayer,
37 type SourceArtifactStoreShape,
38 type ScopeConfigStoreShape,52 type ScopeConfigStoreShape,
39 type ScopeStateStoreShape,53 type ScopeStateStoreShape,
54 type SourceArtifactStoreShape,
40} from "../scope/storage";55} from "../scope/storage";
56import {
57 registeredManagementToolContributions,
58 registeredSourceContributions,
59} from "./source-plugins";
41import {60import {
42 type RuntimeSourceStore,61 type RuntimeSourceStore,
43 RuntimeSourceStoreService,62 RuntimeSourceStoreService,
44} from "./source-store";63} from "./source-store";
45import {
46 registeredSourceContributions,
47} from "./source-plugins";
48 64
49const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => {65const SourceArraySchema = Schema.Array(SourceSchema);
50 const sources = registeredSourceContributions();66const SourceIdInputSchema = Schema.Struct({
67 sourceId: SourceIdSchema,
68});
69const SourceRemoveResultSchema = Schema.Struct({
70 removed: Schema.Boolean,
71});
72
73type RegisteredSourceContribution =
74 ExecutorSdkPluginRegistry["sources"][number];
75type RegisteredManagementToolContribution =
76 ExecutorSdkPluginRegistry["managementTools"][number];
77
78const createExecutorSourcesAddSchema = (
79 sources: ReadonlyArray<RegisteredSourceContribution>,
80): Schema.Schema<any, any, never> => {
51 if (sources.length === 0) {81 if (sources.length === 0) {
52 return Schema.Unknown;82 return Schema.Unknown;
53 }83 }
@@ -61,27 +91,78 @@ const createExecutorSourcesAddSchema = (): Schema.Schema<any, any, never> => { · 7 unmodified lines
61 Schema.Schema<any, any, never>,91 Schema.Schema<any, any, never>,
62 Schema.Schema<any, any, never>,92 Schema.Schema<any, any, never>,
63 ...Array<Schema.Schema<any, any, never>>,93 ...Array<Schema.Schema<any, any, never>>,
64 ])94 ]),
65 );95 );
66};96};
67 97
68export const getExecutorSourcesAddInputHint = (): string =>98export const getExecutorSourcesAddInputHint = (
69 deriveSchemaTypeSignature(createExecutorSourcesAddSchema(), 340);99 pluginRegistry: ExecutorSdkPluginRegistry,
100): string =>
101 deriveSchemaTypeSignature(
102 createExecutorSourcesAddSchema(
103 registeredSourceContributions(pluginRegistry),
104 ),
105 340,
106 );
70 107
71export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature(108export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature(
72 SourceSchema,109 SourceSchema,
73 260,110 260,
74);111);
75 112
76export const getExecutorSourcesAddInputSchemaJson = (): Record<string, unknown> =>113export const getExecutorSourcesAddInputSchemaJson = (
77 deriveSchemaJson(createExecutorSourcesAddSchema()) ?? {};114 pluginRegistry: ExecutorSdkPluginRegistry,
115): Record<string, unknown> =>
116 deriveSchemaJson(
117 createExecutorSourcesAddSchema(
118 registeredSourceContributions(pluginRegistry),
119 ),
120 ) ?? {};
78 121
79export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(122export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(
80 SourceSchema,123 SourceSchema,
81) ?? {};124) ?? {};
82 125
83export const getExecutorSourcesAddHelpLines = (): readonly string[] => {126const coreSourceManagementHelpLines = (): readonly string[] => [
84 const sources = registeredSourceContributions();127 "- executor.sources.list",
128 `- executor.sources.get: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
129 `- executor.sources.refresh: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
130 `- executor.sources.remove: ${deriveSchemaTypeSignature(SourceIdInputSchema, 180)}`,
131];
132
133export const getExecutorInternalToolHelpLines = (
134 pluginRegistry: ExecutorSdkPluginRegistry,
135): readonly string[] => {
136 const sources = registeredSourceContributions(pluginRegistry);
137 const managementTools = registeredManagementToolContributions(pluginRegistry);
138
139 return [
140 "Core source management tools:",
141 ...coreSourceManagementHelpLines(),
142 ...(managementTools.length === 0
143 ? ["No plugin management tools are registered in this build."]
144 : [
145 "Plugin management tools:",
146 ...managementTools.map((tool) =>
147 `- ${tool.path}: ${deriveSchemaTypeSignature(tool.inputSchema, 260)}`
148 ),
149 ]),
150 ...(sources.length === 0
151 ? []
152 : [
153 "Legacy compatibility tool:",
154 `- executor.sources.add: ${deriveSchemaTypeSignature(
155 createExecutorSourcesAddSchema(sources),
156 340,
157 )}`,
158 ]),
159 ];
160};
161
162export const getExecutorSourcesAddHelpLines = (
163 pluginRegistry: ExecutorSdkPluginRegistry,
164): readonly string[] => {
165 const sources = registeredSourceContributions(pluginRegistry);
85 if (sources.length === 0) {166 if (sources.length === 0) {
86 return ["No source plugins are registered in this build."] as const;167 return ["No source plugins are registered in this build."] as const;
87 }168 }
@@ -98,15 +179,18 @@ export const getExecutorSourcesAddHelpLines = (): readonly string[] => { · 10 unmodified lines
98 ];179 ];
99};180};
100 181
101export const buildExecutorSourcesAddDescription = (): string => {182export const buildExecutorSourcesAddDescription = (
102 const sources = registeredSourceContributions();183 pluginRegistry: ExecutorSdkPluginRegistry,
184): string => {
185 const sources = registeredSourceContributions(pluginRegistry);
103 if (sources.length === 0) {186 if (sources.length === 0) {
104 return "No source plugins are registered in this build.";187 return "No source plugins are registered in this build.";
105 }188 }
106 189
107 return [190 return [
108 "Add a source using one of the registered source plugins.",191 "Legacy compatibility alias for creating a source.",
109 ...getExecutorSourcesAddHelpLines(),192 "Prefer plugin-specific tools such as executor.openapi.createSource or executor.mcp.createSource.",
193 ...getExecutorSourcesAddHelpLines(pluginRegistry),
110 ].join("\n");194 ].join("\n");
111};195};
112 196
@@ -153,7 +237,7 @@ const createSourceConnectorHost = (input: { · 40 unmodified lines
153 },237 },
154});238});
155 239
156const runExecutorSourceEffect = async <A>(240const runExecutorToolEffect = async <A>(
157 effect: Effect.Effect<A, unknown, any>,241 effect: Effect.Effect<A, unknown, any>,
158 input: {242 input: {
159 executorStateStore: ExecutorStateStoreShape;243 executorStateStore: ExecutorStateStoreShape;
@@ -166,6 +250,12 @@ const runExecutorSourceEffect = async <A>( · 6 unmodified lines
166 scopeStateStore: ScopeStateStoreShape;250 scopeStateStore: ScopeStateStoreShape;
167 sourceArtifactStore: SourceArtifactStoreShape;251 sourceArtifactStore: SourceArtifactStoreShape;
168 runtimeLocalScope: RuntimeLocalScopeState | null;252 runtimeLocalScope: RuntimeLocalScopeState | null;
253 secretMaterialServices: {
254 resolve: ResolveSecretMaterial;
255 store: StoreSecretMaterial;
256 delete: DeleteSecretMaterial;
257 update: UpdateSecretMaterial;
258 };
169 },259 },
170): Promise<A> => {260): Promise<A> => {
171 const servicesLayer = Layer.mergeAll(261 const servicesLayer = Layer.mergeAll(
@@ -181,6 +271,22 @@ const runExecutorSourceEffect = async <A>( · 9 unmodified lines
181 RuntimeSourceCatalogSyncService,271 RuntimeSourceCatalogSyncService,
182 input.sourceCatalogSyncService,272 input.sourceCatalogSyncService,
183 ),273 ),
274 Layer.succeed(
275 SecretMaterialResolverService,
276 input.secretMaterialServices.resolve,
277 ),
278 Layer.succeed(
279 SecretMaterialStorerService,
280 input.secretMaterialServices.store,
281 ),
282 Layer.succeed(
283 SecretMaterialDeleterService,
284 input.secretMaterialServices.delete,
285 ),
286 Layer.succeed(
287 SecretMaterialUpdaterService,
288 input.secretMaterialServices.update,
289 ),
184 );290 );
185 291
186 return Effect.runPromise(292 return Effect.runPromise(
@@ -192,11 +298,11 @@ const runExecutorSourceEffect = async <A>( · 5 unmodified lines
192};298};
193 299
194const resolveSourceContribution = (300const resolveSourceContribution = (
195 sources: ReturnType<typeof registeredSourceContributions>,301 sources: ReadonlyArray<RegisteredSourceContribution>,
196 args: unknown,302 args: unknown,
197):303):
198 | {304 | {
199 source: ReturnType<typeof registeredSourceContributions>[number];305 source: RegisteredSourceContribution;
200 parsedArgs: unknown;306 parsedArgs: unknown;
201 }307 }
202 | null => {308 | null => {
@@ -216,7 +322,52 @@ const resolveSourceContribution = ( · 13 unmodified lines
216const toSerializableValue = <A>(value: A): A =>322const toSerializableValue = <A>(value: A): A =>
217 JSON.parse(JSON.stringify(value)) as A;323 JSON.parse(JSON.stringify(value)) as A;
218 324
325const pluginRemoveToolPath = (
326 source: RegisteredSourceContribution,
327): `executor.${string}` =>
328 `executor.${source.pluginKey}.removeSource`;
329
330const pluginRefreshToolPath = (
331 source: RegisteredSourceContribution,
332): `executor.${string}` =>
333 `executor.${source.pluginKey}.refreshSource`;
334
335const findManagementTool = (
336 managementTools: ReadonlyArray<RegisteredManagementToolContribution>,
337 path: string,
338): RegisteredManagementToolContribution | null =>
339 managementTools.find((tool) => tool.path === path) ?? null;
340
341const createPluginManagementToolMap = (input: {
342 managementTools: ReadonlyArray<RegisteredManagementToolContribution>;
343 host: ReturnType<typeof createSourceConnectorHost>;
344 runtime: Parameters<typeof runExecutorToolEffect>[1];
345}): ToolMap =>
346 Object.fromEntries(
347 input.managementTools.map((tool) => [
348 tool.path,
349 toTool({
350 tool: {
351 description: tool.description,
352 inputSchema: Schema.standardSchemaV1(tool.inputSchema),
353 outputSchema: Schema.standardSchemaV1(tool.outputSchema),
354 execute: async (args: unknown) =>
355 toSerializableValue(
356 await runExecutorToolEffect(
357 tool.execute({
358 args: args as never,
359 host: input.host,
360 }),
361 input.runtime,
362 ),
363 ),
364 },
365 }),
366 ]),
367 );
368
219export const createExecutorToolMap = (input: {369export const createExecutorToolMap = (input: {
370 pluginRegistry: ExecutorSdkPluginRegistry;
220 scopeId: ScopeId;371 scopeId: ScopeId;
221 actorScopeId: ScopeId;372 actorScopeId: ScopeId;
222 executorStateStore: ExecutorStateStoreShape;373 executorStateStore: ExecutorStateStoreShape;
@@ -229,22 +380,182 @@ export const createExecutorToolMap = (input: { · 6 unmodified lines
229 scopeStateStore: ScopeStateStoreShape;380 scopeStateStore: ScopeStateStoreShape;
230 sourceArtifactStore: SourceArtifactStoreShape;381 sourceArtifactStore: SourceArtifactStoreShape;
231 runtimeLocalScope: RuntimeLocalScopeState | null;382 runtimeLocalScope: RuntimeLocalScopeState | null;
383 secretMaterialServices: {
384 resolve: ResolveSecretMaterial;
385 store: StoreSecretMaterial;
386 delete: DeleteSecretMaterial;
387 update: UpdateSecretMaterial;
388 };
232}): ToolMap => {389}): ToolMap => {
233 const sources = registeredSourceContributions();390 const sources = registeredSourceContributions(input.pluginRegistry);
234 if (sources.length === 0) {391 const managementTools = registeredManagementToolContributions(
235 return {};392 input.pluginRegistry,
236 }393 );
237
238 const host = createSourceConnectorHost({394 const host = createSourceConnectorHost({
239 scopeId: input.scopeId,395 scopeId: input.scopeId,
240 actorScopeId: input.actorScopeId,396 actorScopeId: input.actorScopeId,
241 });397 });
242 398
243 return {399 const runtime = {
244 "executor.sources.add": toTool({400 executorStateStore: input.executorStateStore,
401 sourceStore: input.sourceStore,
402 sourceCatalogSyncService: input.sourceCatalogSyncService,
403 installationStore: input.installationStore,
404 scopeConfigStore: input.scopeConfigStore,
405 scopeStateStore: input.scopeStateStore,
406 sourceArtifactStore: input.sourceArtifactStore,
407 runtimeLocalScope: input.runtimeLocalScope,
408 secretMaterialServices: input.secretMaterialServices,
409 };
410
411 const pluginToolMap = createPluginManagementToolMap({
412 managementTools,
413 host,
414 runtime,
415 });
416
417 const coreTools: ToolMap = {
418 "executor.sources.list": toTool({
419 tool: {
420 description: "List sources in the current executor scope.",
421 inputSchema: Schema.standardSchemaV1(Schema.Struct({})),
422 outputSchema: Schema.standardSchemaV1(SourceArraySchema),
423 execute: async () =>
424 toSerializableValue(
425 await runExecutorToolEffect(
426 listSources({
427 scopeId: input.scopeId,
428 actorScopeId: input.actorScopeId,
429 }),
430 runtime,
431 ),
432 ),
433 },
434 }),
435 "executor.sources.get": toTool({
436 tool: {
437 description: "Get a source by id.",
438 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
439 outputSchema: Schema.standardSchemaV1(SourceSchema),
440 execute: async (args: typeof SourceIdInputSchema.Type) =>
441 toSerializableValue(
442 await runExecutorToolEffect(
443 getSource({
444 scopeId: input.scopeId,
445 sourceId: args.sourceId,
446 actorScopeId: input.actorScopeId,
447 }),
448 runtime,
449 ),
450 ),
451 },
452 }),
453 "executor.sources.refresh": toTool({
245 tool: {454 tool: {
246 description: buildExecutorSourcesAddDescription(),455 description: "Refresh catalog artifacts for a source by id.",
247 inputSchema: Schema.standardSchemaV1(createExecutorSourcesAddSchema()),456 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
457 outputSchema: Schema.standardSchemaV1(SourceSchema),
458 execute: async (args: typeof SourceIdInputSchema.Type) => {
459 const source = await runExecutorToolEffect(
460 getSource({
461 scopeId: input.scopeId,
462 sourceId: args.sourceId,
463 actorScopeId: input.actorScopeId,
464 }),
465 runtime,
466 );
467 const contribution = sources.find(
468 (candidate) => candidate.kind === source.kind,
469 );
470 const pluginRefreshTool = contribution
471 ? findManagementTool(
472 managementTools,
473 pluginRefreshToolPath(contribution),
474 )
475 : undefined;
476 if (pluginRefreshTool) {
477 return toSerializableValue(
478 await runExecutorToolEffect(
479 pluginRefreshTool.execute({
480 args,
481 host,
482 }),
483 runtime,
484 ),
485 );
486 }
487
488 return toSerializableValue(
489 await runExecutorToolEffect(
490 refreshManagedSourceCatalog({
491 scopeId: input.scopeId,
492 sourceId: args.sourceId,
493 actorScopeId: input.actorScopeId,
494 }),
495 runtime,
496 ),
497 );
498 },
499 },
500 }),
501 "executor.sources.remove": toTool({
502 tool: {
503 description:
504 "Remove a source by id. Delegates to plugin-owned cleanup when available.",
505 inputSchema: Schema.standardSchemaV1(SourceIdInputSchema),
506 outputSchema: Schema.standardSchemaV1(SourceRemoveResultSchema),
507 execute: async (args: typeof SourceIdInputSchema.Type) => {
508 const source = await runExecutorToolEffect(
509 getSource({
510 scopeId: input.scopeId,
511 sourceId: args.sourceId,
512 actorScopeId: input.actorScopeId,
513 }),
514 runtime,
515 );
516 const contribution = sources.find(
517 (candidate) => candidate.kind === source.kind,
518 );
519 const pluginRemoveTool = contribution
520 ? findManagementTool(
521 managementTools,
522 pluginRemoveToolPath(contribution),
523 )
524 : undefined;
525 if (pluginRemoveTool) {
526 const removed = await runExecutorToolEffect(
527 pluginRemoveTool.execute({
528 args,
529 host,
530 }),
531 runtime,
532 );
533 return toSerializableValue({
534 removed: Boolean(removed),
535 });
536 }
537
538 return toSerializableValue(
539 await runExecutorToolEffect(
540 removeSource({
541 scopeId: input.scopeId,
542 sourceId: args.sourceId,
543 }),
544 runtime,
545 ),
546 );
547 },
548 },
549 }),
550 };
551
552 if (sources.length > 0) {
553 coreTools["executor.sources.add"] = toTool({
554 tool: {
555 description: buildExecutorSourcesAddDescription(input.pluginRegistry),
556 inputSchema: Schema.standardSchemaV1(
557 createExecutorSourcesAddSchema(sources),
558 ),
248 outputSchema: Schema.standardSchemaV1(SourceSchema),559 outputSchema: Schema.standardSchemaV1(SourceSchema),
249 execute: async (args: unknown): Promise<Source> => {560 execute: async (args: unknown): Promise<Source> => {
250 const matched = resolveSourceContribution(sources, args);561 const matched = resolveSourceContribution(sources, args);
@@ -254,17 +565,22 @@ export const createExecutorToolMap = (input: { · 3 unmodified lines
254 );565 );
255 }566 }
256 567
257 const createdSource = await runExecutorSourceEffect(568 const createdSource = await runExecutorToolEffect(
258 matched.source.createSource({569 matched.source.createSource({
259 args: matched.parsedArgs,570 args: matched.parsedArgs,
260 host,571 host,
261 }),572 }),
262 input,573 runtime,
263 );574 );
264 575
265 return toSerializableValue(createdSource);576 return toSerializableValue(createdSource);
266 },577 },
267 },578 },
268 }),579 });
580 }
581
582 return {
583 ...coreTools,
584 ...pluginToolMap,
269 };585 };
270};586};
packages/platform/sdk/src/runtime/sources/source-definitions.ts+52−16
@@ -16,7 +16,9 @@ import { · 15 unmodified lines
16} from "#schema";16} from "#schema";
17import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
18 18
19import { getSourceContributionForSource } from "./source-plugins";19import type {
20 ExecutorSdkPluginRegistry,
21} from "../../plugins";
20 22
21const trimOrNull = (value: string | null | undefined): string | null => {23const trimOrNull = (value: string | null | undefined): string | null => {
22 if (value === null || value === undefined) {24 if (value === null || value === undefined) {
@@ -29,29 +31,56 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 6 unmodified lines
29 31
30type SourceCatalogSourceConfig = Record<string, unknown>;32type SourceCatalogSourceConfig = Record<string, unknown>;
31 33
32const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig =>34const getSourceContributionOption = (
33 getSourceContributionForSource(source).catalogIdentity?.({35 source: Source,
36 pluginRegistry?: ExecutorSdkPluginRegistry,
37) => {
38 if (!pluginRegistry) {
39 return null;
40 }
41
42 try {
43 return pluginRegistry.getSourceContributionForSource(source);
44 } catch {
45 return null;
46 }
47};
48
49const sourceConfigFromSource = (
50 source: Source,
51 pluginRegistry?: ExecutorSdkPluginRegistry,
52): SourceCatalogSourceConfig =>
53 getSourceContributionOption(source, pluginRegistry)?.catalogIdentity?.({
34 source,54 source,
35 }) ?? {55 }) ?? {
36 kind: source.kind,56 kind: source.kind,
37 namespace: source.namespace,57 namespace: source.namespace,
38 };58 };
39 59
40const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind =>60const sourceCatalogKindFromSource = (
41 getSourceContributionForSource(source).catalogKind;61 source: Source,
62 pluginRegistry?: ExecutorSdkPluginRegistry,
63): SourceCatalogKind =>
64 getSourceContributionOption(source, pluginRegistry)?.catalogKind ?? "imported";
42 65
43const sourceCatalogPluginKeyFromSource = (source: Source): SourceCatalogPluginKey =>66const sourceCatalogPluginKeyFromSource = (
44 getSourceContributionForSource(source).kind;67 source: Source,
68 pluginRegistry?: ExecutorSdkPluginRegistry,
69): SourceCatalogPluginKey =>
70 getSourceContributionOption(source, pluginRegistry)?.kind ?? source.kind;
45 71
46const stableHash = (value: string): string =>72const stableHash = (value: string): string =>
47 sha256Hex(value).slice(0, 24);73 sha256Hex(value).slice(0, 24);
48 74
49const sourceCatalogSignature = (source: Source): string =>75const sourceCatalogSignature = (
76 source: Source,
77 pluginRegistry?: ExecutorSdkPluginRegistry,
78): string =>
50 JSON.stringify({79 JSON.stringify({
51 catalogKind: sourceCatalogKindFromSource(source),80 catalogKind: sourceCatalogKindFromSource(source, pluginRegistry),
52 pluginKey: sourceCatalogPluginKeyFromSource(source),81 pluginKey: sourceCatalogPluginKeyFromSource(source, pluginRegistry),
53 sourceId: source.id,82 sourceId: source.id,
54 sourceConfig: sourceConfigFromSource(source),83 sourceConfig: sourceConfigFromSource(source, pluginRegistry),
55 });84 });
56 85
57export const sourceConfigSignature = (source: Source): string =>86export const sourceConfigSignature = (source: Source): string =>
@@ -64,8 +93,13 @@ export const sourceConfigSignature = (source: Source): string => · 6 unmodified lines
64 updatedAt: source.updatedAt,93 updatedAt: source.updatedAt,
65 });94 });
66 95
67export const stableSourceCatalogId = (source: Source): SourceCatalogId =>96export const stableSourceCatalogId = (
68 SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`);97 source: Source,
98 pluginRegistry?: ExecutorSdkPluginRegistry,
99): SourceCatalogId =>
100 SourceCatalogIdSchema.make(
101 `src_catalog_${stableHash(sourceCatalogSignature(source, pluginRegistry))}`,
102 );
69 103
70export const stableSourceCatalogRevisionId = (104export const stableSourceCatalogRevisionId = (
71 source: Source,105 source: Source,
@@ -109,12 +143,13 @@ export const normalizeSourceForSave = (input: { · 37 unmodified lines
109 143
110export const createSourceCatalogRecord = (input: {144export const createSourceCatalogRecord = (input: {
111 source: Source;145 source: Source;
146 pluginRegistry?: ExecutorSdkPluginRegistry;
112 catalogId?: SourceCatalogId | null;147 catalogId?: SourceCatalogId | null;
113 latestRevisionId: SourceCatalogRevisionId;148 latestRevisionId: SourceCatalogRevisionId;
114}): StoredSourceCatalogRecord => ({149}): StoredSourceCatalogRecord => ({
115 id: input.catalogId ?? stableSourceCatalogId(input.source),150 id: input.catalogId ?? stableSourceCatalogId(input.source, input.pluginRegistry),
116 kind: sourceCatalogKindFromSource(input.source),151 kind: sourceCatalogKindFromSource(input.source, input.pluginRegistry),
117 pluginKey: sourceCatalogPluginKeyFromSource(input.source),152 pluginKey: sourceCatalogPluginKeyFromSource(input.source, input.pluginRegistry),
118 name: input.source.name,153 name: input.source.name,
119 summary: null,154 summary: null,
120 visibility: "scope",155 visibility: "scope",
@@ -125,6 +160,7 @@ export const createSourceCatalogRecord = (input: { · 4 unmodified lines
125 160
126export const createSourceCatalogRevisionRecord = (input: {161export const createSourceCatalogRevisionRecord = (input: {
127 source: Source;162 source: Source;
163 pluginRegistry?: ExecutorSdkPluginRegistry;
128 catalogId: SourceCatalogId;164 catalogId: SourceCatalogId;
129 catalogRevisionId?: SourceCatalogRevisionId | null;165 catalogRevisionId?: SourceCatalogRevisionId | null;
130 revisionNumber: number;166 revisionNumber: number;
packages/platform/sdk/src/runtime/sources/source-plugins/index.ts+33−17
@@ -1,29 +1,45 @@
1import * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
3
1import {4import {
2 registerExecutorSdkPlugins,5 registerExecutorSdkPlugins,
3 type ExecutorSdkPlugin,6 type ExecutorSdkPlugin,
7 type ExecutorSdkPluginRegistry,
4} from "../../../plugins";8} from "../../../plugins";
5 9
6let configuredSourcePlugins: readonly ExecutorSdkPlugin<any, any>[] = [];10export class ExecutorPluginRegistryService extends Context.Tag(
7let registry = registerExecutorSdkPlugins(configuredSourcePlugins);11 "#runtime/ExecutorPluginRegistryService",
12)<ExecutorPluginRegistryService, ExecutorSdkPluginRegistry>() {}
13
14export const createExecutorPluginRegistry = (
15 plugins: readonly ExecutorSdkPlugin<any, any>[] = [],
16): ExecutorSdkPluginRegistry => registerExecutorSdkPlugins(plugins);
17
18export const emptyExecutorPluginRegistry = (): ExecutorSdkPluginRegistry =>
19 createExecutorPluginRegistry();
20
21export const ExecutorPluginRegistryLive = (
22 registry: ExecutorSdkPluginRegistry,
23) => Layer.succeed(ExecutorPluginRegistryService, registry);
8 24
9const refreshRegistry = () => {25export const registeredSourceContributions = (
10 registry = registerExecutorSdkPlugins(configuredSourcePlugins);26 registry: ExecutorSdkPluginRegistry,
11};27) => registry.sources;
12 28
13export const configureExecutorSourcePlugins = (29export const registeredManagementToolContributions = (
14 plugins: readonly ExecutorSdkPlugin<any, any>[],30 registry: ExecutorSdkPluginRegistry,
15): void => {31) => registry.managementTools;
16 configuredSourcePlugins = plugins;
17 refreshRegistry();
18};
19 32
20export const registeredSourceContributions = () => registry.sources;33export const getSourceContribution = (
34 registry: ExecutorSdkPluginRegistry,
35 kind: string,
36) => registry.getSourceContribution(kind);
21 37
22export const getSourceContribution = (kind: string) =>
23 registry.getSourceContribution(kind);
24export const getSourceContributionForSource = (38export const getSourceContributionForSource = (
25 source: Parameters<typeof registry.getSourceContributionForSource>[0],39 registry: ExecutorSdkPluginRegistry,
40 source: Parameters<ExecutorSdkPluginRegistry["getSourceContributionForSource"]>[0],
26) => registry.getSourceContributionForSource(source);41) => registry.getSourceContributionForSource(source);
27 42
28export const hasRegisteredExternalSourcePlugins = () =>43export const hasRegisteredExternalSourcePlugins = (
29 configuredSourcePlugins.length > 0;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
38 return SourceIdSchema.make(candidate);38 return SourceIdSchema.make(candidate);
39};39};
40 40
41export const configSourceBaseFromLocalSource = (input: {
42 source: Source;
43}): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({
44 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
45 ? { name: input.source.name }
46 : {}),
47 ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)
48 ? { namespace: input.source.namespace ?? undefined }
49 : {}),
50 ...(input.source.enabled === false ? { enabled: false } : {}),
51});
52
41export const configSourceFromLocalSource = (input: {53export const configSourceFromLocalSource = (input: {
42 source: Source;54 source: Source;
55 existingConfig?: LocalConfigSource | null;
43}): LocalConfigSource => {56}): LocalConfigSource => {
44 return {57 return {
58 ...configSourceBaseFromLocalSource({
59 source: input.source,
60 }),
45 kind: input.source.kind as LocalConfigSource["kind"],61 kind: input.source.kind as LocalConfigSource["kind"],
46 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)62 ...(input.existingConfig?.config !== undefined
47 ? { name: input.source.name }63 ? {
64 config: cloneJson(input.existingConfig.config),
65 }
66 : {}),
67 ...(input.existingConfig?.connection !== undefined
68 ? {
69 connection: cloneJson(input.existingConfig.connection),
70 }
48 : {}),71 : {}),
49 ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)72 ...(input.existingConfig?.binding !== undefined
50 ? { namespace: input.source.namespace ?? undefined }73 ? {
74 binding: cloneJson(input.existingConfig.binding),
75 }
51 : {}),76 : {}),
52 ...(input.source.enabled === false ? { enabled: false } : {}),
53 } as LocalConfigSource;77 } as LocalConfigSource;
54};78};
packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts+1−0
@@ -95,6 +95,7 @@ export const persistSourceWithDeps = ( · 94 unmodified lines
95 };95 };
96 sources[nextSource.id] = configSourceFromLocalSource({96 sources[nextSource.id] = configSourceFromLocalSource({
97 source: nextSource,97 source: nextSource,
98 existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,
98 });99 });
99 yield* localScope.scopeConfigStore.writeProject({100 yield* localScope.scopeConfigStore.writeProject({
100 config: {101 config: {
packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts+123−0
@@ -0,0 +1,123 @@
1import type {
2 LocalConfigSecretInput,
3 LocalConfigSource,
4 LocalExecutorConfig,
5 SecretRef,
6 Source,
7} from "#schema";
8import {
9 LocalConfigSecretInputSchema,
10} from "#schema";
11import * as Option from "effect/Option";
12import * as Schema from "effect/Schema";
13
14import {
15 toConfigSecretProviderId,
16} from "../../scope/config-secrets";
17import {
18 cloneJson,
19 configSourceBaseFromLocalSource,
20 trimOrNull,
21} from "./config";
22
23export const createPluginLocalConfigEntrySchema = <
24 TKind extends string,
25 TConfig,
26>(
27 input: {
28 kind: TKind;
29 config: Schema.Schema<TConfig, any, never>;
30 },
31) =>
32 Schema.Struct({
33 kind: Schema.Literal(input.kind),
34 name: Schema.optional(Schema.String),
35 namespace: Schema.optional(Schema.String),
36 enabled: Schema.optional(Schema.Boolean),
37 config: input.config,
38 connection: Schema.optional(Schema.Unknown),
39 binding: Schema.optional(Schema.Unknown),
40 });
41
42export const pluginLocalConfigSourceFromConfig = <TConfig>(input: {
43 source: Source;
44 config: TConfig;
45}): LocalConfigSource => ({
46 ...configSourceBaseFromLocalSource({
47 source: input.source,
48 }),
49 kind: input.source.kind as LocalConfigSource["kind"],
50 config: cloneJson(input.config),
51});
52
53const decodeLocalConfigSecretInputOption = Schema.decodeUnknownOption(
54 LocalConfigSecretInputSchema,
55);
56
57const resolveDefaultConfigSecretProviderAlias = (
58 config: LocalExecutorConfig | null | undefined,
59): string | null => {
60 const defaultAlias = trimOrNull(config?.secrets?.defaults?.env);
61 if (defaultAlias !== null && config?.secrets?.providers?.[defaultAlias]) {
62 return defaultAlias;
63 }
64
65 return config?.secrets?.providers?.default ? "default" : null;
66};
67
68export const secretRefFromLocalConfigSecretInput = (input: {
69 auth: LocalConfigSecretInput;
70 loadedConfig: LocalExecutorConfig | null;
71}): SecretRef => {
72 if (typeof input.auth === "string") {
73 const providerAlias = resolveDefaultConfigSecretProviderAlias(
74 input.loadedConfig,
75 );
76
77 return {
78 providerId: providerAlias ? toConfigSecretProviderId(providerAlias) : "env",
79 handle: input.auth,
80 };
81 }
82
83 return {
84 providerId:
85 input.auth.provider === "params"
86 ? "params"
87 : input.auth.provider
88 ? toConfigSecretProviderId(input.auth.provider)
89 : input.auth.source === "params"
90 ? "params"
91 : "env",
92 handle: input.auth.id,
93 };
94};
95
96export const decodeCurrentOrLegacyLocalConfigAuth = <TAuth>(input: {
97 auth: unknown;
98 authSchema: Schema.Schema<TAuth, any, never>;
99 loadedConfig: LocalExecutorConfig | null;
100 onLegacySecretRef: (ref: SecretRef) => TAuth;
101 fallback: TAuth;
102}): TAuth => {
103 if (input.auth === undefined) {
104 return input.fallback;
105 }
106
107 const current = Schema.decodeUnknownOption(input.authSchema)(input.auth);
108 if (Option.isSome(current)) {
109 return current.value;
110 }
111
112 const legacy = decodeLocalConfigSecretInputOption(input.auth);
113 if (Option.isSome(legacy)) {
114 return input.onLegacySecretRef(
115 secretRefFromLocalConfigSecretInput({
116 auth: legacy.value,
117 loadedConfig: input.loadedConfig,
118 }),
119 );
120 }
121
122 throw new Error("Unsupported local source auth configuration.");
123};
packages/platform/sdk/src/schema/models/local-config.ts+8−0
@@ -56,10 +56,18 @@ export const LocalConfigSecretInputSchema = Schema.Union( · 55 unmodified lines
56 LocalConfigExplicitSecretRefSchema,56 LocalConfigExplicitSecretRefSchema,
57);57);
58 58
59export const LocalConfigSourceConnectionSchema = Schema.Struct({
60 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
61 auth: Schema.optional(Schema.Unknown),
62});
63
59const LocalConfigSourceEntryBaseSchema = Schema.Struct({64const LocalConfigSourceEntryBaseSchema = Schema.Struct({
60 name: Schema.optional(Schema.String),65 name: Schema.optional(Schema.String),
61 namespace: Schema.optional(Schema.String),66 namespace: Schema.optional(Schema.String),
62 enabled: Schema.optional(Schema.Boolean),67 enabled: Schema.optional(Schema.Boolean),
68 config: Schema.optional(Schema.Unknown),
69 connection: Schema.optional(LocalConfigSourceConnectionSchema),
70 binding: Schema.optional(Schema.Unknown),
63});71});
64 72
65export const LocalConfigSourceSchema = Schema.extend(73export const LocalConfigSourceSchema = Schema.extend(
packages/platform/server/src/index.test.ts+225−1
@@ -35,7 +35,12 @@ import { · 34 unmodified lines
35 writeLocalSourceArtifact,35 writeLocalSourceArtifact,
36 writeProjectLocalExecutorConfig,36 writeProjectLocalExecutorConfig,
37} from "@executor/platform-sdk-file/effect";37} from "@executor/platform-sdk-file/effect";
38import { SourceIdSchema } from "@executor/platform-sdk/schema";38import {
39 type LocalConfigSource,
40 SourceCatalogIdSchema,
41 SourceCatalogRevisionIdSchema,
42 SourceIdSchema,
43} from "@executor/platform-sdk/schema";
39import { googleDiscoveryHttpPlugin } from "@executor/plugin-google-discovery-http";44import { googleDiscoveryHttpPlugin } from "@executor/plugin-google-discovery-http";
40import { graphqlHttpPlugin } from "@executor/plugin-graphql-http";45import { graphqlHttpPlugin } from "@executor/plugin-graphql-http";
41import { mcpHttpPlugin } from "@executor/plugin-mcp-http";46import { mcpHttpPlugin } from "@executor/plugin-mcp-http";
@@ -55,6 +60,7 @@ import { · 13 unmodified lines
55 contentHash,60 contentHash,
56 createCatalogImportMetadata,61 createCatalogImportMetadata,
57 createSourceCatalogSyncResult,62 createSourceCatalogSyncResult,
63 snapshotFromSourceCatalogSyncResult,
58} from "../../../sources/core/src/index";64} from "../../../sources/core/src/index";
59import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";65import { createMcpCatalogFragment } from "../../../../plugins/mcp/sdk/catalog";
60 66
@@ -256,6 +262,103 @@ const writeConfiguredLocalMcpSource = (input: { · 195 unmodified lines
256 });262 });
257 }).pipe(Effect.provide(NodeFileSystem.layer));263 }).pipe(Effect.provide(NodeFileSystem.layer));
258 264
265const 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
288const 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
259const makeServer = createIsolatedLocalExecutorServer({362const makeServer = createIsolatedLocalExecutorServer({
260 port: 0,363 port: 0,
261 localDataDir: ":memory:",364 localDataDir: ":memory:",
@@ -1702,6 +1805,127 @@ describe("local-executor-server", () => { · 1440 unmodified lines
1702 15_000,1805 15_000,
1703 );1806 );
1704 1807
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
1705 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>1929 it.scoped("marks execution failed when a configured MCP endpoint is invalid", () =>
1706 Effect.gen(function* () {1930 Effect.gen(function* () {
1707 const localDataDir = yield* makeTempWorkspaceRoot();1931 const localDataDir = yield* makeTempWorkspaceRoot();
plugins/google-discovery/react/index.ts+4−1
@@ -9,7 +9,10 @@ import { · 8 unmodified lines
9 GoogleDiscoveryDetailRoute,9 GoogleDiscoveryDetailRoute,
10 GoogleDiscoveryEditRoute,10 GoogleDiscoveryEditRoute,
11} from "./components";11} from "./components";
12export { getGoogleDiscoveryIconUrl } from "./icons";12export {
13 getGoogleDiscoveryIconUrl,
14 getGoogleDiscoveryIconUrlForService,
15} from "./icons";
13 16
14export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({17export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({
15 key: GOOGLE_DISCOVERY_PLUGIN_KEY,18 key: GOOGLE_DISCOVERY_PLUGIN_KEY,
plugins/google-discovery/sdk/index.ts+427−15
@@ -1,15 +1,25 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
3 4
4import {5import {
5 createCatalogImportMetadata,6 createCatalogImportMetadata,
6 createSourceCatalogSyncResult,7 createSourceCatalogSyncResult,
7} from "@executor/source-core";8} from "@executor/source-core";
8import type { Source } from "@executor/platform-sdk/schema";9import type {
10 LocalExecutorConfig,
11 Source,
12} from "@executor/platform-sdk/schema";
13import {
14 SourceSchema,
15} from "@executor/platform-sdk/schema";
9import {16import {
10 defineExecutorSourcePlugin,17 defineExecutorSourcePlugin,
11} from "@executor/platform-sdk/plugins";18} from "@executor/platform-sdk/plugins";
12import {19import {
20 createPluginLocalConfigEntrySchema,
21 decodeCurrentOrLegacyLocalConfigAuth,
22 pluginLocalConfigSourceFromConfig,
13 SecretMaterialDeleterService,23 SecretMaterialDeleterService,
14 SecretMaterialResolverService,24 SecretMaterialResolverService,
15 SecretMaterialStorerService,25 SecretMaterialStorerService,
@@ -21,11 +31,18 @@ import { · 5 unmodified lines
21 GOOGLE_DISCOVERY_EXECUTOR_KEY,31 GOOGLE_DISCOVERY_EXECUTOR_KEY,
22 GOOGLE_DISCOVERY_SOURCE_KIND,32 GOOGLE_DISCOVERY_SOURCE_KIND,
23 GoogleDiscoveryBatchSourceInputSchema,33 GoogleDiscoveryBatchSourceInputSchema,
34 GoogleDiscoveryConnectInputSchema,
24 GoogleDiscoveryConnectionAuthSchema,35 GoogleDiscoveryConnectionAuthSchema,
36 GoogleDiscoveryOAuthPopupResultSchema,
25 GoogleDiscoveryOAuthSessionSchema,37 GoogleDiscoveryOAuthSessionSchema,
38 GoogleDiscoverySourceConfigPayloadSchema,
39 GoogleDiscoveryStartBatchOAuthInputSchema,
40 GoogleDiscoveryStartOAuthInputSchema,
41 GoogleDiscoveryStartOAuthResultSchema,
26 defaultGoogleDiscoveryUrl,42 defaultGoogleDiscoveryUrl,
27 deriveGoogleDiscoveryNamespace,43 deriveGoogleDiscoveryNamespace,
28 GoogleDiscoveryStoredSourceDataSchema,44 GoogleDiscoveryStoredSourceDataSchema,
45 GoogleDiscoveryUpdateSourceInputSchema,
29 type GoogleDiscoveryBatchSourceInput,46 type GoogleDiscoveryBatchSourceInput,
30 type GoogleDiscoveryConnectInput,47 type GoogleDiscoveryConnectInput,
31 type GoogleDiscoveryConnectionAuth,48 type GoogleDiscoveryConnectionAuth,
@@ -62,6 +79,16 @@ import { · 30 unmodified lines
62 refreshOAuth2AccessToken,79 refreshOAuth2AccessToken,
63} from "./oauth2";80} from "./oauth2";
64 81
82const GoogleDiscoverySourceIdInputSchema = Schema.Struct({
83 sourceId: Schema.String,
84});
85const GoogleDiscoveryCompleteOAuthInputSchema = Schema.Struct({
86 state: Schema.String,
87 code: Schema.optional(Schema.String),
88 error: Schema.optional(Schema.String),
89 errorDescription: Schema.optional(Schema.String),
90});
91
65const decodeStoredSourceData = Schema.decodeUnknownSync(92const decodeStoredSourceData = Schema.decodeUnknownSync(
66 GoogleDiscoveryStoredSourceDataSchema,93 GoogleDiscoveryStoredSourceDataSchema,
67);94);
@@ -70,6 +97,34 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 2 unmodified lines
70);97);
71const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);98const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);
72const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);99const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);
100const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(
101 createPluginLocalConfigEntrySchema({
102 kind: GOOGLE_DISCOVERY_SOURCE_KIND,
103 config: GoogleDiscoveryStoredSourceDataSchema,
104 }),
105);
106const decodeGoogleDiscoveryLegacyLocalConfigOption = Schema.decodeUnknownOption(
107 Schema.Struct({
108 kind: Schema.Literal(GOOGLE_DISCOVERY_SOURCE_KIND),
109 name: Schema.optional(Schema.String),
110 namespace: Schema.optional(Schema.String),
111 enabled: Schema.optional(Schema.Boolean),
112 connection: Schema.Struct({
113 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
114 auth: Schema.optional(Schema.Unknown),
115 }),
116 binding: Schema.Struct({
117 service: Schema.String,
118 version: Schema.String,
119 discoveryUrl: Schema.optional(Schema.NullOr(Schema.String)),
120 defaultHeaders: Schema.optional(
121 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
122 ),
123 scopes: Schema.optional(Schema.Array(Schema.String)),
124 }),
125 config: Schema.optional(Schema.Unknown),
126 }),
127);
73 128
74export type GoogleDiscoverySourceStorage = {129export type GoogleDiscoverySourceStorage = {
75 get: (input: {130 get: (input: {
@@ -518,6 +573,44 @@ const googleDiscoveryConnectInputFromAddInput = ( · 442 unmodified lines
518 auth: input.auth,573 auth: input.auth,
519});574});
520 575
576const googleDiscoveryStoredSourceDataFromLocalConfig = (input: {
577 config: unknown;
578 loadedConfig: LocalExecutorConfig | null;
579}): GoogleDiscoveryStoredSourceData => {
580 const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config);
581 if (Option.isSome(current)) {
582 return current.value.config;
583 }
584
585 const legacy = decodeGoogleDiscoveryLegacyLocalConfigOption(input.config);
586 if (Option.isSome(legacy)) {
587 return storedSourceDataFromInput({
588 name:
589 legacy.value.name
590 ?? `${legacy.value.binding.service} ${legacy.value.binding.version}`,
591 service: legacy.value.binding.service,
592 version: legacy.value.binding.version,
593 discoveryUrl: legacy.value.binding.discoveryUrl ?? null,
594 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
595 scopes: legacy.value.binding.scopes ?? [],
596 auth: decodeCurrentOrLegacyLocalConfigAuth({
597 auth: legacy.value.connection.auth,
598 authSchema: GoogleDiscoveryConnectionAuthSchema,
599 loadedConfig: input.loadedConfig,
600 onLegacySecretRef: (tokenSecretRef) => ({
601 kind: "bearer",
602 tokenSecretRef,
603 }),
604 fallback: {
605 kind: "none",
606 },
607 }),
608 });
609 }
610
611 throw new Error("Unsupported Google Discovery local source config.");
612};
613
521export const googleDiscoverySdkPlugin = (options: {614export const googleDiscoverySdkPlugin = (options: {
522 storage: GoogleDiscoverySourceStorage;615 storage: GoogleDiscoverySourceStorage;
523 oauthSessions: GoogleDiscoveryOAuthSessionStorage;616 oauthSessions: GoogleDiscoveryOAuthSessionStorage;
@@ -575,6 +668,18 @@ export const googleDiscoverySdkPlugin = (options: { · 51 unmodified lines
575 }668 }
576 }),669 }),
577 },670 },
671 localConfig: {
672 toConfigSource: ({ source, stored }) =>
673 pluginLocalConfigSourceFromConfig({
674 source,
675 config: stored,
676 }),
677 recoverStored: ({ config, loadedConfig }) =>
678 googleDiscoveryStoredSourceDataFromLocalConfig({
679 config,
680 loadedConfig,
681 }),
682 },
578 catalog: {683 catalog: {
579 kind: "imported",684 kind: "imported",
580 identity: ({ source }) => ({685 identity: ({ source }) => ({
@@ -584,20 +689,10 @@ export const googleDiscoverySdkPlugin = (options: { · 3 unmodified lines
584 sync: ({ source, stored }) =>689 sync: ({ source, stored }) =>
585 Effect.gen(function* () {690 Effect.gen(function* () {
586 if (stored === null) {691 if (stored === null) {
587 return createSourceCatalogSyncResult({692 return yield* runtimeEffectError(
588 fragment: {693 "plugins/google-discovery/sdk",
589 version: "ir.v1.fragment",694 `Google Discovery source storage missing for ${source.id}`,
590 },695 );
591 importMetadata: {
592 ...createCatalogImportMetadata({
593 source,
594 pluginKey: GOOGLE_DISCOVERY_SOURCE_KIND,
595 }),
596 importerVersion: "ir.v1.google_discovery",
597 sourceConfigHash: "missing",
598 },
599 sourceHash: null,
600 });
601 }696 }
602 697
603 const headers = yield* resolveGoogleAuthHeaders({698 const headers = yield* resolveGoogleAuthHeaders({
@@ -725,6 +820,323 @@ export const googleDiscoverySdkPlugin = (options: { · 121 unmodified lines
725 }),820 }),
726 },821 },
727 },822 },
823 tools: [
824 {
825 name: "getSourceConfig",
826 description: "Load the saved configuration for a Google Discovery source.",
827 inputSchema: GoogleDiscoverySourceIdInputSchema,
828 outputSchema: GoogleDiscoverySourceConfigPayloadSchema,
829 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
830 },
831 {
832 name: "createSource",
833 description: "Create a Google Discovery source.",
834 inputSchema: GoogleDiscoveryConnectInputSchema,
835 outputSchema: SourceSchema,
836 execute: ({ args, source }) => source.createSource(args),
837 },
838 {
839 name: "updateSource",
840 description: "Update a Google Discovery source configuration.",
841 inputSchema: GoogleDiscoveryUpdateSourceInputSchema,
842 outputSchema: SourceSchema,
843 execute: ({ args, source }) => source.updateSource(args),
844 },
845 {
846 name: "removeSource",
847 description:
848 "Remove a Google Discovery source and its stored plugin data.",
849 inputSchema: GoogleDiscoverySourceIdInputSchema,
850 outputSchema: Schema.Boolean,
851 execute: ({ args, source }) => source.removeSource(args.sourceId),
852 },
853 {
854 name: "startOAuth",
855 description: "Start a Google OAuth flow for a single API source.",
856 inputSchema: GoogleDiscoveryStartOAuthInputSchema,
857 outputSchema: GoogleDiscoveryStartOAuthResultSchema,
858 execute: ({ args }) =>
859 Effect.gen(function* () {
860 const discoveryUrl =
861 args.discoveryUrl?.trim()
862 || defaultGoogleDiscoveryUrl(args.service, args.version);
863 let scopes: ReadonlyArray<string>;
864 if (args.scopes.length > 0) {
865 const configuredScopes: Array<string> = args.scopes
866 .map((scope: string) => scope.trim())
867 .filter((scope: string) => scope.length > 0);
868 scopes = [...new Set(configuredScopes)];
869 } else {
870 scopes = yield* fetchGoogleDiscoveryDocumentWithHeaders({
871 url: discoveryUrl,
872 headers: args.defaultHeaders ?? {},
873 }).pipe(
874 Effect.flatMap((document) =>
875 inferScopesFromDiscoveryDocument({
876 service: args.service,
877 document,
878 })
879 ),
880 );
881 }
882 const sessionId = `gdisc_oauth_${crypto.randomUUID()}`;
883 const codeVerifier = createPkceCodeVerifier();
884
885 yield* options.oauthSessions.put({
886 sessionId,
887 value: decodeSession({
888 kind: "single",
889 service: args.service.trim(),
890 version: args.version.trim(),
891 discoveryUrl,
892 defaultHeaders: args.defaultHeaders,
893 scopes: [...scopes],
894 clientId: args.clientId.trim(),
895 clientSecretRef: args.clientSecretRef,
896 clientAuthentication: args.clientAuthentication,
897 redirectUrl: args.redirectUrl,
898 codeVerifier,
899 }),
900 });
901
902 return {
903 sessionId,
904 authorizationUrl: buildOAuth2AuthorizationUrl({
905 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
906 clientId: args.clientId.trim(),
907 redirectUri: args.redirectUrl,
908 scopes,
909 state: sessionId,
910 codeVerifier,
911 extraParams: {
912 access_type: "offline",
913 prompt: "consent",
914 include_granted_scopes: "true",
915 },
916 }),
917 scopes: [...scopes],
918 };
919 }),
920 },
921 {
922 name: "startBatchOAuth",
923 description: "Start one Google OAuth flow for multiple API sources.",
924 inputSchema: GoogleDiscoveryStartBatchOAuthInputSchema,
925 outputSchema: GoogleDiscoveryStartOAuthResultSchema,
926 execute: ({ args }) =>
927 Effect.gen(function* () {
928 const normalizedSources: Array<GoogleDiscoveryBatchSourceInput> =
929 args.sources.map((source: GoogleDiscoveryBatchSourceInput) =>
930 normalizeBatchSourceInput(source)
931 );
932 if (normalizedSources.length === 0) {
933 return yield* runtimeEffectError(
934 "plugins/google-discovery/sdk",
935 "Select at least one Google API to connect.",
936 );
937 }
938
939 const inferredScopes: ReadonlyArray<ReadonlyArray<string>> = yield* Effect.forEach(
940 normalizedSources,
941 (source: GoogleDiscoveryBatchSourceInput) =>
942 resolveScopesForBatchSource({
943 source,
944 }),
945 );
946 const scopes: ReadonlyArray<string> = [
947 ...new Set(inferredScopes.flat()),
948 ];
949 const sessionId = `gdisc_oauth_${crypto.randomUUID()}`;
950 const codeVerifier = createPkceCodeVerifier();
951
952 yield* options.oauthSessions.put({
953 sessionId,
954 value: decodeSession({
955 kind: "batch",
956 sources: normalizedSources.map((
957 source: GoogleDiscoveryBatchSourceInput,
958 index: number,
959 ) => ({
960 ...source,
961 scopes: inferredScopes[index] ?? [],
962 })),
963 scopes,
964 clientId: args.clientId.trim(),
965 clientSecretRef: args.clientSecretRef,
966 clientAuthentication: args.clientAuthentication,
967 redirectUrl: args.redirectUrl,
968 codeVerifier,
969 }),
970 });
971
972 return {
973 sessionId,
974 authorizationUrl: buildOAuth2AuthorizationUrl({
975 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
976 clientId: args.clientId.trim(),
977 redirectUri: args.redirectUrl,
978 scopes,
979 state: sessionId,
980 codeVerifier,
981 extraParams: {
982 access_type: "offline",
983 prompt: "consent",
984 include_granted_scopes: "true",
985 },
986 }),
987 scopes,
988 };
989 }),
990 },
991 {
992 name: "completeOAuth",
993 description:
994 "Complete a Google OAuth callback and return the connected auth payload.",
995 inputSchema: GoogleDiscoveryCompleteOAuthInputSchema,
996 outputSchema: GoogleDiscoveryOAuthPopupResultSchema,
997 execute: ({ args, source }) =>
998 Effect.gen(function* () {
999 if (args.error) {
1000 return yield* runtimeEffectError(
1001 "plugins/google-discovery/sdk",
1002 args.errorDescription || args.error || "Google OAuth failed",
1003 );
1004 }
1005 if (!args.code) {
1006 return yield* runtimeEffectError(
1007 "plugins/google-discovery/sdk",
1008 "Missing Google OAuth code.",
1009 );
1010 }
1011
1012 const session = yield* options.oauthSessions.get(args.state);
1013 if (session === null) {
1014 return yield* runtimeEffectError(
1015 "plugins/google-discovery/sdk",
1016 `Google OAuth session not found: ${args.state}`,
1017 );
1018 }
1019
1020 const resolveSecretMaterial = yield* SecretMaterialResolverService;
1021 const clientSecret = session.clientSecretRef
1022 ? yield* resolveSecretMaterial({
1023 ref: session.clientSecretRef,
1024 })
1025 : null;
1026 const tokenResponse = yield* exchangeOAuth2AuthorizationCode({
1027 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1028 clientId: session.clientId,
1029 clientAuthentication: session.clientAuthentication,
1030 clientSecret,
1031 redirectUri: session.redirectUrl,
1032 codeVerifier: session.codeVerifier,
1033 code: args.code,
1034 });
1035 const expiresAt =
1036 typeof tokenResponse.expires_in === "number"
1037 ? Date.now() + tokenResponse.expires_in * 1000
1038 : null;
1039 const storeSecretMaterial = yield* SecretMaterialStorerService;
1040
1041 if (session.kind === "single") {
1042 const accessTokenRef = yield* storeSecretMaterial({
1043 purpose: "oauth_access_token",
1044 value: tokenResponse.access_token,
1045 name: `${session.service} Google Access Token`,
1046 });
1047 const refreshTokenRef = tokenResponse.refresh_token
1048 ? yield* storeSecretMaterial({
1049 purpose: "oauth_refresh_token",
1050 value: tokenResponse.refresh_token,
1051 name: `${session.service} Google Refresh Token`,
1052 })
1053 : null;
1054
1055 if (options.oauthSessions.remove) {
1056 yield* options.oauthSessions.remove(args.state);
1057 }
1058
1059 return {
1060 type: "executor:oauth-result" as const,
1061 ok: true as const,
1062 sessionId: args.state,
1063 mode: "single" as const,
1064 auth: {
1065 kind: "oauth2" as const,
1066 clientId: session.clientId,
1067 clientSecretRef: session.clientSecretRef,
1068 clientAuthentication: session.clientAuthentication,
1069 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
1070 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1071 scopes: session.scopes,
1072 accessTokenRef,
1073 refreshTokenRef,
1074 expiresAt,
1075 },
1076 };
1077 }
1078
1079 const createdSources = [];
1080 for (const sessionSource of session.sources) {
1081 const accessTokenRef = yield* storeSecretMaterial({
1082 purpose: "oauth_access_token",
1083 value: tokenResponse.access_token,
1084 name: `${sessionSource.service} Google Access Token`,
1085 });
1086 const refreshTokenRef = tokenResponse.refresh_token
1087 ? yield* storeSecretMaterial({
1088 purpose: "oauth_refresh_token",
1089 value: tokenResponse.refresh_token,
1090 name: `${sessionSource.service} Google Refresh Token`,
1091 })
1092 : null;
1093
1094 const createdSource = yield* source.createSource({
1095 name: sessionSource.name,
1096 service: sessionSource.service,
1097 version: sessionSource.version,
1098 discoveryUrl: sessionSource.discoveryUrl,
1099 defaultHeaders: sessionSource.defaultHeaders,
1100 scopes:
1101 sessionSource.scopes.length > 0
1102 ? sessionSource.scopes
1103 : session.scopes,
1104 auth: {
1105 kind: "oauth2",
1106 clientId: session.clientId,
1107 clientSecretRef: session.clientSecretRef,
1108 clientAuthentication: session.clientAuthentication,
1109 authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
1110 tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
1111 scopes:
1112 sessionSource.scopes.length > 0
1113 ? sessionSource.scopes
1114 : session.scopes,
1115 accessTokenRef,
1116 refreshTokenRef,
1117 expiresAt,
1118 },
1119 });
1120 createdSources.push(createdSource);
1121 }
1122
1123 if (options.oauthSessions.remove) {
1124 yield* options.oauthSessions.remove(args.state);
1125 }
1126
1127 return {
1128 type: "executor:oauth-result" as const,
1129 ok: true as const,
1130 sessionId: args.state,
1131 mode: "batch" as const,
1132 sources: createdSources.map((createdSource) => ({
1133 id: createdSource.id,
1134 name: createdSource.name,
1135 })),
1136 };
1137 }),
1138 },
1139 ] as const,
728 extendExecutor: ({ source, executor }) => {1140 extendExecutor: ({ source, executor }) => {
729 const provideRuntime = <A>(1141 const provideRuntime = <A>(
730 effect: Effect.Effect<A, Error, any>,1142 effect: Effect.Effect<A, Error, any>,
plugins/graphql/sdk/index.ts+125−15
@@ -4,25 +4,38 @@ import { · 3 unmodified lines
4 applyJsonBodyPlacements,4 applyJsonBodyPlacements,
5} from "@executor/codemode-core";5} from "@executor/codemode-core";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
8 9
9import {10import {
10 createCatalogImportMetadata,11 createCatalogImportMetadata,
11 createSourceCatalogSyncResult,12 createSourceCatalogSyncResult,
12} from "@executor/source-core";13} from "@executor/source-core";
13import type { Source } from "@executor/platform-sdk/schema";14import type {
15 LocalExecutorConfig,
16 Source,
17} from "@executor/platform-sdk/schema";
18import {
19 SourceSchema,
20} from "@executor/platform-sdk/schema";
14import {21import {
15 defineExecutorSourcePlugin,22 defineExecutorSourcePlugin,
16} from "@executor/platform-sdk/plugins";23} from "@executor/platform-sdk/plugins";
17import {24import {
18 SecretMaterialResolverService,25 SecretMaterialResolverService,
26 createPluginLocalConfigEntrySchema,
27 decodeCurrentOrLegacyLocalConfigAuth,
28 pluginLocalConfigSourceFromConfig,
19 provideExecutorRuntime,29 provideExecutorRuntime,
20 runtimeEffectError,30 runtimeEffectError,
21} from "@executor/platform-sdk/runtime";31} from "@executor/platform-sdk/runtime";
22import {32import {
23 deriveGraphqlNamespace,33 deriveGraphqlNamespace,
34 GraphqlConnectInputSchema,
24 GraphqlConnectionAuthSchema,35 GraphqlConnectionAuthSchema,
36 GraphqlSourceConfigPayloadSchema,
25 GraphqlStoredSourceDataSchema,37 GraphqlStoredSourceDataSchema,
38 GraphqlUpdateSourceInputSchema,
26 type GraphqlConnectInput,39 type GraphqlConnectInput,
27 type GraphqlSourceConfigPayload,40 type GraphqlSourceConfigPayload,
28 type GraphqlStoredSourceData,41 type GraphqlStoredSourceData,
@@ -44,6 +57,10 @@ import { · 15 unmodified lines
44 type GraphqlToolProviderData,57 type GraphqlToolProviderData,
45} from "./provider-data";58} from "./provider-data";
46 59
60const GraphqlSourceIdInputSchema = Schema.Struct({
61 sourceId: Schema.String,
62});
63
47const decodeStoredSourceData = Schema.decodeUnknownSync(GraphqlStoredSourceDataSchema);64const decodeStoredSourceData = Schema.decodeUnknownSync(GraphqlStoredSourceDataSchema);
48const decodeProviderData = Schema.decodeUnknownSync(GraphqlToolProviderDataSchema);65const decodeProviderData = Schema.decodeUnknownSync(GraphqlToolProviderDataSchema);
49 66
@@ -134,6 +151,32 @@ const asStringRecord = (value: unknown): Record<string, string> => · 84 unmodified lines
134 ),151 ),
135 );152 );
136 153
154const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(
155 createPluginLocalConfigEntrySchema({
156 kind: "graphql",
157 config: GraphqlStoredSourceDataSchema,
158 }),
159);
160
161const decodeGraphqlLegacyLocalConfigOption = Schema.decodeUnknownOption(
162 Schema.Struct({
163 kind: Schema.Literal("graphql"),
164 name: Schema.optional(Schema.String),
165 namespace: Schema.optional(Schema.String),
166 enabled: Schema.optional(Schema.Boolean),
167 connection: Schema.Struct({
168 endpoint: Schema.String,
169 auth: Schema.optional(Schema.Unknown),
170 }),
171 binding: Schema.Struct({
172 defaultHeaders: Schema.optional(
173 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
174 ),
175 }),
176 config: Schema.optional(Schema.Unknown),
177 }),
178);
179
137const withoutUndefinedEntries = (180const withoutUndefinedEntries = (
138 record: Readonly<Record<string, unknown>>,181 record: Readonly<Record<string, unknown>>,
139): Record<string, unknown> =>182): Record<string, unknown> =>
@@ -311,6 +354,41 @@ const graphqlConnectInputFromAddInput = ( · 171 unmodified lines
311 auth: input.auth,354 auth: input.auth,
312});355});
313 356
357const graphqlStoredSourceDataFromLocalConfig = (input: {
358 config: unknown;
359 loadedConfig: LocalExecutorConfig | null;
360}): GraphqlStoredSourceData => {
361 const current = decodeGraphqlCurrentLocalConfigOption(input.config);
362 if (Option.isSome(current)) {
363 return normalizeStoredSourceData(current.value.config);
364 }
365
366 const legacy = decodeGraphqlLegacyLocalConfigOption(input.config);
367 if (Option.isSome(legacy)) {
368 return storedSourceDataFromInput({
369 name: legacy.value.name ?? "GraphQL",
370 endpoint: legacy.value.connection.endpoint,
371 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
372 auth: decodeCurrentOrLegacyLocalConfigAuth({
373 auth: legacy.value.connection.auth,
374 authSchema: GraphqlConnectionAuthSchema,
375 loadedConfig: input.loadedConfig,
376 onLegacySecretRef: (tokenSecretRef) => ({
377 kind: "bearer",
378 tokenSecretRef,
379 headerName: null,
380 prefix: null,
381 }),
382 fallback: {
383 kind: "none",
384 },
385 }),
386 });
387 }
388
389 throw new Error("Unsupported GraphQL local source config.");
390};
391
314export const graphqlSdkPlugin = (options: {392export const graphqlSdkPlugin = (options: {
315 storage: GraphqlSourceStorage;393 storage: GraphqlSourceStorage;
316}) => defineExecutorSourcePlugin<394}) => defineExecutorSourcePlugin<
@@ -363,6 +441,18 @@ export const graphqlSdkPlugin = (options: { · 46 unmodified lines
363 toConfig: ({ source, stored }) =>441 toConfig: ({ source, stored }) =>
364 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),442 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),
365 },443 },
444 localConfig: {
445 toConfigSource: ({ source, stored }) =>
446 pluginLocalConfigSourceFromConfig({
447 source,
448 config: normalizeStoredSourceData(stored),
449 }),
450 recoverStored: ({ config, loadedConfig }) =>
451 graphqlStoredSourceDataFromLocalConfig({
452 config,
453 loadedConfig,
454 }),
455 },
366 catalog: {456 catalog: {
367 kind: "imported",457 kind: "imported",
368 identity: ({ source }) => ({458 identity: ({ source }) => ({
@@ -372,20 +462,10 @@ export const graphqlSdkPlugin = (options: { · 3 unmodified lines
372 sync: ({ source, stored }) =>462 sync: ({ source, stored }) =>
373 Effect.gen(function* () {463 Effect.gen(function* () {
374 if (stored === null) {464 if (stored === null) {
375 return createSourceCatalogSyncResult({465 return yield* runtimeEffectError(
376 fragment: {466 "plugins/graphql/sdk",
377 version: "ir.v1.fragment",467 `GraphQL source storage missing for ${source.id}`,
378 },468 );
379 importMetadata: {
380 ...createCatalogImportMetadata({
381 source,
382 pluginKey: "graphql",
383 }),
384 importerVersion: "ir.v1.graphql",
385 sourceConfigHash: "missing",
386 },
387 sourceHash: null,
388 });
389 }469 }
390 470
391 const normalizedStored = normalizeStoredSourceData(stored);471 const normalizedStored = normalizeStoredSourceData(stored);
@@ -522,6 +602,36 @@ export const graphqlSdkPlugin = (options: { · 130 unmodified lines
522 }),602 }),
523 },603 },
524 },604 },
605 tools: [
606 {
607 name: "getSourceConfig",
608 description: "Load the saved configuration for a GraphQL source.",
609 inputSchema: GraphqlSourceIdInputSchema,
610 outputSchema: GraphqlSourceConfigPayloadSchema,
611 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
612 },
613 {
614 name: "createSource",
615 description: "Create a GraphQL source.",
616 inputSchema: GraphqlConnectInputSchema,
617 outputSchema: SourceSchema,
618 execute: ({ args, source }) => source.createSource(args),
619 },
620 {
621 name: "updateSource",
622 description: "Update a GraphQL source configuration.",
623 inputSchema: GraphqlUpdateSourceInputSchema,
624 outputSchema: SourceSchema,
625 execute: ({ args, source }) => source.updateSource(args),
626 },
627 {
628 name: "removeSource",
629 description: "Remove a GraphQL source and its stored plugin data.",
630 inputSchema: GraphqlSourceIdInputSchema,
631 outputSchema: Schema.Boolean,
632 execute: ({ args, source }) => source.removeSource(args.sourceId),
633 },
634 ] as const,
525 extendExecutor: ({ source, executor }) => {635 extendExecutor: ({ source, executor }) => {
526 const provideRuntime = <A>(636 const provideRuntime = <A>(
527 effect: Effect.Effect<A, Error, any>,637 effect: Effect.Effect<A, Error, any>,
plugins/mcp/sdk/index.ts+275−15
@@ -21,13 +21,21 @@ import { · 20 unmodified lines
21 createSourceCatalogSyncResult,21 createSourceCatalogSyncResult,
22 normalizeSourceDiscoveryUrl,22 normalizeSourceDiscoveryUrl,
23 probeHeadersFromAuth,23 probeHeadersFromAuth,
24 SourceTransportSchema,
25 StringArraySchema,
26 StringMapSchema,
24 type SourceDiscoveryResult,27 type SourceDiscoveryResult,
25} from "@executor/source-core";28} from "@executor/source-core";
26import type { Source } from "@executor/platform-sdk/schema";29import {
30 type Source,
31 SourceSchema,
32} from "@executor/platform-sdk/schema";
27import {33import {
28 defineExecutorSourcePlugin,34 defineExecutorSourcePlugin,
29} from "@executor/platform-sdk/plugins";35} from "@executor/platform-sdk/plugins";
30import {36import {
37 createPluginLocalConfigEntrySchema,
38 pluginLocalConfigSourceFromConfig,
31 SecretMaterialDeleterService,39 SecretMaterialDeleterService,
32 SecretMaterialResolverService,40 SecretMaterialResolverService,
33 SecretMaterialStorerService,41 SecretMaterialStorerService,
@@ -36,9 +44,17 @@ import { · 2 unmodified lines
36 runtimeEffectError,44 runtimeEffectError,
37} from "@executor/platform-sdk/runtime";45} from "@executor/platform-sdk/runtime";
38import {46import {
47 McpConnectInputSchema,
39 McpConnectionAuthSchema,48 McpConnectionAuthSchema,
49 McpDiscoverInputSchema,
50 McpDiscoverResultSchema,
51 McpOAuthPopupResultSchema,
40 McpOAuthSessionSchema,52 McpOAuthSessionSchema,
53 McpSourceConfigPayloadSchema,
54 McpStartOAuthInputSchema,
55 McpStartOAuthResultSchema,
41 McpStoredSourceDataSchema,56 McpStoredSourceDataSchema,
57 McpUpdateSourceInputSchema,
42 deriveMcpNamespace,58 deriveMcpNamespace,
43 resolveMcpEndpoint,59 resolveMcpEndpoint,
44 type McpConnectInput,60 type McpConnectInput,
@@ -54,6 +70,7 @@ import { · 9 unmodified lines
54 type McpUpdateSourceInput,70 type McpUpdateSourceInput,
55} from "@executor/plugin-mcp-shared";71} from "@executor/plugin-mcp-shared";
56import * as Effect from "effect/Effect";72import * as Effect from "effect/Effect";
73import * as Option from "effect/Option";
57import * as Schema from "effect/Schema";74import * as Schema from "effect/Schema";
58 75
59import {76import {
@@ -83,6 +100,16 @@ import { · 23 unmodified lines
83const decodeStoredSourceData = Schema.decodeUnknownSync(McpStoredSourceDataSchema);100const decodeStoredSourceData = Schema.decodeUnknownSync(McpStoredSourceDataSchema);
84const decodeSession = Schema.decodeUnknownSync(McpOAuthSessionSchema);101const decodeSession = Schema.decodeUnknownSync(McpOAuthSessionSchema);
85 102
103const McpSourceIdInputSchema = Schema.Struct({
104 sourceId: Schema.String,
105});
106const McpCompleteOAuthInputSchema = Schema.Struct({
107 state: Schema.String,
108 code: Schema.optional(Schema.String),
109 error: Schema.optional(Schema.String),
110 errorDescription: Schema.optional(Schema.String),
111});
112
86const McpExecutableBindingSchema = Schema.Struct({113const McpExecutableBindingSchema = Schema.Struct({
87 toolId: Schema.String,114 toolId: Schema.String,
88 toolName: Schema.String,115 toolName: Schema.String,
@@ -222,6 +249,38 @@ const normalizeStringArray = ( · 133 unmodified lines
222 return normalized.length > 0 ? normalized : null;249 return normalized.length > 0 ? normalized : null;
223};250};
224 251
252const decodeMcpAuthOption = Schema.decodeUnknownOption(McpConnectionAuthSchema);
253const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(
254 createPluginLocalConfigEntrySchema({
255 kind: "mcp",
256 config: McpStoredSourceDataSchema,
257 }),
258);
259const decodeMcpLegacyLocalConfigOption = Schema.decodeUnknownOption(
260 Schema.Struct({
261 kind: Schema.Literal("mcp"),
262 name: Schema.optional(Schema.String),
263 namespace: Schema.optional(Schema.String),
264 enabled: Schema.optional(Schema.Boolean),
265 connection: Schema.optional(
266 Schema.Struct({
267 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
268 auth: Schema.optional(Schema.Unknown),
269 }),
270 ),
271 binding: Schema.Struct({
272 transport: Schema.optional(Schema.NullOr(SourceTransportSchema)),
273 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
274 headers: Schema.optional(Schema.NullOr(StringMapSchema)),
275 command: Schema.optional(Schema.NullOr(Schema.String)),
276 args: Schema.optional(Schema.NullOr(StringArraySchema)),
277 env: Schema.optional(Schema.NullOr(StringMapSchema)),
278 cwd: Schema.optional(Schema.NullOr(Schema.String)),
279 }),
280 config: Schema.optional(Schema.Unknown),
281 }),
282);
283
225const normalizeStoredSourceData = (284const normalizeStoredSourceData = (
226 input: McpConnectInput | McpSourceConfigPayload,285 input: McpConnectInput | McpSourceConfigPayload,
227): Effect.Effect<McpStoredSourceData, Error, never> =>286): Effect.Effect<McpStoredSourceData, Error, never> =>
@@ -332,6 +391,51 @@ const mcpConnectInputFromAddInput = ( · 104 unmodified lines
332 auth: input.auth,391 auth: input.auth,
333});392});
334 393
394const mcpStoredSourceDataFromLocalConfig = (
395 config: unknown,
396): Effect.Effect<McpStoredSourceData, Error, never> => {
397 const current = decodeMcpCurrentLocalConfigOption(config);
398 if (Option.isSome(current)) {
399 return Effect.succeed(current.value.config);
400 }
401
402 const legacy = decodeMcpLegacyLocalConfigOption(config);
403 if (Option.isSome(legacy)) {
404 const auth = decodeMcpAuthOption(legacy.value.connection?.auth);
405 if (
406 legacy.value.connection?.auth !== undefined &&
407 Option.isNone(auth)
408 ) {
409 return runtimeEffectError(
410 "plugins/mcp/sdk",
411 "Unsupported MCP auth configuration in local source config.",
412 );
413 }
414
415 return normalizeStoredSourceData({
416 name: legacy.value.name ?? "MCP",
417 endpoint: legacy.value.connection?.endpoint ?? null,
418 transport: legacy.value.binding.transport ?? null,
419 queryParams: legacy.value.binding.queryParams ?? null,
420 headers: legacy.value.binding.headers ?? null,
421 command: legacy.value.binding.command ?? null,
422 args: legacy.value.binding.args ?? null,
423 env: legacy.value.binding.env ?? null,
424 cwd: legacy.value.binding.cwd ?? null,
425 auth: Option.isSome(auth)
426 ? auth.value
427 : {
428 kind: "none",
429 },
430 });
431 }
432
433 return runtimeEffectError(
434 "plugins/mcp/sdk",
435 "Unsupported MCP local source config.",
436 );
437};
438
335const mcpCatalogOperationFromManifestEntry = (input: {439const mcpCatalogOperationFromManifestEntry = (input: {
336 entry: McpToolManifestEntry;440 entry: McpToolManifestEntry;
337 server: McpServerMetadata | null | undefined;441 server: McpServerMetadata | null | undefined;
@@ -658,6 +762,15 @@ export const mcpSdkPlugin = ( · 320 unmodified lines
658 }762 }
659 }),763 }),
660 },764 },
765 localConfig: {
766 toConfigSource: ({ source, stored }) =>
767 pluginLocalConfigSourceFromConfig({
768 source,
769 config: stored,
770 }),
771 recoverStored: ({ config }) =>
772 mcpStoredSourceDataFromLocalConfig(config),
773 },
661 catalog: {774 catalog: {
662 kind: "imported",775 kind: "imported",
663 identity: ({ source }) => ({776 identity: ({ source }) => ({
@@ -667,20 +780,10 @@ export const mcpSdkPlugin = ( · 3 unmodified lines
667 sync: ({ source, stored }) =>780 sync: ({ source, stored }) =>
668 Effect.gen(function* () {781 Effect.gen(function* () {
669 if (stored === null) {782 if (stored === null) {
670 return createSourceCatalogSyncResult({783 return yield* runtimeEffectError(
671 fragment: {784 "plugins/mcp/sdk",
672 version: "ir.v1.fragment",785 `MCP source storage missing for ${source.id}`,
673 },786 );
674 importMetadata: {
675 ...createCatalogImportMetadata({
676 source,
677 pluginKey: "mcp",
678 }),
679 importerVersion: "ir.v1.mcp",
680 sourceConfigHash: "missing",
681 },
682 sourceHash: null,
683 });
684 }787 }
685 788
686 const connector = yield* createStoredMcpConnector({789 const connector = yield* createStoredMcpConnector({
@@ -853,6 +956,163 @@ export const mcpSdkPlugin = ( · 166 unmodified lines
853 }),956 }),
854 },957 },
855 },958 },
959 tools: [
960 {
961 name: "discoverSource",
962 description: "Probe an MCP endpoint before creating a source.",
963 inputSchema: McpDiscoverInputSchema,
964 outputSchema: McpDiscoverResultSchema,
965 execute: ({ args }) =>
966 Effect.gen(function* () {
967 const normalizedUrl = normalizeSourceDiscoveryUrl(args.endpoint);
968 const discovered = yield* detectMcpSource({
969 normalizedUrl,
970 headers: probeHeadersFromAuth(args.probeAuth ?? null),
971 });
972 return discovered satisfies SourceDiscoveryResult | null;
973 }),
974 },
975 {
976 name: "getSourceConfig",
977 description: "Load the saved configuration for an MCP source.",
978 inputSchema: McpSourceIdInputSchema,
979 outputSchema: McpSourceConfigPayloadSchema,
980 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
981 },
982 {
983 name: "createSource",
984 description: "Create an MCP source.",
985 inputSchema: McpConnectInputSchema,
986 outputSchema: SourceSchema,
987 execute: ({ args, source }) => source.createSource(args),
988 },
989 {
990 name: "updateSource",
991 description: "Update an MCP source configuration.",
992 inputSchema: McpUpdateSourceInputSchema,
993 outputSchema: SourceSchema,
994 execute: ({ args, source }) => source.updateSource(args),
995 },
996 {
997 name: "removeSource",
998 description: "Remove an MCP source and its stored plugin data.",
999 inputSchema: McpSourceIdInputSchema,
1000 outputSchema: Schema.Boolean,
1001 execute: ({ args, source }) => source.removeSource(args.sourceId),
1002 },
1003 {
1004 name: "startOAuth",
1005 description: "Start an MCP OAuth flow for a source connection.",
1006 inputSchema: McpStartOAuthInputSchema,
1007 outputSchema: McpStartOAuthResultSchema,
1008 execute: ({ args }) =>
1009 Effect.gen(function* () {
1010 const endpoint = resolveMcpEndpoint({
1011 endpoint: args.endpoint.trim(),
1012 queryParams: args.queryParams,
1013 });
1014 const sessionId = `mcp_oauth_${crypto.randomUUID()}`;
1015 const started = yield* startMcpOAuthAuthorization({
1016 endpoint,
1017 redirectUrl: args.redirectUrl,
1018 state: sessionId,
1019 });
1020
1021 yield* options.oauthSessions.put({
1022 sessionId,
1023 value: decodeSession({
1024 endpoint,
1025 redirectUrl: args.redirectUrl,
1026 codeVerifier: started.codeVerifier,
1027 resourceMetadataUrl: started.resourceMetadataUrl,
1028 authorizationServerUrl: started.authorizationServerUrl,
1029 resourceMetadata: started.resourceMetadata,
1030 authorizationServerMetadata: started.authorizationServerMetadata,
1031 clientInformation: started.clientInformation,
1032 }),
1033 });
1034
1035 return {
1036 sessionId,
1037 authorizationUrl: started.authorizationUrl,
1038 };
1039 }),
1040 },
1041 {
1042 name: "completeOAuth",
1043 description: "Complete an MCP OAuth callback and return the connected auth payload.",
1044 inputSchema: McpCompleteOAuthInputSchema,
1045 outputSchema: McpOAuthPopupResultSchema,
1046 execute: ({ args }) =>
1047 Effect.gen(function* () {
1048 if (args.error) {
1049 return yield* runtimeEffectError(
1050 "plugins/mcp/sdk",
1051 args.errorDescription || args.error || "MCP OAuth failed",
1052 );
1053 }
1054 if (!args.code) {
1055 return yield* runtimeEffectError(
1056 "plugins/mcp/sdk",
1057 "Missing MCP OAuth code.",
1058 );
1059 }
1060
1061 const session = yield* options.oauthSessions.get(args.state);
1062 if (session === null) {
1063 return yield* runtimeEffectError(
1064 "plugins/mcp/sdk",
1065 `MCP OAuth session not found: ${args.state}`,
1066 );
1067 }
1068
1069 const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
1070 session,
1071 code: args.code,
1072 });
1073 const storeSecretMaterial = yield* SecretMaterialStorerService;
1074 const accessTokenRef = yield* storeSecretMaterial({
1075 purpose: "oauth_access_token",
1076 value: exchanged.tokens.access_token,
1077 name: "MCP Access Token",
1078 });
1079 const refreshTokenRef = exchanged.tokens.refresh_token
1080 ? yield* storeSecretMaterial({
1081 purpose: "oauth_refresh_token",
1082 value: exchanged.tokens.refresh_token,
1083 name: "MCP Refresh Token",
1084 })
1085 : null;
1086
1087 if (options.oauthSessions.remove) {
1088 yield* options.oauthSessions.remove(args.state);
1089 }
1090
1091 return {
1092 type: "executor:oauth-result" as const,
1093 ok: true as const,
1094 sessionId: args.state,
1095 auth: {
1096 kind: "oauth2" as const,
1097 redirectUri: session.redirectUrl,
1098 accessTokenRef,
1099 refreshTokenRef,
1100 tokenType: exchanged.tokens.token_type ?? "Bearer",
1101 expiresAt:
1102 typeof exchanged.tokens.expires_in === "number"
1103 ? Date.now() + exchanged.tokens.expires_in * 1000
1104 : null,
1105 scope: exchanged.tokens.scope ?? null,
1106 resourceMetadataUrl: session.resourceMetadataUrl,
1107 authorizationServerUrl: session.authorizationServerUrl,
1108 resourceMetadata: session.resourceMetadata,
1109 authorizationServerMetadata: session.authorizationServerMetadata,
1110 clientInformation: session.clientInformation,
1111 },
1112 };
1113 }),
1114 },
1115 ] as const,
856 extendExecutor: ({ source, executor }) => {1116 extendExecutor: ({ source, executor }) => {
857 const provideRuntime = <A>(1117 const provideRuntime = <A>(
858 effect: Effect.Effect<A, Error, any>,1118 effect: Effect.Effect<A, Error, any>,
plugins/openapi/sdk/index.ts+165−15
@@ -1,21 +1,36 @@
1import { createHash } from "node:crypto";1import { createHash } from "node:crypto";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
4 5
5import {6import {
6 createCatalogImportMetadata,7 createCatalogImportMetadata,
7 createSourceCatalogSyncResult,8 createSourceCatalogSyncResult,
8} from "@executor/source-core";9} from "@executor/source-core";
9import type { Source } from "@executor/platform-sdk/schema";10import type {
11 LocalExecutorConfig,
12 Source,
13} from "@executor/platform-sdk/schema";
14import {
15 SourceSchema,
16} from "@executor/platform-sdk/schema";
10import {17import {
11 defineExecutorSourcePlugin,18 defineExecutorSourcePlugin,
12} from "@executor/platform-sdk/plugins";19} from "@executor/platform-sdk/plugins";
13import {20import {
21 createPluginLocalConfigEntrySchema,
22 decodeCurrentOrLegacyLocalConfigAuth,
23 pluginLocalConfigSourceFromConfig,
14 runtimeEffectError,24 runtimeEffectError,
15 SecretMaterialResolverService,25 SecretMaterialResolverService,
16} from "@executor/platform-sdk/runtime";26} from "@executor/platform-sdk/runtime";
17import {27import {
18 OpenApiConnectionAuthSchema,28 OpenApiConnectionAuthSchema,
29 OpenApiConnectInputSchema,
30 OpenApiPreviewRequestSchema,
31 OpenApiPreviewResponseSchema,
32 OpenApiSourceConfigPayloadSchema,
33 OpenApiUpdateSourceInputSchema,
19 deriveOpenApiNamespace,34 deriveOpenApiNamespace,
20 previewOpenApiDocument,35 previewOpenApiDocument,
21 type OpenApiConnectInput,36 type OpenApiConnectInput,
@@ -46,6 +61,10 @@ import { · 24 unmodified lines
46 type OpenApiToolProviderData,61 type OpenApiToolProviderData,
47} from "./types";62} from "./types";
48 63
64const OpenApiSourceIdInputSchema = Schema.Struct({
65 sourceId: Schema.String,
66});
67
49const stableSourceHash = (value: OpenApiStoredSourceData): string =>68const stableSourceHash = (value: OpenApiStoredSourceData): string =>
50 createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);69 createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);
51 70
@@ -130,6 +149,40 @@ const normalizeStoredSourceData = ( · 78 unmodified lines
130 auth: normalizeOpenApiAuth(stored.auth as Record<string, unknown>),149 auth: normalizeOpenApiAuth(stored.auth as Record<string, unknown>),
131});150});
132 151
152const OpenApiLocalSourceConfigSchema = Schema.Struct({
153 specUrl: Schema.String,
154 baseUrl: Schema.NullOr(Schema.String),
155 auth: OpenApiConnectionAuthSchema,
156 defaultHeaders: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
157});
158
159const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(
160 createPluginLocalConfigEntrySchema({
161 kind: "openapi",
162 config: OpenApiLocalSourceConfigSchema,
163 }),
164);
165
166const decodeOpenApiLegacyLocalConfigOption = Schema.decodeUnknownOption(
167 Schema.Struct({
168 kind: Schema.Literal("openapi"),
169 name: Schema.optional(Schema.String),
170 namespace: Schema.optional(Schema.String),
171 enabled: Schema.optional(Schema.Boolean),
172 connection: Schema.Struct({
173 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
174 auth: Schema.optional(Schema.Unknown),
175 }),
176 binding: Schema.Struct({
177 specUrl: Schema.String,
178 defaultHeaders: Schema.optional(
179 Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
180 ),
181 }),
182 config: Schema.optional(Schema.Unknown),
183 }),
184);
185
133const createStoredSourceData = (186const createStoredSourceData = (
134 input: OpenApiConnectInput,187 input: OpenApiConnectInput,
135): OpenApiStoredSourceData => ({188): OpenApiStoredSourceData => ({
@@ -158,6 +211,47 @@ const configFromStoredSourceData = ( · 22 unmodified lines
158 auth: stored.auth,211 auth: stored.auth,
159});212});
160 213
214const openApiStoredSourceDataFromLocalConfig = (input: {
215 config: unknown;
216 loadedConfig: LocalExecutorConfig | null;
217}): OpenApiStoredSourceData => {
218 const current = decodeOpenApiCurrentLocalConfigOption(input.config);
219 if (Option.isSome(current)) {
220 return normalizeStoredSourceData({
221 ...current.value.config,
222 etag: null,
223 lastSyncAt: null,
224 });
225 }
226
227 const legacy = decodeOpenApiLegacyLocalConfigOption(input.config);
228 if (Option.isSome(legacy)) {
229 return normalizeStoredSourceData({
230 specUrl: legacy.value.binding.specUrl.trim(),
231 baseUrl: legacy.value.connection.endpoint?.trim() || null,
232 auth: decodeCurrentOrLegacyLocalConfigAuth({
233 auth: legacy.value.connection.auth,
234 authSchema: OpenApiConnectionAuthSchema,
235 loadedConfig: input.loadedConfig,
236 onLegacySecretRef: (tokenSecretRef) => ({
237 kind: "bearer",
238 tokenSecretRef,
239 headerName: null,
240 prefix: null,
241 }),
242 fallback: {
243 kind: "none",
244 },
245 }),
246 defaultHeaders: legacy.value.binding.defaultHeaders ?? null,
247 etag: null,
248 lastSyncAt: null,
249 });
250 }
251
252 throw new Error("Unsupported OpenAPI local source config.");
253};
254
161const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);255const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);
162 256
163const asRecord = (value: unknown): Record<string, unknown> =>257const asRecord = (value: unknown): Record<string, unknown> =>
@@ -491,6 +585,23 @@ export const openApiSdkPlugin = ( · 327 unmodified lines
491 toConfig: ({ source, stored }) =>585 toConfig: ({ source, stored }) =>
492 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),586 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),
493 },587 },
588 localConfig: {
589 toConfigSource: ({ source, stored }) =>
590 pluginLocalConfigSourceFromConfig({
591 source,
592 config: {
593 specUrl: stored.specUrl,
594 baseUrl: stored.baseUrl,
595 auth: stored.auth,
596 defaultHeaders: stored.defaultHeaders,
597 },
598 }),
599 recoverStored: ({ config, loadedConfig }) =>
600 openApiStoredSourceDataFromLocalConfig({
601 config,
602 loadedConfig,
603 }),
604 },
494 catalog: {605 catalog: {
495 kind: "imported",606 kind: "imported",
496 identity: ({ source }) => ({607 identity: ({ source }) => ({
@@ -500,20 +611,10 @@ export const openApiSdkPlugin = ( · 3 unmodified lines
500 sync: ({ source, stored }) =>611 sync: ({ source, stored }) =>
501 Effect.gen(function* () {612 Effect.gen(function* () {
502 if (stored === null) {613 if (stored === null) {
503 return createSourceCatalogSyncResult({614 return yield* runtimeEffectError(
504 fragment: {615 "plugins/openapi/sdk",
505 version: "ir.v1.fragment",616 `OpenAPI source storage missing for ${source.id}`,
506 },617 );
507 importMetadata: {
508 ...createCatalogImportMetadata({
509 source,
510 pluginKey: "openapi",
511 }),
512 importerVersion: "ir.v1.openapi",
513 sourceConfigHash: "missing",
514 },
515 sourceHash: null,
516 });
517 }618 }
518 619
519 const normalizedStored = normalizeStoredSourceData(stored);620 const normalizedStored = normalizeStoredSourceData(stored);
@@ -713,6 +814,55 @@ export const openApiSdkPlugin = ( · 193 unmodified lines
713 }),814 }),
714 },815 },
715 },816 },
817 tools: [
818 {
819 name: "previewDocument",
820 description: "Preview an OpenAPI document before creating a source.",
821 inputSchema: OpenApiPreviewRequestSchema,
822 outputSchema: OpenApiPreviewResponseSchema,
823 execute: ({ args }) =>
824 Effect.tryPromise({
825 try: () => previewOpenApiDocument(args),
826 catch: (cause) =>
827 cause instanceof Error ? cause : new Error(String(cause)),
828 }),
829 },
830 {
831 name: "getSourceConfig",
832 description: "Load the saved configuration for an OpenAPI source.",
833 inputSchema: OpenApiSourceIdInputSchema,
834 outputSchema: OpenApiSourceConfigPayloadSchema,
835 execute: ({ args, source }) => source.getSourceConfig(args.sourceId),
836 },
837 {
838 name: "createSource",
839 description: "Create an OpenAPI source.",
840 inputSchema: OpenApiConnectInputSchema,
841 outputSchema: SourceSchema,
842 execute: ({ args, source }) => source.createSource(args),
843 },
844 {
845 name: "updateSource",
846 description: "Update an OpenAPI source configuration.",
847 inputSchema: OpenApiUpdateSourceInputSchema,
848 outputSchema: SourceSchema,
849 execute: ({ args, source }) => source.updateSource(args),
850 },
851 {
852 name: "refreshSource",
853 description: "Refresh an OpenAPI source and resync its catalog.",
854 inputSchema: OpenApiSourceIdInputSchema,
855 outputSchema: SourceSchema,
856 execute: ({ args, source }) => source.refreshSource(args.sourceId),
857 },
858 {
859 name: "removeSource",
860 description: "Remove an OpenAPI source and its stored plugin data.",
861 inputSchema: OpenApiSourceIdInputSchema,
862 outputSchema: Schema.Boolean,
863 execute: ({ args, source }) => source.removeSource(args.sourceId),
864 },
865 ] as const,
716 extendExecutor: ({ source, executor }) => {866 extendExecutor: ({ source, executor }) => {
717 const provideRuntime = <A>(867 const provideRuntime = <A>(
718 effect: Effect.Effect<A, Error, any>,868 effect: Effect.Effect<A, Error, any>,