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