Unified · Split
| @@ -48,7 +48,7 @@ Open `http://127.0.0.1:4788`, go to **Add Source**, paste a URL, and Executor wi · 47 unmodified lines | ||
| 48 | 48 | ### Via the CLI |
| 49 | 49 | |
| 50 | 50 | ```bash |
| 51 | −executor call openapi addSource '{ | |
| 51 | +executor call executor openapi addSource '{ | |
| 52 | 52 | "spec": "https://petstore3.swagger.io/api/v3/openapi.json", |
| 53 | 53 | "namespace": "petstore", |
| 54 | 54 | "baseUrl": "https://petstore3.swagger.io/api/v3" |
| @@ -14,7 +14,7 @@ · 13 unmodified lines | ||
| 14 | 14 | // |
| 15 | 15 | // Elicitation coverage uses the openapi plugin: a tiny Effect HttpApi |
| 16 | 16 | // upstream is stood up in-process, its generated spec is handed to |
| 17 | −// `tools.openapi.addSource`, and the cloud engine invokes a POST operation. | |
| 17 | +// `tools.executor.openapi.addSource`, and the cloud engine invokes a POST operation. | |
| 18 | 18 | // `requiresApproval: true` fires the executor's approval elicitation, which |
| 19 | 19 | // round-trips back to the SDK Client's `ElicitRequestSchema` handler. |
| 20 | 20 | // --------------------------------------------------------------------------- |
| @@ -455,7 +455,7 @@ layer(TestEnv, { timeout: 60_000 })("cloud MCP over real HTTP (miniflare)", (it) · 434 unmodified lines | ||
| 455 | 455 | method: "POST", |
| 456 | 456 | headers: { |
| 457 | 457 | accept: "application/json, text/event-stream", |
| 458 | − authorization: "Bearer bogus", | |
| 458 | + authorization: "Bearer bogus.bogus.bogus", | |
| 459 | 459 | "content-type": "application/json", |
| 460 | 460 | }, |
| 461 | 461 | body: JSON.stringify({ |
| @@ -794,7 +794,7 @@ layer(TestEnv, { timeout: 60_000 })("cloud MCP over real HTTP (miniflare)", (it) · 332 unmodified lines | ||
| 794 | 794 | // `HttpApiGroup` name ("approve") becomes part of the sandbox path, |
| 795 | 795 | // so the invocation reads `tools.approveapi.approve.approveThing`. |
| 796 | 796 | const code = [ |
| 797 | − `await tools.openapi.addSource({ scope: ${JSON.stringify(orgId)}, spec: ${JSON.stringify(specJson)}, namespace: "approveapi" });`, | |
| 797 | + `await tools.executor.openapi.addSource({ scope: ${JSON.stringify(orgId)}, spec: ${JSON.stringify(specJson)}, namespace: "approveapi" });`, | |
| 798 | 798 | `return await tools.approveapi.approve.approveThing({});`, |
| 799 | 799 | ].join("\n"); |
| 800 | 800 | const result = yield* Effect.promise(() => |
| @@ -15,7 +15,7 @@ · 14 unmodified lines | ||
| 15 | 15 | // before prod does. |
| 16 | 16 | |
| 17 | 17 | import { describe, expect, it } from "@effect/vitest"; |
| 18 | −import { Effect } from "effect"; | |
| 18 | +import { Effect, Schema } from "effect"; | |
| 19 | 19 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 20 | 20 | import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; |
| 21 | 21 | import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; |
| @@ -44,11 +44,9 @@ import { DbService } from "./services/db"; · 22 unmodified lines | ||
| 44 | 44 | // eliciting test drive the real engine + sandbox rather than a stub engine. |
| 45 | 45 | // --------------------------------------------------------------------------- |
| 46 | 46 | |
| 47 | −const EMPTY_INPUT_SCHEMA = { | |
| 48 | − type: "object", | |
| 49 | − properties: {}, | |
| 50 | − additionalProperties: false, | |
| 51 | −} as const; | |
| 47 | +const EMPTY_INPUT_SCHEMA = Schema.toStandardSchemaV1( | |
| 48 | + Schema.toStandardJSONSchemaV1(Schema.Struct({})), | |
| 49 | +); | |
| 52 | 50 | |
| 53 | 51 | const elicitingTestPlugin = definePlugin(() => ({ |
| 54 | 52 | id: "eliciting-test" as const, |
| @@ -676,13 +676,13 @@ describe("sources api (HTTP)", () => { · 675 unmodified lines | ||
| 676 | 676 | it.effect("sources.remove on a static source is rejected", () => |
| 677 | 677 | Effect.gen(function* () { |
| 678 | 678 | // `canRemove: false` is reserved for static (plugin-declared) |
| 679 | − // sources. The openapi plugin declares one with id "openapi" | |
| 680 | − // for its control tools. | |
| 679 | + // sources. Plugin-owned executor tools are mounted under the | |
| 680 | + // built-in executor source. | |
| 681 | 681 | const org = `org_${crypto.randomUUID()}`; |
| 682 | 682 | |
| 683 | 683 | const result = yield* asOrg(org, (client) => |
| 684 | 684 | client.sources |
| 685 | − .remove({ params: { scopeId: ScopeId.make(org), sourceId: "openapi" } }) | |
| 685 | + .remove({ params: { scopeId: ScopeId.make(org), sourceId: "executor" } }) | |
| 686 | 686 | .pipe(Effect.result), |
| 687 | 687 | ); |
| 688 | 688 | expect(Result.isFailure(result)).toBe(true); |
| @@ -298,7 +298,7 @@ function SourceList(props: { pathname: string; onNavigate?: () => void }) { · 297 unmodified lines | ||
| 298 | 298 | : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", |
| 299 | 299 | ].join(" ")} |
| 300 | 300 | > |
| 301 | − <SourceFavicon url={s.url} /> | |
| 301 | + <SourceFavicon sourceId={s.id} url={s.url} /> | |
| 302 | 302 | <span className="flex-1 truncate">{s.name}</span> |
| 303 | 303 | <span className="rounded bg-secondary/50 px-1 py-px text-xs font-medium text-muted-foreground"> |
| 304 | 304 | {s.kind} |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { readFileSync } from "node:fs"; |
| 2 | +import { resolve } from "node:path"; | |
| 2 | 3 | import { Readable } from "node:stream"; |
| 3 | 4 | import { defineConfig, type Plugin } from "vite"; |
| 4 | 5 | import react from "@vitejs/plugin-react"; |
| @@ -38,6 +39,11 @@ function executorApiPlugin(): Plugin { · 33 unmodified lines | ||
| 38 | 39 | return { |
| 39 | 40 | name: "executor-api", |
| 40 | 41 | configureServer(server) { |
| 42 | + server.watcher.on("change", (path) => { | |
| 43 | + if (path.includes("/src/server/") || path.endsWith("/executor.config.ts")) { | |
| 44 | + handlers = null; | |
| 45 | + } | |
| 46 | + }); | |
| 41 | 47 | server.middlewares.use(async (req, res, next) => { |
| 42 | 48 | const rawUrl = req.url ?? "/"; |
| 43 | 49 | const isApi = rawUrl.startsWith("/api/") || rawUrl === "/api"; |
| @@ -121,6 +127,9 @@ export default defineConfig({ · 77 unmodified lines | ||
| 121 | 127 | usePolling: true, |
| 122 | 128 | interval: 200, |
| 123 | 129 | }, |
| 130 | + fs: { | |
| 131 | + allow: [resolve(import.meta.dirname, "../..")], | |
| 132 | + }, | |
| 124 | 133 | }, |
| 125 | 134 | plugins: [ |
| 126 | 135 | tailwindcss(), |
| @@ -316,6 +316,7 @@ · 315 unmodified lines | ||
| 316 | 316 | "version": "0.2.0", |
| 317 | 317 | "dependencies": { |
| 318 | 318 | "@executor-js/storage-core": "workspace:*", |
| 319 | + "@standard-schema/spec": "^1.1.0", | |
| 319 | 320 | "effect": "catalog:", |
| 320 | 321 | "fractional-indexing": "^3.2.0", |
| 321 | 322 | "oauth4webapi": "^3.8.5", |
| @@ -90,7 +90,7 @@ Pattern used in `mcp-miniflare.e2e.node.test.ts`: · 89 unmodified lines | ||
| 90 | 90 | 5. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and |
| 91 | 91 | registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns |
| 92 | 92 | `{ action: "accept", content: {} }`. |
| 93 | −6. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec, | |
| 93 | +6. Call `execute` with code that (a) calls `tools.executor.openapi.addSource({ spec, | |
| 94 | 94 | namespace })`, (b) invokes the POST operation. |
| 95 | 95 | |
| 96 | 96 | **Tool id format inside the `execute` sandbox.** |
| @@ -1,15 +1,13 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | −import { Effect } from "effect"; | |
| 2 | +import { Effect, Schema } from "effect"; | |
| 3 | 3 | |
| 4 | 4 | import { createExecutor, definePlugin, makeTestConfig } from "@executor-js/sdk"; |
| 5 | 5 | |
| 6 | 6 | import { buildExecuteDescription } from "./description"; |
| 7 | 7 | |
| 8 | −const EmptyInputSchema = { | |
| 9 | − type: "object", | |
| 10 | − properties: {}, | |
| 11 | − additionalProperties: false, | |
| 12 | −} as const; | |
| 8 | +const EmptyInputSchema = Schema.toStandardSchemaV1( | |
| 9 | + Schema.toStandardJSONSchemaV1(Schema.Struct({})), | |
| 10 | +); | |
| 13 | 11 | |
| 14 | 12 | // Two plugins registering static sources whose ids are distinct from their |
| 15 | 13 | // pluginIds/names. If `buildExecuteDescription` ever renders the wrong field |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | −import { Effect, Fiber } from "effect"; | |
| 2 | +import { Effect, Fiber, Schema } from "effect"; | |
| 3 | 3 | |
| 4 | 4 | import { |
| 5 | 5 | ElicitationResponse, |
| @@ -14,30 +14,17 @@ import { describeTool, makeExecutorToolInvoker, searchTools } from "./tool-invok · 8 unmodified lines | ||
| 14 | 14 | |
| 15 | 15 | const codeExecutor = makeQuickJsExecutor(); |
| 16 | 16 | |
| 17 | −const RepoInputSchema = { | |
| 18 | − type: "object", | |
| 19 | − properties: { | |
| 20 | − owner: { type: "string" }, | |
| 21 | − repo: { type: "string" }, | |
| 22 | − }, | |
| 23 | − required: ["owner", "repo"], | |
| 24 | − additionalProperties: false, | |
| 25 | −} as const; | |
| 26 | − | |
| 27 | −const ContactInputSchema = { | |
| 28 | − type: "object", | |
| 29 | − properties: { | |
| 30 | − email: { type: "string" }, | |
| 31 | − }, | |
| 32 | − required: ["email"], | |
| 33 | − additionalProperties: false, | |
| 34 | −} as const; | |
| 35 | − | |
| 36 | −const EmptyInputSchema = { | |
| 37 | − type: "object", | |
| 38 | − properties: {}, | |
| 39 | − additionalProperties: false, | |
| 40 | −} as const; | |
| 17 | +const RepoInputSchema = Schema.toStandardSchemaV1( | |
| 18 | + Schema.toStandardJSONSchemaV1(Schema.Struct({ owner: Schema.String, repo: Schema.String })), | |
| 19 | +); | |
| 20 | + | |
| 21 | +const ContactInputSchema = Schema.toStandardSchemaV1( | |
| 22 | + Schema.toStandardJSONSchemaV1(Schema.Struct({ email: Schema.String })), | |
| 23 | +); | |
| 24 | + | |
| 25 | +const EmptyInputSchema = Schema.toStandardSchemaV1( | |
| 26 | + Schema.toStandardJSONSchemaV1(Schema.Struct({})), | |
| 27 | +); | |
| 41 | 28 | |
| 42 | 29 | const acceptAll = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); |
| 43 | 30 | |
| @@ -59,6 +59,7 @@ · 58 unmodified lines | ||
| 59 | 59 | }, |
| 60 | 60 | "dependencies": { |
| 61 | 61 | "@executor-js/storage-core": "workspace:*", |
| 62 | + "@standard-schema/spec": "^1.1.0", | |
| 62 | 63 | "effect": "catalog:", |
| 63 | 64 | "fractional-indexing": "^3.2.0", |
| 64 | 65 | "oauth4webapi": "^3.8.5" |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | −import { Data, Effect, Exit, Predicate, Result } from "effect"; | |
| 2 | +import { Data, Effect, Exit, Predicate, Result, Schema } from "effect"; | |
| 3 | 3 | import { FetchHttpClient } from "effect/unstable/http"; |
| 4 | 4 | |
| 5 | 5 | import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory"; |
| @@ -9,7 +9,7 @@ import { makeInMemoryBlobStore } from "./blob"; · 3 unmodified lines | ||
| 9 | 9 | import { CreateConnectionInput, TokenMaterial } from "./connections"; |
| 10 | 10 | import { collectSchemas, createExecutor } from "./executor"; |
| 11 | 11 | import { ElicitationResponse, FormElicitation, UrlElicitation } from "./elicitation"; |
| 12 | −import { defineSchema, definePlugin } from "./plugin"; | |
| 12 | +import { defineSchema, definePlugin, tool } from "./plugin"; | |
| 13 | 13 | import { RemoveSecretInput, SetSecretInput } from "./secrets"; |
| 14 | 14 | import { makeTestConfig } from "./testing"; |
| 15 | 15 | import type { SecretProvider } from "./secrets"; |
| @@ -45,7 +45,7 @@ const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => · 29 unmodified lines | ||
| 45 | 45 | }); |
| 46 | 46 | |
| 47 | 47 | // --------------------------------------------------------------------------- |
| 48 | −// Tiny test plugin — declares a static source with two control tools, a | |
| 48 | +// Tiny test plugin — declares a static source with two built-in tools, a | |
| 49 | 49 | // plugin schema for a per-row key/value table, and a dynamic invokeTool |
| 50 | 50 | // handler. Exercises everything createExecutor has to wire up. |
| 51 | 51 | // --------------------------------------------------------------------------- |
| @@ -837,21 +837,19 @@ describe("createExecutor", () => { · 785 unmodified lines | ||
| 837 | 837 | kind: "control", |
| 838 | 838 | name: "Preview Ctl", |
| 839 | 839 | tools: [ |
| 840 | − { | |
| 840 | + tool({ | |
| 841 | 841 | name: "createContact", |
| 842 | 842 | description: "create", |
| 843 | − inputSchema: { | |
| 844 | − type: "object", | |
| 845 | − properties: { | |
| 846 | − name: { type: "string" }, | |
| 847 | − age: { type: "number" }, | |
| 848 | − }, | |
| 849 | − required: ["name", "age"], | |
| 850 | − additionalProperties: false, | |
| 851 | − }, | |
| 852 | − outputSchema: { type: "string" }, | |
| 853 | − handler: ({ args }) => Effect.succeed(args), | |
| 854 | − }, | |
| 843 | + inputSchema: Schema.toStandardSchemaV1( | |
| 844 | + Schema.toStandardJSONSchemaV1( | |
| 845 | + Schema.Struct({ name: Schema.String, age: Schema.Finite }), | |
| 846 | + ), | |
| 847 | + ), | |
| 848 | + outputSchema: Schema.toStandardSchemaV1( | |
| 849 | + Schema.toStandardJSONSchemaV1(Schema.String), | |
| 850 | + ), | |
| 851 | + execute: (input) => Effect.succeed(input), | |
| 852 | + }), | |
| 855 | 853 | ], |
| 856 | 854 | }, |
| 857 | 855 | ], |
| @@ -1147,12 +1145,12 @@ const tenantPlugin = definePlugin(() => ({ · 289 unmodified lines | ||
| 1147 | 1145 | kind: "control" as const, |
| 1148 | 1146 | name: "Tenant Ctl", |
| 1149 | 1147 | tools: [ |
| 1150 | − { | |
| 1148 | + tool({ | |
| 1151 | 1149 | name: "noop", |
| 1152 | 1150 | description: "noop", |
| 1153 | − inputSchema: { type: "object", additionalProperties: false }, | |
| 1154 | − handler: () => Effect.succeed(null), | |
| 1155 | − }, | |
| 1151 | + inputSchema: Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(Schema.Struct({}))), | |
| 1152 | + execute: () => Effect.succeed(null), | |
| 1153 | + }), | |
| 1156 | 1154 | ], |
| 1157 | 1155 | }, |
| 1158 | 1156 | ], |
| @@ -103,6 +103,7 @@ import type { · 102 unmodified lines | ||
| 103 | 103 | PluginExtensions, |
| 104 | 104 | StaticSourceDecl, |
| 105 | 105 | StaticToolDecl, |
| 106 | + StaticToolSchema, | |
| 106 | 107 | StorageDeps, |
| 107 | 108 | } from "./plugin"; |
| 108 | 109 | import type { Scope } from "./scope"; |
| @@ -457,11 +458,32 @@ const staticDeclToTool = ( · 348 unmodified lines | ||
| 457 | 458 | pluginId, |
| 458 | 459 | name: tool.name, |
| 459 | 460 | description: tool.description, |
| 460 | − inputSchema: tool.inputSchema, | |
| 461 | − outputSchema: tool.outputSchema, | |
| 461 | + inputSchema: toToolJsonSchema(tool.inputSchema), | |
| 462 | + outputSchema: toToolJsonSchema(tool.outputSchema, "output"), | |
| 462 | 463 | annotations: tool.annotations, |
| 463 | 464 | }); |
| 464 | 465 | |
| 466 | +const toToolJsonSchema = ( | |
| 467 | + schema: StaticToolSchema | undefined, | |
| 468 | + direction: "input" | "output" = "input", | |
| 469 | +): unknown => { | |
| 470 | + if (schema == null) return undefined; | |
| 471 | + return schema["~standard"].jsonSchema[direction]({ | |
| 472 | + target: "draft-2020-12", | |
| 473 | + }); | |
| 474 | +}; | |
| 475 | + | |
| 476 | +const EXECUTOR_SOURCE_ID = "executor"; | |
| 477 | +const EXECUTOR_SOURCE: StaticSourceDecl = { | |
| 478 | + id: EXECUTOR_SOURCE_ID, | |
| 479 | + kind: "built-in", | |
| 480 | + name: "Executor", | |
| 481 | + canRemove: false, | |
| 482 | + canRefresh: false, | |
| 483 | + canEdit: false, | |
| 484 | + tools: [], | |
| 485 | +}; | |
| 486 | + | |
| 465 | 487 | // --------------------------------------------------------------------------- |
| 466 | 488 | // Dynamic-row writers — used by ctx.core.sources.register. Static sources |
| 467 | 489 | // never touch these functions. |
| @@ -2719,18 +2741,40 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = []> · 2251 unmodified lines | ||
| 2719 | 2741 | } |
| 2720 | 2742 | |
| 2721 | 2743 | // Resolve static declarations to the in-memory pools. NO DB WRITES. |
| 2744 | + // Plugin-owned executor tools are intentionally mounted under the | |
| 2745 | + // single `executor` namespace so source inventory is about configured | |
| 2746 | + // integrations, not plugin management surfaces: | |
| 2747 | + // openapi.addSource -> executor.openapi.addSource | |
| 2722 | 2748 | const decls = plugin.staticSources ? plugin.staticSources(extension) : []; |
| 2723 | 2749 | for (const source of decls) { |
| 2724 | − if (staticSources.has(source.id)) { | |
| 2725 | − return yield* new StorageError({ | |
| 2726 | − message: `Duplicate static source id: ${source.id} (plugin ${plugin.id})`, | |
| 2727 | − cause: undefined, | |
| 2728 | − }); | |
| 2750 | + const mountUnderExecutor = source.kind === "executor" && source.id === plugin.id; | |
| 2751 | + const mountedSource = mountUnderExecutor ? EXECUTOR_SOURCE : source; | |
| 2752 | + | |
| 2753 | + if (mountUnderExecutor) { | |
| 2754 | + if (!staticSources.has(EXECUTOR_SOURCE_ID)) { | |
| 2755 | + staticSources.set(EXECUTOR_SOURCE_ID, { | |
| 2756 | + source: EXECUTOR_SOURCE, | |
| 2757 | + pluginId: EXECUTOR_SOURCE_ID, | |
| 2758 | + }); | |
| 2759 | + } | |
| 2760 | + } else { | |
| 2761 | + if (staticSources.has(source.id)) { | |
| 2762 | + return yield* new StorageError({ | |
| 2763 | + message: `Duplicate static source id: ${source.id} (plugin ${plugin.id})`, | |
| 2764 | + cause: undefined, | |
| 2765 | + }); | |
| 2766 | + } | |
| 2767 | + staticSources.set(source.id, { source, pluginId: plugin.id }); | |
| 2729 | 2768 | } |
| 2730 | − staticSources.set(source.id, { source, pluginId: plugin.id }); | |
| 2731 | 2769 | |
| 2732 | 2770 | for (const tool of source.tools) { |
| 2733 | − const fqid = `${source.id}.${tool.name}`; | |
| 2771 | + const mountedTool = mountUnderExecutor | |
| 2772 | + ? { | |
| 2773 | + ...tool, | |
| 2774 | + name: `${plugin.id}.${tool.name}`, | |
| 2775 | + } | |
| 2776 | + : tool; | |
| 2777 | + const fqid = `${mountedSource.id}.${mountedTool.name}`; | |
| 2734 | 2778 | if (staticTools.has(fqid)) { |
| 2735 | 2779 | return yield* new StorageError({ |
| 2736 | 2780 | message: `Duplicate static tool id: ${fqid} (plugin ${plugin.id})`, |
| @@ -2738,8 +2782,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = []> · 1 unmodified line | ||
| 2738 | 2782 | }); |
| 2739 | 2783 | } |
| 2740 | 2784 | staticTools.set(fqid, { |
| 2741 | − source, | |
| 2742 | − tool, | |
| 2785 | + source: mountedSource, | |
| 2786 | + tool: mountedTool, | |
| 2743 | 2787 | pluginId: plugin.id, |
| 2744 | 2788 | ctx, |
| 2745 | 2789 | }); |
| @@ -3028,8 +3072,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = []> · 282 unmodified lines | ||
| 3028 | 3072 | name: staticEntry.tool.name, |
| 3029 | 3073 | description: staticEntry.tool.description, |
| 3030 | 3074 | sourceId: undefined, |
| 3031 | − rawInput: staticEntry.tool.inputSchema, | |
| 3032 | − rawOutput: staticEntry.tool.outputSchema, | |
| 3075 | + rawInput: toToolJsonSchema(staticEntry.tool.inputSchema), | |
| 3076 | + rawOutput: toToolJsonSchema(staticEntry.tool.outputSchema, "output"), | |
| 3033 | 3077 | }); |
| 3034 | 3078 | } |
| 3035 | 3079 | // Innermost-wins lookup: the scope-wrapped adapter returns rows |
| @@ -280,13 +280,16 @@ export { · 279 unmodified lines | ||
| 280 | 280 | type StorageDeps, |
| 281 | 281 | type StaticSourceDecl, |
| 282 | 282 | type StaticToolDecl, |
| 283 | + type StaticToolExecuteContext, | |
| 283 | 284 | type StaticToolHandlerInput, |
| 285 | + type StaticToolInput, | |
| 284 | 286 | type InvokeToolInput, |
| 285 | 287 | type SourceLifecycleInput, |
| 286 | 288 | type SecretListEntry, |
| 287 | 289 | type Elicit, |
| 288 | 290 | definePlugin, |
| 289 | 291 | defineSchema, |
| 292 | + tool, | |
| 290 | 293 | } from "./plugin"; |
| 291 | 294 | |
| 292 | 295 | // Executor |
| @@ -1,6 +1,8 @@ | ||
| 1 | −import type { Context, Effect, Layer } from "effect"; | |
| 1 | +import { Effect } from "effect"; | |
| 2 | +import type { Context, Layer } from "effect"; | |
| 2 | 3 | import type { HttpClient } from "effect/unstable/http"; |
| 3 | 4 | import type { HttpApiGroup } from "effect/unstable/httpapi"; |
| 5 | +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"; | |
| 4 | 6 | import type { DBSchema, StorageFailure } from "@executor-js/storage-core"; |
| 5 | 7 | |
| 6 | 8 | import type { PluginBlobStore } from "./blob"; |
| @@ -249,11 +251,21 @@ export interface StaticToolHandlerInput<TStore = unknown> { · 242 unmodified lines | ||
| 249 | 251 | readonly elicit: Elicit; |
| 250 | 252 | } |
| 251 | 253 | |
| 254 | +export interface StaticToolExecuteContext<TStore = unknown> { | |
| 255 | + readonly ctx: PluginCtx<TStore>; | |
| 256 | + /** Suspend the fiber to request user input. The handler passed to | |
| 257 | + * `createExecutor({ onElicitation })` is called. */ | |
| 258 | + readonly elicit: Elicit; | |
| 259 | +} | |
| 260 | + | |
| 261 | +export type StaticToolSchema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & | |
| 262 | + StandardJSONSchemaV1<Input, Output>; | |
| 263 | + | |
| 252 | 264 | export interface StaticToolDecl<TStore = unknown> { |
| 253 | 265 | readonly name: string; |
| 254 | 266 | readonly description: string; |
| 255 | − readonly inputSchema?: unknown; | |
| 256 | − readonly outputSchema?: unknown; | |
| 267 | + readonly inputSchema?: StaticToolSchema; | |
| 268 | + readonly outputSchema?: StaticToolSchema; | |
| 257 | 269 | /** Default-policy annotations — `requiresApproval`, `approvalDescription`, |
| 258 | 270 | * `mayElicit`. Enforced by the executor before the handler runs. |
| 259 | 271 | * Inline because static tools have no plugin storage to resolve from; |
| @@ -262,13 +274,68 @@ export interface StaticToolDecl<TStore = unknown> { · 2 unmodified lines | ||
| 262 | 274 | readonly handler: (input: StaticToolHandlerInput<TStore>) => Effect.Effect<unknown, unknown>; |
| 263 | 275 | } |
| 264 | 276 | |
| 277 | +const decodeStaticToolArgs = ( | |
| 278 | + schema: StaticToolSchema | undefined, | |
| 279 | + args: unknown, | |
| 280 | +): Effect.Effect<unknown, unknown> => { | |
| 281 | + if (schema == null) return Effect.succeed(args); | |
| 282 | + return Effect.promise(() => Promise.resolve(schema["~standard"].validate(args))).pipe( | |
| 283 | + Effect.flatMap((result) => | |
| 284 | + "value" in result ? Effect.succeed(result.value) : Effect.fail(result), | |
| 285 | + ), | |
| 286 | + ); | |
| 287 | +}; | |
| 288 | + | |
| 289 | +export interface StaticToolInput< | |
| 290 | + TStore = unknown, | |
| 291 | + TInputSchema extends StaticToolSchema | undefined = StaticToolSchema | undefined, | |
| 292 | +> { | |
| 293 | + readonly name: string; | |
| 294 | + readonly description: string; | |
| 295 | + readonly inputSchema?: TInputSchema; | |
| 296 | + readonly outputSchema?: StaticToolSchema; | |
| 297 | + /** Default-policy annotations — `requiresApproval`, `approvalDescription`, | |
| 298 | + * `mayElicit`. Enforced by the executor before the handler runs. */ | |
| 299 | + readonly annotations?: ToolAnnotations; | |
| 300 | + readonly execute: ( | |
| 301 | + args: TInputSchema extends StaticToolSchema | |
| 302 | + ? StandardSchemaV1.InferOutput<TInputSchema> | |
| 303 | + : unknown, | |
| 304 | + context: StaticToolExecuteContext<TStore>, | |
| 305 | + ) => Effect.Effect<unknown, unknown>; | |
| 306 | +} | |
| 307 | + | |
| 308 | +export const tool = < | |
| 309 | + TStore = unknown, | |
| 310 | + TInputSchema extends StaticToolSchema | undefined = StaticToolSchema | undefined, | |
| 311 | +>( | |
| 312 | + input: StaticToolInput<TStore, TInputSchema>, | |
| 313 | +): StaticToolDecl<TStore> => ({ | |
| 314 | + name: input.name, | |
| 315 | + description: input.description, | |
| 316 | + inputSchema: input.inputSchema, | |
| 317 | + outputSchema: input.outputSchema, | |
| 318 | + annotations: input.annotations, | |
| 319 | + handler: ({ args, ctx, elicit }) => | |
| 320 | + decodeStaticToolArgs(input.inputSchema, args).pipe( | |
| 321 | + Effect.flatMap((decoded) => | |
| 322 | + input.execute( | |
| 323 | + decoded as TInputSchema extends StaticToolSchema | |
| 324 | + ? StandardSchemaV1.InferOutput<TInputSchema> | |
| 325 | + : unknown, | |
| 326 | + { ctx, elicit }, | |
| 327 | + ), | |
| 328 | + ), | |
| 329 | + ), | |
| 330 | +}); | |
| 331 | + | |
| 265 | 332 | export interface StaticSourceDecl<TStore = unknown> { |
| 266 | 333 | readonly id: string; |
| 267 | 334 | readonly kind: string; |
| 268 | 335 | readonly name: string; |
| 269 | 336 | readonly url?: string; |
| 270 | − /** Static sources default to `canRemove: false` — they represent | |
| 271 | − * plugin-provided control surfaces and shouldn't be user-removable. | |
| 337 | + /** Static sources default to `canRemove: false` because they are | |
| 338 | + * plugin-provided surfaces and shouldn't usually be user-removable. | |
| 272 | 339 | * Override only if you really want that. */ |
| 273 | 340 | readonly canRemove?: boolean; |
| 274 | 341 | readonly canRefresh?: boolean; |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | 3 | import { createExecutor } from "./promise"; |
| 4 | −import { definePlugin, defineSchema } from "./plugin"; | |
| 5 | −import { Effect } from "effect"; | |
| 4 | +import { definePlugin, defineSchema, tool } from "./plugin"; | |
| 5 | +import { Effect, Schema } from "effect"; | |
| 6 | 6 | |
| 7 | 7 | // A minimal static-tool plugin built on the Effect surface, consumed |
| 8 | 8 | // through the Promise façade. Exercises the proxy's ability to promisify |
| @@ -17,18 +17,14 @@ const echoPlugin = definePlugin(() => ({ · 8 unmodified lines | ||
| 17 | 17 | kind: "control" as const, |
| 18 | 18 | name: "Echo Ctl", |
| 19 | 19 | tools: [ |
| 20 | − { | |
| 20 | + tool({ | |
| 21 | 21 | name: "say", |
| 22 | 22 | description: "Echo the input", |
| 23 | − inputSchema: { | |
| 24 | − type: "object", | |
| 25 | − properties: { message: { type: "string" } }, | |
| 26 | − required: ["message"], | |
| 27 | − additionalProperties: false, | |
| 28 | − }, | |
| 29 | − handler: ({ args }: { args: unknown }) => | |
| 30 | − Effect.succeed((args as { message: string }).message), | |
| 31 | − }, | |
| 23 | + inputSchema: Schema.toStandardSchemaV1( | |
| 24 | + Schema.toStandardJSONSchemaV1(Schema.Struct({ message: Schema.String })), | |
| 25 | + ), | |
| 26 | + execute: (input) => Effect.succeed(input.message), | |
| 27 | + }), | |
| 32 | 28 | ], |
| 33 | 29 | }, |
| 34 | 30 | ], |
| @@ -80,13 +76,15 @@ describe("promise/createExecutor", () => { · 45 unmodified lines | ||
| 80 | 76 | kind: "control" as const, |
| 81 | 77 | name: "Ap Ctl", |
| 82 | 78 | tools: [ |
| 83 | − { | |
| 79 | + tool({ | |
| 84 | 80 | name: "go", |
| 85 | 81 | description: "Requires approval", |
| 86 | 82 | annotations: { requiresApproval: true } as const, |
| 87 | − inputSchema: { type: "object", additionalProperties: false }, | |
| 88 | − handler: () => Effect.succeed("ran"), | |
| 89 | − }, | |
| 83 | + inputSchema: Schema.toStandardSchemaV1( | |
| 84 | + Schema.toStandardJSONSchemaV1(Schema.Struct({})), | |
| 85 | + ), | |
| 86 | + execute: () => Effect.succeed("ran"), | |
| 87 | + }), | |
| 90 | 88 | ], |
| 91 | 89 | }, |
| 92 | 90 | ], |
| @@ -283,8 +283,8 @@ describe("graphqlPlugin", () => { · 282 unmodified lines | ||
| 283 | 283 | const ids = tools.map((t) => t.id); |
| 284 | 284 | expect(ids).toContain("test_api.query.hello"); |
| 285 | 285 | expect(ids).toContain("test_api.mutation.setGreeting"); |
| 286 | − // static control tool also present | |
| 287 | − expect(ids).toContain("graphql.addSource"); | |
| 286 | + // static executor tool also present under the executor namespace | |
| 287 | + expect(ids).toContain("executor.graphql.addSource"); | |
| 288 | 288 | |
| 289 | 289 | const queryTool = tools.find((t) => t.id === "test_api.query.hello"); |
| 290 | 290 | expect(queryTool?.description).toBe("Say hello"); |
| @@ -320,7 +320,7 @@ describe("graphqlPlugin", () => { · 29 unmodified lines | ||
| 320 | 320 | }), |
| 321 | 321 | ); |
| 322 | 322 | |
| 323 | − it.effect("lists sources with the static control source", () => | |
| 323 | + it.effect("lists sources with the executor built-in source", () => | |
| 324 | 324 | Effect.gen(function* () { |
| 325 | 325 | const executor = yield* createExecutor( |
| 326 | 326 | makeTestConfig({ plugins: [graphqlPlugin()] as const }), |
| @@ -341,7 +341,8 @@ describe("graphqlPlugin", () => { · 14 unmodified lines | ||
| 341 | 341 | expect(dynamic!.canEdit).toBe(true); |
| 342 | 342 | expect(dynamic!.runtime).toBe(false); |
| 343 | 343 | |
| 344 | − const control = sources.find((s) => s.id === "graphql"); | |
| 344 | + expect(sources.find((s) => s.id === "graphql")).toBeUndefined(); | |
| 345 | + const control = sources.find((s) => s.id === "executor"); | |
| 345 | 346 | expect(control).toBeDefined(); |
| 346 | 347 | expect(control!.runtime).toBe(true); |
| 347 | 348 | }), |
| @@ -401,7 +402,7 @@ describe("graphqlPlugin", () => { · 53 unmodified lines | ||
| 401 | 402 | }), |
| 402 | 403 | ); |
| 403 | 404 | |
| 404 | − it.effect("static graphql.addSource delegates to extension", () => | |
| 405 | + it.effect("static executor.graphql.addSource delegates to extension", () => | |
| 405 | 406 | Effect.gen(function* () { |
| 406 | 407 | const userScope = ScopeId.make("static-user"); |
| 407 | 408 | const orgScope = ScopeId.make("static-org"); |
| @@ -416,7 +417,7 @@ describe("graphqlPlugin", () => { · 8 unmodified lines | ||
| 416 | 417 | ); |
| 417 | 418 | |
| 418 | 419 | const result = yield* executor.tools.invoke( |
| 419 | − "graphql.addSource", | |
| 420 | + "executor.graphql.addSource", | |
| 420 | 421 | { |
| 421 | 422 | scope: String(orgScope), |
| 422 | 423 | endpoint: "http://localhost:4000/graphql", |
| @@ -436,6 +437,22 @@ describe("graphqlPlugin", () => { · 13 unmodified lines | ||
| 436 | 437 | }), |
| 437 | 438 | ); |
| 438 | 439 | |
| 440 | + it.effect("describes static addSource parameters from Standard Schema", () => | |
| 441 | + Effect.gen(function* () { | |
| 442 | + const executor = yield* createExecutor(makeTestConfig({ plugins: [graphqlPlugin()] })); | |
| 443 | + | |
| 444 | + const schema = yield* executor.tools.schema("executor.graphql.addSource"); | |
| 445 | + | |
| 446 | + expect(schema).not.toBeNull(); | |
| 447 | + expect(schema!.inputTypeScript).toContain("scope: string"); | |
| 448 | + expect(schema!.inputTypeScript).toContain("endpoint: string"); | |
| 449 | + expect( | |
| 450 | + (schema!.inputSchema as { properties?: Record<string, unknown> }).properties, | |
| 451 | + ).toHaveProperty("credentialTargetScope"); | |
| 452 | + expect(schema!.inputTypeScript).not.toBe("Record<string, unknown>"); | |
| 453 | + }), | |
| 454 | + ); | |
| 455 | + | |
| 439 | 456 | it.effect("requires approval before a runtime-added query sends prior tool output", () => |
| 440 | 457 | Effect.gen(function* () { |
| 441 | 458 | const server = yield* serveGreetingServer; |
| @@ -451,7 +468,7 @@ describe("graphqlPlugin", () => { · 9 unmodified lines | ||
| 451 | 468 | expect(trusted).toBe("sample-value"); |
| 452 | 469 | const declined = yield* executor.tools |
| 453 | 470 | .invoke( |
| 454 | − "graphql.addSource", | |
| 471 | + "executor.graphql.addSource", | |
| 455 | 472 | { |
| 456 | 473 | endpoint: server.endpoint, |
| 457 | 474 | scope: TEST_SCOPE, |
| @@ -478,7 +495,7 @@ describe("graphqlPlugin", () => { · 20 unmodified lines | ||
| 478 | 495 | ); |
| 479 | 496 | |
| 480 | 497 | yield* executor.tools.invoke( |
| 481 | − "graphql.addSource", | |
| 498 | + "executor.graphql.addSource", | |
| 482 | 499 | { |
| 483 | 500 | endpoint: server.endpoint, |
| 484 | 501 | scope: TEST_SCOPE, |
| @@ -7,6 +7,7 @@ import { · 6 unmodified lines | ||
| 7 | 7 | ConfiguredCredentialBinding, |
| 8 | 8 | type CredentialBindingRef, |
| 9 | 9 | definePlugin, |
| 10 | + tool, | |
| 10 | 11 | ScopeId, |
| 11 | 12 | SecretId, |
| 12 | 13 | SourceDetectionResult, |
| @@ -108,6 +109,13 @@ const StaticAddSourceInputSchema = Schema.Struct({ · 95 unmodified lines | ||
| 108 | 109 | auth: Schema.optional(GraphqlSourceAuthInputSchema), |
| 109 | 110 | }); |
| 110 | 111 | |
| 112 | +const StaticAddSourceInputStandardSchema = Schema.toStandardSchemaV1( | |
| 113 | + Schema.toStandardJSONSchemaV1(StaticAddSourceInputSchema), | |
| 114 | +); | |
| 115 | +const StaticAddSourceOutputStandardSchema = Schema.toStandardSchemaV1( | |
| 116 | + Schema.toStandardJSONSchemaV1(Schema.Struct({ toolCount: Schema.Number })), | |
| 117 | +); | |
| 118 | + | |
| 111 | 119 | // --------------------------------------------------------------------------- |
| 112 | 120 | // Plugin extension |
| 113 | 121 | // --------------------------------------------------------------------------- |
| @@ -974,44 +982,20 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 860 unmodified lines | ||
| 974 | 982 | staticSources: (self) => [ |
| 975 | 983 | { |
| 976 | 984 | id: "graphql", |
| 977 | − kind: "control", | |
| 985 | + kind: "executor", | |
| 978 | 986 | name: "GraphQL", |
| 979 | 987 | tools: [ |
| 980 | − { | |
| 988 | + tool({ | |
| 981 | 989 | name: "addSource", |
| 982 | 990 | description: "Add a GraphQL endpoint and register its operations as tools", |
| 983 | 991 | annotations: { |
| 984 | 992 | requiresApproval: true, |
| 985 | 993 | approvalDescription: "Add a GraphQL source", |
| 986 | 994 | }, |
| 987 | − inputSchema: { | |
| 988 | − type: "object", | |
| 989 | − properties: { | |
| 990 | − scope: { type: "string" }, | |
| 991 | − endpoint: { type: "string" }, | |
| 992 | − name: { type: "string" }, | |
| 993 | − introspectionJson: { type: "string" }, | |
| 994 | − namespace: { type: "string" }, | |
| 995 | − headers: { type: "object" }, | |
| 996 | − queryParams: { type: "object" }, | |
| 997 | − credentialTargetScope: { type: "string" }, | |
| 998 | − auth: { type: "object" }, | |
| 999 | − }, | |
| 1000 | − required: ["scope", "endpoint"], | |
| 1001 | − }, | |
| 1002 | − outputSchema: { | |
| 1003 | − type: "object", | |
| 1004 | − properties: { | |
| 1005 | − toolCount: { type: "number" }, | |
| 1006 | − }, | |
| 1007 | − required: ["toolCount"], | |
| 1008 | − }, | |
| 1009 | − handler: ({ args }) => | |
| 1010 | − Effect.gen(function* () { | |
| 1011 | − const input = yield* Schema.decodeUnknownEffect(StaticAddSourceInputSchema)(args); | |
| 1012 | − return yield* self.addSource(input); | |
| 1013 | − }), | |
| 1014 | − }, | |
| 995 | + inputSchema: StaticAddSourceInputStandardSchema, | |
| 996 | + outputSchema: StaticAddSourceOutputStandardSchema, | |
| 997 | + execute: (input) => self.addSource(input), | |
| 998 | + }), | |
| 1015 | 999 | ], |
| 1016 | 1000 | }, |
| 1017 | 1001 | ], |
| @@ -212,7 +212,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 211 unmodified lines | ||
| 212 | 212 | }), |
| 213 | 213 | ); |
| 214 | 214 | |
| 215 | − it.effect("registers static openapi control tools", () => | |
| 215 | + it.effect("registers static openapi executor tools", () => | |
| 216 | 216 | Effect.gen(function* () { |
| 217 | 217 | const httpClient = yield* HttpClient.HttpClient; |
| 218 | 218 | const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); |
| @@ -228,12 +228,12 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 9 unmodified lines | ||
| 228 | 228 | |
| 229 | 229 | const tools = yield* executor.tools.list(); |
| 230 | 230 | const ids = tools.map((t) => t.id); |
| 231 | − expect(ids).toContain("openapi.previewSpec"); | |
| 232 | − expect(ids).toContain("openapi.addSource"); | |
| 231 | + expect(ids).toContain("executor.openapi.previewSpec"); | |
| 232 | + expect(ids).toContain("executor.openapi.addSource"); | |
| 233 | 233 | }), |
| 234 | 234 | ); |
| 235 | 235 | |
| 236 | − it.effect("lists openapi as a static runtime source", () => | |
| 236 | + it.effect("lists executor as the static runtime source", () => | |
| 237 | 237 | Effect.gen(function* () { |
| 238 | 238 | const httpClient = yield* HttpClient.HttpClient; |
| 239 | 239 | const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); |
| @@ -248,7 +248,8 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 8 unmodified lines | ||
| 248 | 248 | ); |
| 249 | 249 | |
| 250 | 250 | const sources = yield* executor.sources.list(); |
| 251 | − const control = sources.find((s) => s.id === "openapi"); | |
| 251 | + expect(sources.find((s) => s.id === "openapi")).toBeUndefined(); | |
| 252 | + const control = sources.find((s) => s.id === "executor"); | |
| 252 | 253 | expect(control).toBeDefined(); |
| 253 | 254 | expect(control!.runtime).toBe(true); |
| 254 | 255 | expect(control!.canRemove).toBe(false); |
| @@ -270,7 +271,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 15 unmodified lines | ||
| 270 | 271 | ); |
| 271 | 272 | |
| 272 | 273 | const result = (yield* executor.tools.invoke( |
| 273 | − "openapi.previewSpec", | |
| 274 | + "executor.openapi.previewSpec", | |
| 274 | 275 | { spec: specJson }, |
| 275 | 276 | autoApprove, |
| 276 | 277 | )) as { operationCount: number }; |
| @@ -279,6 +280,26 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 2 unmodified lines | ||
| 279 | 280 | }), |
| 280 | 281 | ); |
| 281 | 282 | |
| 283 | + it.effect("describes static addSource parameters from Standard Schema", () => | |
| 284 | + Effect.gen(function* () { | |
| 285 | + const executor = yield* createExecutor( | |
| 286 | + makeTestConfig({ | |
| 287 | + plugins: [openApiPlugin(), memorySecretsPlugin()] as const, | |
| 288 | + }), | |
| 289 | + ); | |
| 290 | + | |
| 291 | + const schema = yield* executor.tools.schema("executor.openapi.addSource"); | |
| 292 | + | |
| 293 | + expect(schema).not.toBeNull(); | |
| 294 | + expect(schema!.inputTypeScript).toContain("scope: string"); | |
| 295 | + expect(schema!.inputTypeScript).toContain("spec: string"); | |
| 296 | + expect( | |
| 297 | + (schema!.inputSchema as { properties?: Record<string, unknown> }).properties, | |
| 298 | + ).toHaveProperty("credentialTargetScope"); | |
| 299 | + expect(schema!.inputTypeScript).not.toBe("Record<string, unknown>"); | |
| 300 | + }), | |
| 301 | + ); | |
| 302 | + | |
| 282 | 303 | it.effect("invokes static addSource through executor.tools.invoke", () => |
| 283 | 304 | Effect.gen(function* () { |
| 284 | 305 | const httpClient = yield* HttpClient.HttpClient; |
| @@ -300,7 +321,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 15 unmodified lines | ||
| 300 | 321 | ); |
| 301 | 322 | |
| 302 | 323 | const result = (yield* executor.tools.invoke( |
| 303 | − "openapi.addSource", | |
| 324 | + "executor.openapi.addSource", | |
| 304 | 325 | { scope: String(orgScope), spec: specJson, namespace: "runtime" }, |
| 305 | 326 | autoApprove, |
| 306 | 327 | )) as { sourceId: string; toolCount: number }; |
| @@ -322,7 +343,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 15 unmodified lines | ||
| 322 | 343 | |
| 323 | 344 | const declined = yield* executor.tools |
| 324 | 345 | .invoke( |
| 325 | − "openapi.addSource", | |
| 346 | + "executor.openapi.addSource", | |
| 326 | 347 | { scope: TEST_SCOPE, spec: specJson, namespace: "runtime_declined" }, |
| 327 | 348 | { onElicitation: () => Effect.succeed({ action: "decline" as const }) }, |
| 328 | 349 | ) |
| @@ -825,7 +846,7 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 496 unmodified lines | ||
| 825 | 846 | |
| 826 | 847 | const remaining = yield* executor.tools.list(); |
| 827 | 848 | const ids = remaining.map((t) => t.id).sort(); |
| 828 | − expect(ids).toEqual(["openapi.addSource", "openapi.previewSpec"]); | |
| 849 | + expect(ids).toEqual(["executor.openapi.addSource", "executor.openapi.previewSpec"]); | |
| 829 | 850 | }), |
| 830 | 851 | ); |
| 831 | 852 | |
| @@ -8,6 +8,7 @@ import { · 7 unmodified lines | ||
| 8 | 8 | SourceDetectionResult, |
| 9 | 9 | StorageError, |
| 10 | 10 | definePlugin, |
| 11 | + tool, | |
| 11 | 12 | resolveSecretBackedMap, |
| 12 | 13 | type CredentialBindingRef, |
| 13 | 14 | type PluginCtx, |
| @@ -169,7 +170,6 @@ const PreviewSpecInputSchema = Schema.Struct({ · 155 unmodified lines | ||
| 169 | 170 | }), |
| 170 | 171 | ), |
| 171 | 172 | }); |
| 172 | −type PreviewSpecInput = typeof PreviewSpecInputSchema.Type; | |
| 173 | 173 | |
| 174 | 174 | const OpenApiHeaderInputSchema = Schema.Union([HeaderValueSchema, ConfiguredHeaderValueSchema]); |
| 175 | 175 | const OpenApiOAuthInputSchema = OAuth2SourceConfig; |
| @@ -191,7 +191,21 @@ const AddSourceInputSchema = Schema.Struct({ · 15 unmodified lines | ||
| 191 | 191 | }), |
| 192 | 192 | ), |
| 193 | 193 | }); |
| 194 | −type AddSourceInput = typeof AddSourceInputSchema.Type; | |
| 194 | + | |
| 195 | +const AddSourceOutputSchema = Schema.Struct({ | |
| 196 | + sourceId: Schema.String, | |
| 197 | + toolCount: Schema.Number, | |
| 198 | +}); | |
| 199 | + | |
| 200 | +const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1( | |
| 201 | + Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema), | |
| 202 | +); | |
| 203 | +const AddSourceInputStandardSchema = Schema.toStandardSchemaV1( | |
| 204 | + Schema.toStandardJSONSchemaV1(AddSourceInputSchema), | |
| 205 | +); | |
| 206 | +const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1( | |
| 207 | + Schema.toStandardJSONSchemaV1(AddSourceOutputSchema), | |
| 208 | +); | |
| 195 | 209 | |
| 196 | 210 | // --------------------------------------------------------------------------- |
| 197 | 211 | // Helpers |
| @@ -1172,60 +1186,26 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 974 unmodified lines | ||
| 1172 | 1186 | staticSources: (self) => [ |
| 1173 | 1187 | { |
| 1174 | 1188 | id: "openapi", |
| 1175 | − kind: "control", | |
| 1189 | + kind: "executor", | |
| 1176 | 1190 | name: "OpenAPI", |
| 1177 | 1191 | tools: [ |
| 1178 | − { | |
| 1192 | + tool({ | |
| 1179 | 1193 | name: "previewSpec", |
| 1180 | 1194 | description: "Preview an OpenAPI document before adding it as a source", |
| 1181 | − inputSchema: { | |
| 1182 | − type: "object", | |
| 1183 | − properties: { | |
| 1184 | − spec: { type: "string" }, | |
| 1185 | − specFetchCredentials: { type: "object" }, | |
| 1186 | − }, | |
| 1187 | − required: ["spec"], | |
| 1188 | − }, | |
| 1189 | − handler: ({ args }) => self.previewSpec(args as PreviewSpecInput), | |
| 1190 | − }, | |
| 1191 | − { | |
| 1195 | + inputSchema: PreviewSpecInputStandardSchema, | |
| 1196 | + execute: (input) => self.previewSpec(input), | |
| 1197 | + }), | |
| 1198 | + tool({ | |
| 1192 | 1199 | name: "addSource", |
| 1193 | 1200 | description: "Add an OpenAPI source and register its operations as tools", |
| 1194 | 1201 | annotations: { |
| 1195 | 1202 | requiresApproval: true, |
| 1196 | 1203 | approvalDescription: "Add an OpenAPI source", |
| 1197 | 1204 | }, |
| 1198 | − inputSchema: { | |
| 1199 | − type: "object", | |
| 1200 | − properties: { | |
| 1201 | − scope: { type: "string" }, | |
| 1202 | − spec: { type: "string" }, | |
| 1203 | − name: { type: "string" }, | |
| 1204 | − baseUrl: { type: "string" }, | |
| 1205 | − namespace: { type: "string" }, | |
| 1206 | − headers: { type: "object" }, | |
| 1207 | − queryParams: { type: "object" }, | |
| 1208 | − oauth2: { type: "object" }, | |
| 1209 | − credentialTargetScope: { type: "string" }, | |
| 1210 | − specFetchCredentials: { type: "object" }, | |
| 1211 | − }, | |
| 1212 | − required: ["scope", "spec"], | |
| 1213 | − }, | |
| 1214 | − outputSchema: { | |
| 1215 | − type: "object", | |
| 1216 | − properties: { | |
| 1217 | − sourceId: { type: "string" }, | |
| 1218 | − toolCount: { type: "number" }, | |
| 1219 | − }, | |
| 1220 | − required: ["sourceId", "toolCount"], | |
| 1221 | − }, | |
| 1222 | − handler: ({ args }) => | |
| 1223 | − Effect.gen(function* () { | |
| 1224 | − const input: AddSourceInput = | |
| 1225 | − yield* Schema.decodeUnknownEffect(AddSourceInputSchema)(args); | |
| 1226 | − return yield* self.addSpec(input); | |
| 1227 | − }), | |
| 1228 | − }, | |
| 1205 | + inputSchema: AddSourceInputStandardSchema, | |
| 1206 | + outputSchema: AddSourceOutputStandardSchema, | |
| 1207 | + execute: (input) => self.addSpec(input), | |
| 1208 | + }), | |
| 1229 | 1209 | ], |
| 1230 | 1210 | }, |
| 1231 | 1211 | ], |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | −import { sourceFaviconUrl } from "./source-favicon"; | |
| 3 | +import { sourceFaviconUrl, sourceLocalIconUrl } from "./source-favicon"; | |
| 4 | 4 | |
| 5 | 5 | describe("SourceFavicon", () => { |
| 6 | 6 | it("uses the favicon service that handles provider-specific icon locations", () => { |
| @@ -19,4 +19,9 @@ describe("SourceFavicon", () => { · 12 unmodified lines | ||
| 19 | 19 | "https://www.google.com/s2/favicons?domain=github.com&sz=40", |
| 20 | 20 | ); |
| 21 | 21 | }); |
| 22 | + | |
| 23 | + it("uses the Executor favicon for the built-in executor source", () => { | |
| 24 | + expect(sourceLocalIconUrl("executor")).toBe("/favicon-32.png"); | |
| 25 | + expect(sourceLocalIconUrl("openapi")).toBeNull(); | |
| 26 | + }); | |
| 22 | 27 | }); |
| @@ -14,9 +14,22 @@ export function sourceFaviconUrl(url: string | undefined, size: number): string · 13 unmodified lines | ||
| 14 | 14 | return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`; |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | −export function SourceFavicon({ url, size = 16 }: { url?: string; size?: number }) { | |
| 17 | +export function sourceLocalIconUrl(sourceId: string | undefined): string | null { | |
| 18 | + if (sourceId !== "executor") return null; | |
| 19 | + return "/favicon-32.png"; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function SourceFavicon({ | |
| 23 | + sourceId, | |
| 24 | + url, | |
| 25 | + size = 16, | |
| 26 | +}: { | |
| 27 | + sourceId?: string; | |
| 28 | + url?: string; | |
| 29 | + size?: number; | |
| 30 | +}) { | |
| 18 | 31 | const [failed, setFailed] = useState(false); |
| 19 | − const src = failed ? null : sourceFaviconUrl(url, size); | |
| 32 | + const src = failed ? null : (sourceLocalIconUrl(sourceId) ?? sourceFaviconUrl(url, size)); | |
| 20 | 33 | |
| 21 | 34 | if (!src) { |
| 22 | 35 | return ( |
| @@ -157,7 +157,9 @@ export function SourceDetailPage(props: { namespace: string }) { · 156 unmodified lines | ||
| 157 | 157 | {sourceData?.runtime && ( |
| 158 | 158 | <Badge className="bg-muted text-muted-foreground">built-in</Badge> |
| 159 | 159 | )} |
| 160 | − <Badge variant="secondary">{sourceData?.kind ?? "source"}</Badge> | |
| 160 | + {(!sourceData?.runtime || sourceData.kind !== "built-in") && ( | |
| 161 | + <Badge variant="secondary">{sourceData?.kind ?? "source"}</Badge> | |
| 162 | + )} | |
| 161 | 163 | {AsyncResult.isSuccess(tools) && !editing && ( |
| 162 | 164 | <span className="hidden text-xs tabular-nums text-muted-foreground sm:block"> |
| 163 | 165 | {sourceTools.length} {sourceTools.length === 1 ? "tool" : "tools"} |
| @@ -411,7 +411,7 @@ function SourceGrid(props: { · 410 unmodified lines | ||
| 411 | 411 | <CardStackEntry key={s.id} asChild searchText={`${s.name} ${s.id} ${s.kind}`}> |
| 412 | 412 | <Link to="/sources/$namespace" params={{ namespace: s.id }}> |
| 413 | 413 | <CardStackEntryMedia> |
| 414 | − <SourceFavicon url={s.url} size={32} /> | |
| 414 | + <SourceFavicon sourceId={s.id} url={s.url} size={32} /> | |
| 415 | 415 | </CardStackEntryMedia> |
| 416 | 416 | <CardStackEntryContent> |
| 417 | 417 | <CardStackEntryTitle>{s.name}</CardStackEntryTitle> |
| @@ -423,8 +423,11 @@ function SourceGrid(props: { · 5 unmodified lines | ||
| 423 | 423 | <SummaryComponent sourceId={s.id} /> |
| 424 | 424 | </Suspense> |
| 425 | 425 | )} |
| 426 | − {s.runtime && <Badge className="bg-muted text-muted-foreground">built-in</Badge>} | |
| 427 | − <Badge variant="secondary">{s.kind}</Badge> | |
| 426 | + {s.runtime ? ( | |
| 427 | + <Badge className="bg-muted text-muted-foreground">built-in</Badge> | |
| 428 | + ) : ( | |
| 429 | + <Badge variant="secondary">{s.kind}</Badge> | |
| 430 | + )} | |
| 428 | 431 | </CardStackEntryActions> |
| 429 | 432 | </Link> |
| 430 | 433 | </CardStackEntry> |