Unified · Split
| @@ -47,6 +47,12 @@ v2 changes the default: · 46 unmodified lines | |||
| 47 | - `cloud` and `self-hosted` are not separate architecture branches. | 47 | - `cloud` and `self-hosted` are not separate architecture branches. |
| 48 | - They are both **remote Convex targets**; only URL/auth/deployment ownership differs. | 48 | - They are both **remote Convex targets**; only URL/auth/deployment ownership differs. |
| 49 | 49 | ||
| 50 | ### Runtime targets (adapter-based) | ||
| 51 | |||
| 52 | - **`deno-subprocess`**: local sandbox runtime (current local secure target in v2). | ||
| 53 | - **`cloudflare-worker-loader`**: production cloud runtime target. | ||
| 54 | - **`node:vm`**: explicit unsafe/dev fallback only, not default. | ||
| 55 | |||
| 50 | --- | 56 | --- |
| 51 | 57 | ||
| 52 | ## 4) Control + execution model | 58 | ## 4) Control + execution model |
| @@ -391,27 +397,36 @@ Implemented in `v2` so far: · 338 unmodified lines | |||
| 391 | - monorepo app/package skeleton | 397 | - monorepo app/package skeleton |
| 392 | - `confect` imported from quickhub | 398 | - `confect` imported from quickhub |
| 393 | - schema package scaffold with Effect models and event envelope | 399 | - schema package scaffold with Effect models and event envelope |
| 400 | - schema-driven runtime tool models (canonical descriptor + OpenAPI invocation/manifest) | ||
| 394 | - domain ID groundwork in `schema/src/ids.ts` | 401 | - domain ID groundwork in `schema/src/ids.ts` |
| 395 | - Convex table ID groundwork in `persistence-convex/src/convex-ids.ts` | 402 | - Convex table ID groundwork in `persistence-convex/src/convex-ids.ts` |
| 396 | - shared persistence ports for `SourceStore` and `ToolArtifactStore` | 403 | - shared persistence ports for `SourceStore` and `ToolArtifactStore` |
| 397 | - local persistence adapters for source/tool artifacts | 404 | - local persistence adapters for source/tool artifacts |
| 398 | - local-only snapshot/WAL contract and persistence implementation | 405 | - local-only snapshot/WAL contract and persistence implementation |
| 399 | - OpenAPI extraction + artifact refresh/reuse flow | 406 | - OpenAPI extraction + artifact refresh/reuse flow |
| 400 | - service-first wiring (`Context.Tag` + Layer) in source manager and local persistence | 407 | - provider registry contracts and routing in `engine` |
| 408 | - OpenAPI provider invocation + in-memory provider path through registry | ||
| 409 | - local in-process JS runner with `tools.*` proxy dispatch | ||
| 410 | - Deno subprocess runtime path with IPC tool-call proxying and tests | ||
| 411 | - runtime adapter contract + registry scaffold in `engine` with mapped adapters (`local-inproc`, `deno-subprocess`, `cloudflare-worker-loader`) | ||
| 412 | - minimal PM execute lifecycle wired through MCP `executor.execute` -> runtime adapter registry (single run, no persisted run state yet) | ||
| 413 | - vertical test: OpenAPI spec -> manifest -> execute code -> HTTP tool call | ||
| 414 | - service-first wiring (`Context.Tag` + Layer) across source manager and persistence/local runtime orchestration | ||
| 401 | 415 | ||
| 402 | Not implemented yet: | 416 | Not implemented yet: |
| 403 | - provider registry and canonical tool descriptor routing in engine/runtime | 417 | - `tools.executor.sources.add/list/remove` end-to-end PM + MCP gateway wiring |
| 404 | - execute lifecycle through PM beyond stub MCP tool | 418 | - approval adapter/state machine integration (pending/resume/deny persistence) in v2 runtime path |
| 405 | - MCP/GraphQL providers and in-memory tool registration/invocation path | 419 | - provider/runtime conformance suite beyond current baseline tests (timeouts, cancellation, pending approval) |
| 420 | - Cloudflare worker loader adapter execution integration (current adapter is explicit scaffold/not-implemented) | ||
| 421 | - MCP/GraphQL provider implementations | ||
| 406 | - full rpc/sdk adapter wiring and sync engine | 422 | - full rpc/sdk adapter wiring and sync engine |
| 407 | 423 | ||
| 408 | --- | 424 | --- |
| 409 | 425 | ||
| 410 | ## 19) Immediate next steps | 426 | ## 19) Immediate next steps |
| 411 | 427 | ||
| 412 | 1. Define canonical provider tool descriptor + provider service contracts (`discover` / `invoke`). | 428 | 1. Add first `tools.executor.sources.add/list/remove` control operations end-to-end in PM + MCP gateway. |
| 413 | 2. Implement engine-side provider registry and routing for tool invocation. | 429 | 2. Implement approval adapter contract with persisted pending/resume/deny flow for runtime tool calls. |
| 414 | 3. Wire minimal `apps/pm` execute flow (single run lifecycle) using provider routing. | 430 | 3. Add provider/runtime conformance tests (invoke success/failure, timeout, pending approval, cancel). |
| 415 | 4. Implement first end-to-end providers: `openapi` (network) and `in_memory` (AI-SDK style). | 431 | 4. Add Cloudflare worker loader adapter integration against the runtime adapter contract. |
| 416 | 5. Add first `tools.executor.sources.add/list/remove` control operations in PM gateway. | 432 | 5. Extend PM execute lifecycle from single-run scaffold to persisted run state + streaming status. |
| 417 | 6. Add provider conformance tests and one vertical test: OpenAPI spec -> manifest -> execute tool call. | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -5,4 +5,5 @@ Local Process Manager app scaffold for Executor v2. · 4 unmodified lines | |||
| 5 | Current scaffold includes: | 5 | Current scaffold includes: |
| 6 | - local MCP endpoint at `POST /mcp` | 6 | - local MCP endpoint at `POST /mcp` |
| 7 | - health endpoint at `GET /healthz` | 7 | - health endpoint at `GET /healthz` |
| 8 | - MCP stub tool routing via `@executor-v2/mcp-gateway` | 8 | - MCP tool routing via `@executor-v2/mcp-gateway` |
| 9 | - `executor.execute` wired to engine runtime adapters (`local-inproc` default, `deno-subprocess` optional) | ||
| @@ -8,6 +8,8 @@ · 7 unmodified lines | |||
| 8 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 8 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 9 | }, | 9 | }, |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "@executor-v2/mcp-gateway": "workspace:*" | 11 | "@executor-v2/engine": "workspace:*", |
| 12 | "@executor-v2/mcp-gateway": "workspace:*", | ||
| 13 | "effect": "^3.19.8" | ||
| 12 | } | 14 | } |
| 13 | } | 15 | } |
| @@ -1,12 +1,78 @@ | |||
| 1 | import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway"; | 1 | import { |
| 2 | makeCloudflareWorkerLoaderRuntimeAdapter, | ||
| 3 | makeDenoSubprocessRuntimeAdapter, | ||
| 4 | makeLocalInProcessRuntimeAdapter, | ||
| 5 | makeRuntimeAdapterRegistry, | ||
| 6 | makeToolProviderRegistry, | ||
| 7 | ToolProviderRegistryService, | ||
| 8 | } from "@executor-v2/engine"; | ||
| 9 | import { | ||
| 10 | handleMcpHttpRequest, | ||
| 11 | type ExecuteToolInput, | ||
| 12 | type ExecuteToolResult, | ||
| 13 | } from "@executor-v2/mcp-gateway"; | ||
| 14 | import * as Effect from "effect/Effect"; | ||
| 15 | import * as Either from "effect/Either"; | ||
| 2 | 16 | ||
| 3 | const port = Number(Bun.env.PORT ?? 8787); | 17 | const port = Number(Bun.env.PORT ?? 8787); |
| 4 | 18 | ||
| 19 | const runtimeRegistry = makeRuntimeAdapterRegistry([ | ||
| 20 | makeLocalInProcessRuntimeAdapter(), | ||
| 21 | makeDenoSubprocessRuntimeAdapter(), | ||
| 22 | makeCloudflareWorkerLoaderRuntimeAdapter(), | ||
| 23 | ]); | ||
| 24 | |||
| 25 | const toolRegistry = makeToolProviderRegistry([]); | ||
| 26 | |||
| 27 | const errorToText = (error: unknown): string => { | ||
| 28 | if (error instanceof Error) { | ||
| 29 | return error.message; | ||
| 30 | } | ||
| 31 | |||
| 32 | if ( | ||
| 33 | typeof error === "object" && | ||
| 34 | error !== null && | ||
| 35 | "message" in error && | ||
| 36 | typeof error.message === "string" | ||
| 37 | ) { | ||
| 38 | return error.message; | ||
| 39 | } | ||
| 40 | |||
| 41 | return String(error); | ||
| 42 | }; | ||
| 43 | |||
| 44 | const executeTool = async (input: ExecuteToolInput): Promise<ExecuteToolResult> => { | ||
| 45 | const result = await Effect.runPromise( | ||
| 46 | Effect.either( | ||
| 47 | runtimeRegistry | ||
| 48 | .execute({ | ||
| 49 | runtimeKind: input.runtimeKind ?? "local-inproc", | ||
| 50 | code: input.code, | ||
| 51 | tools: [], | ||
| 52 | }) | ||
| 53 | .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)), | ||
| 54 | ), | ||
| 55 | ); | ||
| 56 | |||
| 57 | if (Either.isLeft(result)) { | ||
| 58 | return { | ||
| 59 | isError: true, | ||
| 60 | error: errorToText(result.left), | ||
| 61 | }; | ||
| 62 | } | ||
| 63 | |||
| 64 | return { | ||
| 65 | isError: false, | ||
| 66 | output: result.right, | ||
| 67 | }; | ||
| 68 | }; | ||
| 69 | |||
| 5 | const handleMcp = async (request: Request): Promise<Response> => | 70 | const handleMcp = async (request: Request): Promise<Response> => |
| 6 | handleMcpHttpRequest(request, { | 71 | handleMcpHttpRequest(request, { |
| 7 | target: "local", | 72 | target: "local", |
| 8 | serverName: "executor-v2-pm", | 73 | serverName: "executor-v2-pm", |
| 9 | serverVersion: "0.0.0", | 74 | serverVersion: "0.0.0", |
| 75 | execute: executeTool, | ||
| 10 | }); | 76 | }); |
| 11 | 77 | ||
| 12 | const server = Bun.serve({ | 78 | const server = Bun.serve({ |
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -25,7 +25,9 @@ · 24 unmodified lines | |||
| 25 | "apps/pm": { | 25 | "apps/pm": { |
| 26 | "name": "@executor-v2/app-pm", | 26 | "name": "@executor-v2/app-pm", |
| 27 | "dependencies": { | 27 | "dependencies": { |
| 28 | "@executor-v2/engine": "workspace:*", | ||
| 28 | "@executor-v2/mcp-gateway": "workspace:*", | 29 | "@executor-v2/mcp-gateway": "workspace:*", |
| 30 | "effect": "^3.19.8", | ||
| 29 | }, | 31 | }, |
| 30 | }, | 32 | }, |
| 31 | "apps/web": { | 33 | "apps/web": { |
| @@ -50,7 +52,6 @@ · 18 unmodified lines | |||
| 50 | "next": "^16.1.6", | 52 | "next": "^16.1.6", |
| 51 | }, | 53 | }, |
| 52 | "devDependencies": { | 54 | "devDependencies": { |
| 53 | "@effect/vitest": "^0.27.0", | ||
| 54 | "@types/node": "^20.19.9", | 55 | "@types/node": "^20.19.9", |
| 55 | "typescript": "^5.9.2", | 56 | "typescript": "^5.9.2", |
| 56 | "vitest": "^3.2.4", | 57 | "vitest": "^3.2.4", |
| @@ -67,7 +68,9 @@ · 10 unmodified lines | |||
| 67 | "effect": "^3.19.8", | 68 | "effect": "^3.19.8", |
| 68 | }, | 69 | }, |
| 69 | "devDependencies": { | 70 | "devDependencies": { |
| 71 | "@effect/vitest": "^0.27.0", | ||
| 70 | "@executor-v2/source-manager": "workspace:*", | 72 | "@executor-v2/source-manager": "workspace:*", |
| 73 | "vitest": "^3.2.4", | ||
| 71 | }, | 74 | }, |
| 72 | }, | 75 | }, |
| 73 | "packages/mcp-gateway": { | 76 | "packages/mcp-gateway": { |
| @@ -102,6 +105,10 @@ · 28 unmodified lines | |||
| 102 | "@executor-v2/schema": "workspace:*", | 105 | "@executor-v2/schema": "workspace:*", |
| 103 | "effect": "^3.19.8", | 106 | "effect": "^3.19.8", |
| 104 | }, | 107 | }, |
| 108 | "devDependencies": { | ||
| 109 | "@effect/vitest": "^0.27.0", | ||
| 110 | "vitest": "^3.2.4", | ||
| 111 | }, | ||
| 105 | }, | 112 | }, |
| 106 | "packages/state-contracts": { | 113 | "packages/state-contracts": { |
| 107 | "name": "@executor-v2/persistence-ports", | 114 | "name": "@executor-v2/persistence-ports", |
| @@ -0,0 +1,3 @@ | |||
| 1 | { | ||
| 2 | "$schema": "https://unpkg.com/knip@latest/schema.json" | ||
| 3 | } | ||
| @@ -9,7 +9,8 @@ · 8 unmodified lines | |||
| 9 | ], | 9 | ], |
| 10 | "scripts": { | 10 | "scripts": { |
| 11 | "prepare": "effect-language-service patch", | 11 | "prepare": "effect-language-service patch", |
| 12 | "typecheck": "tsc --noEmit" | 12 | "typecheck": "tsc --noEmit", |
| 13 | "knip": "bunx knip --config knip.json" | ||
| 13 | }, | 14 | }, |
| 14 | "devDependencies": { | 15 | "devDependencies": { |
| 15 | "@effect/language-service": "^0.76.0", | 16 | "@effect/language-service": "^0.76.0", |
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -27,7 +27,6 @@ · 26 unmodified lines | |||
| 27 | "next": "^16.1.6" | 27 | "next": "^16.1.6" |
| 28 | }, | 28 | }, |
| 29 | "devDependencies": { | 29 | "devDependencies": { |
| 30 | "@effect/vitest": "^0.27.0", | ||
| 31 | "@types/node": "^20.19.9", | 30 | "@types/node": "^20.19.9", |
| 32 | "typescript": "^5.9.2", | 31 | "typescript": "^5.9.2", |
| 33 | "vitest": "^3.2.4" | 32 | "vitest": "^3.2.4" |
| @@ -44,7 +44,6 @@ export { Rpc, RpcGroup, RpcMiddleware }; · 43 unmodified lines | |||
| 44 | 44 | ||
| 45 | export { | 45 | export { |
| 46 | WrapperTypeId, | 46 | WrapperTypeId, |
| 47 | type WrapperTypeId as WrapperTypeIdType, | ||
| 48 | type Wrapper, | 47 | type Wrapper, |
| 49 | isWrapper, | 48 | isWrapper, |
| 50 | wrap, | 49 | wrap, |
| @@ -9,7 +9,7 @@ export const convexRequestIdHeader = "convex-request-id"; · 8 unmodified lines | |||
| 9 | 9 | ||
| 10 | export type RpcClientKind = "query" | "mutation" | "action"; | 10 | export type RpcClientKind = "query" | "mutation" | "action"; |
| 11 | 11 | ||
| 12 | export interface RpcTelemetryContext { | 12 | interface RpcTelemetryContext { |
| 13 | readonly traceparent: string; | 13 | readonly traceparent: string; |
| 14 | readonly b3?: string; | 14 | readonly b3?: string; |
| 15 | readonly tracestate?: string; | 15 | readonly tracestate?: string; |
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -8,4 +8,5 @@ Current scaffold includes: · 7 unmodified lines | |||
| 8 | - OpenAPI provider invocation and manifest-to-descriptor conversion helpers | 8 | - OpenAPI provider invocation and manifest-to-descriptor conversion helpers |
| 9 | - minimal in-process JavaScript runner with `tools.*` proxy dispatch into provider registry | 9 | - minimal in-process JavaScript runner with `tools.*` proxy dispatch into provider registry |
| 10 | - Deno subprocess runner with line-delimited JSON IPC for proxied `tools.*` calls | 10 | - Deno subprocess runner with line-delimited JSON IPC for proxied `tools.*` calls |
| 11 | - runtime adapter contract + registry (`local-inproc`, `deno-subprocess`, `cloudflare-worker-loader` scaffold) | ||
| 11 | - vertical integration test covering OpenAPI extraction -> descriptor conversion -> code execution -> HTTP call | 12 | - vertical integration test covering OpenAPI extraction -> descriptor conversion -> code execution -> HTTP call |
| @@ -6,7 +6,7 @@ · 5 unmodified lines | |||
| 6 | ".": "./src/index.ts" | 6 | ".": "./src/index.ts" |
| 7 | }, | 7 | }, |
| 8 | "scripts": { | 8 | "scripts": { |
| 9 | "test": "bun test src/*.test.ts", | 9 | "test": "bunx vitest run --no-watch src/*.test.ts", |
| 10 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 10 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 11 | }, | 11 | }, |
| 12 | "dependencies": { | 12 | "dependencies": { |
| @@ -15,6 +15,8 @@ · 2 unmodified lines | |||
| 15 | "effect": "^3.19.8" | 15 | "effect": "^3.19.8" |
| 16 | }, | 16 | }, |
| 17 | "devDependencies": { | 17 | "devDependencies": { |
| 18 | "@executor-v2/source-manager": "workspace:*" | 18 | "@effect/vitest": "^0.27.0", |
| 19 | "@executor-v2/source-manager": "workspace:*", | ||
| 20 | "vitest": "^3.2.4" | ||
| 19 | } | 21 | } |
| 20 | } | 22 | } |
| @@ -1,9 +1,10 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 1 | import { | 2 | import { |
| 2 | CanonicalToolDescriptorSchema, | 3 | CanonicalToolDescriptorSchema, |
| 3 | type CanonicalToolDescriptor, | 4 | type CanonicalToolDescriptor, |
| 4 | } from "@executor-v2/schema"; | 5 | } from "@executor-v2/schema"; |
| 5 | import { describe, expect, test } from "bun:test"; | ||
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Either from "effect/Either"; | ||
| 7 | import * as Schema from "effect/Schema"; | 8 | import * as Schema from "effect/Schema"; |
| 8 | 9 | ||
| 9 | import { | 10 | import { |
| @@ -33,30 +34,30 @@ const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor · 23 unmodified lines | |||
| 33 | }); | 34 | }); |
| 34 | 35 | ||
| 35 | describe("executeJavaScriptInDenoSubprocess", () => { | 36 | describe("executeJavaScriptInDenoSubprocess", () => { |
| 36 | test("runs code in Deno subprocess and proxies tool calls", async () => { | 37 | it.effect("runs code in Deno subprocess and proxies tool calls", () => |
| 37 | const sumProvider: ToolProvider = { | 38 | Effect.gen(function* () { |
| 38 | kind: "in_memory", | 39 | const sumProvider: ToolProvider = { |
| 39 | invoke: (input) => | 40 | kind: "in_memory", |
| 40 | Effect.gen(function* () { | 41 | invoke: (input) => |
| 41 | if (input.tool.toolId !== "sum") { | 42 | Effect.gen(function* () { |
| 43 | if (input.tool.toolId !== "sum") { | ||
| 44 | return { | ||
| 45 | output: `unknown tool: ${input.tool.toolId}`, | ||
| 46 | isError: true, | ||
| 47 | } as const; | ||
| 48 | } | ||
| 49 | |||
| 50 | const args = input.args as { a: number; b: number }; | ||
| 42 | return { | 51 | return { |
| 43 | output: `unknown tool: ${input.tool.toolId}`, | 52 | output: args.a + args.b, |
| 44 | isError: true, | 53 | isError: false, |
| 45 | } as const; | 54 | } as const; |
| 46 | } | 55 | }), |
| 47 | 56 | }; | |
| 48 | const args = input.args as { a: number; b: number }; | ||
| 49 | return { | ||
| 50 | output: args.a + args.b, | ||
| 51 | isError: false, | ||
| 52 | } as const; | ||
| 53 | }), | ||
| 54 | }; | ||
| 55 | 57 | ||
| 56 | const registry = makeToolProviderRegistry([sumProvider]); | 58 | const registry = makeToolProviderRegistry([sumProvider]); |
| 57 | 59 | ||
| 58 | const result = await Effect.runPromise( | 60 | const result = yield* executeJavaScriptInDenoSubprocess({ |
| 59 | executeJavaScriptInDenoSubprocess({ | ||
| 60 | code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });", | 61 | code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });", |
| 61 | tools: [ | 62 | tools: [ |
| 62 | { | 63 | { |
| @@ -65,31 +66,30 @@ describe("executeJavaScriptInDenoSubprocess", () => { · 2 unmodified lines | |||
| 65 | }, | 66 | }, |
| 66 | ], | 67 | ], |
| 67 | timeoutMs: 10_000, | 68 | timeoutMs: 10_000, |
| 68 | }).pipe(Effect.provideService(ToolProviderRegistryService, registry)), | 69 | }).pipe(Effect.provideService(ToolProviderRegistryService, registry)); |
| 69 | ); | ||
| 70 | 70 | ||
| 71 | expect(result).toBe(5); | 71 | expect(result).toBe(5); |
| 72 | }); | 72 | }), |
| 73 | ); | ||
| 73 | 74 | ||
| 74 | test("returns a typed error when Deno executable is missing", async () => { | 75 | it.effect("returns a typed error when Deno executable is missing", () => |
| 75 | const registry = makeToolProviderRegistry([]); | 76 | Effect.gen(function* () { |
| 77 | const registry = makeToolProviderRegistry([]); | ||
| 76 | 78 | ||
| 77 | const result = await Effect.runPromise( | 79 | const result = yield* Effect.either( |
| 78 | Effect.either( | ||
| 79 | executeJavaScriptInDenoSubprocess({ | 80 | executeJavaScriptInDenoSubprocess({ |
| 80 | code: "return 1;", | 81 | code: "return 1;", |
| 81 | tools: [], | 82 | tools: [], |
| 82 | denoExecutable: "/definitely-missing-deno-binary", | 83 | denoExecutable: "/definitely-missing-deno-binary", |
| 83 | timeoutMs: 1_000, | 84 | timeoutMs: 1_000, |
| 84 | }).pipe(Effect.provideService(ToolProviderRegistryService, registry)), | 85 | }).pipe(Effect.provideService(ToolProviderRegistryService, registry)), |
| 85 | ), | 86 | ); |
| 86 | ); | ||
| 87 | |||
| 88 | if (result._tag === "Right") { | ||
| 89 | throw new Error("expected missing Deno executable to fail"); | ||
| 90 | } | ||
| 91 | 87 | ||
| 92 | expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError); | 88 | expect(Either.isLeft(result)).toBe(true); |
| 93 | expect(result.left.operation).toBe("spawn"); | 89 | if (Either.isLeft(result)) { |
| 94 | }); | 90 | expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError); |
| 91 | expect(result.left.operation).toBe("spawn"); | ||
| 92 | } | ||
| 93 | }), | ||
| 94 | ); | ||
| 95 | }); | 95 | }); |
| @@ -11,6 +11,22 @@ export { · 10 unmodified lines | |||
| 11 | type ExecuteJavaScriptInput, | 11 | type ExecuteJavaScriptInput, |
| 12 | type RunnableTool, | 12 | type RunnableTool, |
| 13 | } from "./local-runner"; | 13 | } from "./local-runner"; |
| 14 | export { | ||
| 15 | RuntimeAdapterRegistryLive, | ||
| 16 | RuntimeAdapterRegistryError, | ||
| 17 | RuntimeAdapterRegistryService, | ||
| 18 | RuntimeAdapterError, | ||
| 19 | makeCloudflareWorkerLoaderRuntimeAdapter, | ||
| 20 | makeDenoSubprocessRuntimeAdapter, | ||
| 21 | makeLocalInProcessRuntimeAdapter, | ||
| 22 | makeRuntimeAdapterRegistry, | ||
| 23 | type RuntimeAdapter, | ||
| 24 | type RuntimeAdapterKind, | ||
| 25 | type RuntimeAdapterRegistry, | ||
| 26 | type RuntimeExecuteError, | ||
| 27 | type RuntimeExecuteInput, | ||
| 28 | type RuntimeRunnableTool, | ||
| 29 | } from "./runtime-adapters"; | ||
| 14 | export { | 30 | export { |
| 15 | makeOpenApiToolProvider, | 31 | makeOpenApiToolProvider, |
| 16 | openApiToolDescriptorsFromManifest, | 32 | openApiToolDescriptorsFromManifest, |
| @@ -1,9 +1,10 @@ | |||
| 1 | import { | 1 | import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; |
| 2 | type ToolArtifactStore, | 2 | |
| 3 | } from "@executor-v2/persistence-ports"; | 3 | import { describe, expect, it } from "@effect/vitest"; |
| 4 | import { type ToolArtifactStore } from "@executor-v2/persistence-ports"; | ||
| 4 | import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema"; | 5 | import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema"; |
| 5 | import { makeSourceManagerService } from "@executor-v2/source-manager"; | 6 | import { makeSourceManagerService } from "@executor-v2/source-manager"; |
| 6 | import { describe, expect, test } from "bun:test"; | 7 | import * as Data from "effect/Data"; |
| 7 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 8 | import * as Option from "effect/Option"; | 9 | import * as Option from "effect/Option"; |
| 9 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| @@ -27,51 +28,104 @@ type TestServer = { · 17 unmodified lines | |||
| 27 | query: string; | 28 | query: string; |
| 28 | apiKey: string | null; | 29 | apiKey: string | null; |
| 29 | }>; | 30 | }>; |
| 30 | server: Bun.Server<unknown>; | 31 | close: () => Promise<void>; |
| 31 | }; | 32 | }; |
| 32 | 33 | ||
| 33 | const makeTestServer = Effect.acquireRelease( | 34 | class TestServerReleaseError extends Data.TaggedError("TestServerReleaseError")<{ |
| 34 | Effect.sync<TestServer>(() => { | 35 | message: string; |
| 35 | const requests: TestServer["requests"] = []; | 36 | }> {} |
| 36 | 37 | ||
| 37 | const server = Bun.serve({ | 38 | const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => { |
| 38 | port: 0, | 39 | res.statusCode = statusCode; |
| 39 | fetch: async (request) => { | 40 | res.setHeader("content-type", "application/json"); |
| 40 | const url = new URL(request.url); | 41 | res.end(JSON.stringify(body)); |
| 41 | 42 | }; | |
| 42 | if (url.pathname === "/users/u123") { | 43 | |
| 43 | requests.push({ | 44 | const getHeaderValue = ( |
| 44 | path: url.pathname, | 45 | req: IncomingMessage, |
| 45 | query: url.search, | 46 | key: string, |
| 46 | apiKey: request.headers.get("x-api-key"), | 47 | ): string | null => { |
| 47 | }); | 48 | const value = req.headers[key]; |
| 49 | if (typeof value === "string") { | ||
| 50 | return value; | ||
| 51 | } | ||
| 48 | 52 | ||
| 49 | return Response.json({ | 53 | if (Array.isArray(value)) { |
| 50 | id: "u123", | 54 | return value[0] ?? null; |
| 51 | verbose: url.searchParams.get("verbose") === "true", | 55 | } |
| 52 | apiKey: request.headers.get("x-api-key"), | 56 | |
| 57 | return null; | ||
| 58 | }; | ||
| 59 | |||
| 60 | const makeTestServer = Effect.acquireRelease( | ||
| 61 | Effect.promise<TestServer>( | ||
| 62 | () => | ||
| 63 | new Promise<TestServer>((resolve, reject) => { | ||
| 64 | const requests: TestServer["requests"] = []; | ||
| 65 | |||
| 66 | const server = createServer((req, res) => { | ||
| 67 | const host = getHeaderValue(req, "host") ?? "127.0.0.1"; | ||
| 68 | const url = new URL(req.url ?? "/", `http://${host}`); | ||
| 69 | |||
| 70 | if (url.pathname === "/users/u123") { | ||
| 71 | requests.push({ | ||
| 72 | path: url.pathname, | ||
| 73 | query: url.search, | ||
| 74 | apiKey: getHeaderValue(req, "x-api-key"), | ||
| 75 | }); | ||
| 76 | |||
| 77 | jsonResponse(res, 200, { | ||
| 78 | id: "u123", | ||
| 79 | verbose: url.searchParams.get("verbose") === "true", | ||
| 80 | apiKey: getHeaderValue(req, "x-api-key"), | ||
| 81 | }); | ||
| 82 | return; | ||
| 83 | } | ||
| 84 | |||
| 85 | jsonResponse(res, 404, { error: "not found" }); | ||
| 86 | }); | ||
| 87 | |||
| 88 | server.once("error", (error) => { | ||
| 89 | reject(error); | ||
| 90 | }); | ||
| 91 | |||
| 92 | server.listen(0, "127.0.0.1", () => { | ||
| 93 | const address = server.address(); | ||
| 94 | if (!address || typeof address === "string") { | ||
| 95 | reject(new Error("failed to resolve test server address")); | ||
| 96 | return; | ||
| 97 | } | ||
| 98 | |||
| 99 | resolve({ | ||
| 100 | baseUrl: `http://127.0.0.1:${address.port}`, | ||
| 101 | requests, | ||
| 102 | close: () => | ||
| 103 | new Promise<void>((closeResolve, closeReject) => { | ||
| 104 | server.close((error) => { | ||
| 105 | if (error) { | ||
| 106 | closeReject(error); | ||
| 107 | return; | ||
| 108 | } | ||
| 109 | closeResolve(); | ||
| 110 | }); | ||
| 111 | }), | ||
| 53 | }); | 112 | }); |
| 54 | } | 113 | }); |
| 55 | 114 | }) | |
| 56 | return Response.json({ error: "not found" }, { status: 404 }); | 115 | ), |
| 57 | }, | 116 | (testServer) => |
| 58 | }); | 117 | Effect.tryPromise({ |
| 59 | 118 | try: () => testServer.close(), | |
| 60 | return { | 119 | catch: (cause) => |
| 61 | baseUrl: `http://127.0.0.1:${server.port}`, | 120 | new TestServerReleaseError({ |
| 62 | requests, | 121 | message: cause instanceof Error ? cause.message : String(cause), |
| 63 | server, | 122 | }), |
| 64 | }; | 123 | }).pipe(Effect.orDie), |
| 65 | }), | ||
| 66 | ({ server }) => | ||
| 67 | Effect.sync(() => { | ||
| 68 | server.stop(true); | ||
| 69 | }), | ||
| 70 | ); | 124 | ); |
| 71 | 125 | ||
| 72 | describe("OpenAPI execution vertical slice", () => { | 126 | describe("OpenAPI execution vertical slice", () => { |
| 73 | test("extracts OpenAPI tools and executes code against provider", async () => { | 127 | it.scoped("extracts OpenAPI tools and executes code against provider", () => |
| 74 | const program = Effect.gen(function* () { | 128 | Effect.gen(function* () { |
| 75 | const testServer = yield* makeTestServer; | 129 | const testServer = yield* makeTestServer; |
| 76 | 130 | ||
| 77 | const openApiSpec = { | 131 | const openApiSpec = { |
| @@ -185,8 +239,6 @@ return await tools.getUser({ · 107 unmodified lines | |||
| 185 | expect(testServer.requests[0]?.path).toBe("/users/u123"); | 239 | expect(testServer.requests[0]?.path).toBe("/users/u123"); |
| 186 | expect(testServer.requests[0]?.query).toBe("?verbose=true"); | 240 | expect(testServer.requests[0]?.query).toBe("?verbose=true"); |
| 187 | expect(testServer.requests[0]?.apiKey).toBe("sk_test"); | 241 | expect(testServer.requests[0]?.apiKey).toBe("sk_test"); |
| 188 | }); | 242 | }), |
| 189 | 243 | ); | |
| 190 | await Effect.runPromise(program.pipe(Effect.scoped)); | ||
| 191 | }); | ||
| 192 | }); | 244 | }); |
| @@ -0,0 +1,96 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | import { | ||
| 3 | CanonicalToolDescriptorSchema, | ||
| 4 | type CanonicalToolDescriptor, | ||
| 5 | } from "@executor-v2/schema"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | import * as Either from "effect/Either"; | ||
| 8 | import * as Schema from "effect/Schema"; | ||
| 9 | |||
| 10 | import { | ||
| 11 | makeCloudflareWorkerLoaderRuntimeAdapter, | ||
| 12 | makeLocalInProcessRuntimeAdapter, | ||
| 13 | makeRuntimeAdapterRegistry, | ||
| 14 | RuntimeAdapterError, | ||
| 15 | } from "./runtime-adapters"; | ||
| 16 | import { | ||
| 17 | makeToolProviderRegistry, | ||
| 18 | ToolProviderRegistryService, | ||
| 19 | type ToolProvider, | ||
| 20 | } from "./tool-providers"; | ||
| 21 | |||
| 22 | const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync( | ||
| 23 | CanonicalToolDescriptorSchema, | ||
| 24 | ); | ||
| 25 | |||
| 26 | const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor({ | ||
| 27 | providerKind: "in_memory", | ||
| 28 | sourceId: null, | ||
| 29 | workspaceId: null, | ||
| 30 | toolId: "sum", | ||
| 31 | name: "sum", | ||
| 32 | description: null, | ||
| 33 | invocationMode: "in_memory", | ||
| 34 | availability: "local_only", | ||
| 35 | providerPayload: null, | ||
| 36 | }); | ||
| 37 | |||
| 38 | describe("runtime adapters", () => { | ||
| 39 | it.effect("executes with local-inproc adapter via runtime registry", () => | ||
| 40 | Effect.gen(function* () { | ||
| 41 | const sumProvider: ToolProvider = { | ||
| 42 | kind: "in_memory", | ||
| 43 | invoke: (input) => | ||
| 44 | Effect.gen(function* () { | ||
| 45 | const args = input.args as { a: number; b: number }; | ||
| 46 | return { | ||
| 47 | output: args.a + args.b, | ||
| 48 | isError: false, | ||
| 49 | } as const; | ||
| 50 | }), | ||
| 51 | }; | ||
| 52 | |||
| 53 | const toolRegistry = makeToolProviderRegistry([sumProvider]); | ||
| 54 | const runtimeRegistry = makeRuntimeAdapterRegistry([ | ||
| 55 | makeLocalInProcessRuntimeAdapter(), | ||
| 56 | ]); | ||
| 57 | |||
| 58 | const result = yield* runtimeRegistry | ||
| 59 | .execute({ | ||
| 60 | runtimeKind: "local-inproc", | ||
| 61 | code: "return await tools.sum({ a: 2, b: 4 });", | ||
| 62 | tools: [{ descriptor: sumToolDescriptor, source: null }], | ||
| 63 | }) | ||
| 64 | .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)); | ||
| 65 | |||
| 66 | expect(result).toBe(6); | ||
| 67 | }), | ||
| 68 | ); | ||
| 69 | |||
| 70 | it.effect("returns typed not-implemented error for cloudflare adapter", () => | ||
| 71 | Effect.gen(function* () { | ||
| 72 | const toolRegistry = makeToolProviderRegistry([]); | ||
| 73 | const runtimeRegistry = makeRuntimeAdapterRegistry([ | ||
| 74 | makeCloudflareWorkerLoaderRuntimeAdapter(), | ||
| 75 | ]); | ||
| 76 | |||
| 77 | const result = yield* Effect.either( | ||
| 78 | runtimeRegistry | ||
| 79 | .execute({ | ||
| 80 | runtimeKind: "cloudflare-worker-loader", | ||
| 81 | code: "return 1;", | ||
| 82 | tools: [], | ||
| 83 | }) | ||
| 84 | .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)), | ||
| 85 | ); | ||
| 86 | |||
| 87 | expect(Either.isLeft(result)).toBe(true); | ||
| 88 | if (Either.isLeft(result)) { | ||
| 89 | expect(result.left).toBeInstanceOf(RuntimeAdapterError); | ||
| 90 | if (result.left instanceof RuntimeAdapterError) { | ||
| 91 | expect(result.left.runtimeKind).toBe("cloudflare-worker-loader"); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | }), | ||
| 95 | ); | ||
| 96 | }); | ||
| @@ -0,0 +1,212 @@ | |||
| 1 | import type { Source } from "@executor-v2/schema"; | ||
| 2 | import * as Context from "effect/Context"; | ||
| 3 | import * as Data from "effect/Data"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as Layer from "effect/Layer"; | ||
| 6 | |||
| 7 | import { | ||
| 8 | DenoSubprocessRunnerError, | ||
| 9 | executeJavaScriptInDenoSubprocess, | ||
| 10 | } from "./deno-subprocess-runner"; | ||
| 11 | import { | ||
| 12 | executeJavaScriptWithTools, | ||
| 13 | LocalCodeRunnerError, | ||
| 14 | } from "./local-runner"; | ||
| 15 | import { | ||
| 16 | ToolProviderError, | ||
| 17 | ToolProviderRegistryError, | ||
| 18 | type CanonicalToolDescriptor, | ||
| 19 | type ToolProviderRegistryService, | ||
| 20 | } from "./tool-providers"; | ||
| 21 | |||
| 22 | export type RuntimeAdapterKind = | ||
| 23 | | "local-inproc" | ||
| 24 | | "deno-subprocess" | ||
| 25 | | "cloudflare-worker-loader"; | ||
| 26 | |||
| 27 | export type RuntimeRunnableTool = { | ||
| 28 | descriptor: CanonicalToolDescriptor; | ||
| 29 | source: Source | null; | ||
| 30 | }; | ||
| 31 | |||
| 32 | export type RuntimeExecuteInput = { | ||
| 33 | code: string; | ||
| 34 | tools: ReadonlyArray<RuntimeRunnableTool>; | ||
| 35 | timeoutMs?: number; | ||
| 36 | }; | ||
| 37 | |||
| 38 | export class RuntimeAdapterError extends Data.TaggedError("RuntimeAdapterError")<{ | ||
| 39 | operation: string; | ||
| 40 | runtimeKind: RuntimeAdapterKind; | ||
| 41 | message: string; | ||
| 42 | details: string | null; | ||
| 43 | }> {} | ||
| 44 | |||
| 45 | export type RuntimeExecuteError = | ||
| 46 | | RuntimeAdapterError | ||
| 47 | | LocalCodeRunnerError | ||
| 48 | | DenoSubprocessRunnerError | ||
| 49 | | ToolProviderRegistryError | ||
| 50 | | ToolProviderError; | ||
| 51 | |||
| 52 | export interface RuntimeAdapter { | ||
| 53 | readonly kind: RuntimeAdapterKind; | ||
| 54 | readonly isAvailable: () => Effect.Effect<boolean>; | ||
| 55 | readonly execute: ( | ||
| 56 | input: RuntimeExecuteInput, | ||
| 57 | ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>; | ||
| 58 | } | ||
| 59 | |||
| 60 | export class RuntimeAdapterRegistryError extends Data.TaggedError( | ||
| 61 | "RuntimeAdapterRegistryError", | ||
| 62 | )<{ | ||
| 63 | operation: string; | ||
| 64 | runtimeKind: RuntimeAdapterKind; | ||
| 65 | message: string; | ||
| 66 | }> {} | ||
| 67 | |||
| 68 | export interface RuntimeAdapterRegistry { | ||
| 69 | readonly register: ( | ||
| 70 | adapter: RuntimeAdapter, | ||
| 71 | ) => Effect.Effect<void, RuntimeAdapterRegistryError>; | ||
| 72 | |||
| 73 | readonly registerAll: ( | ||
| 74 | adapters: ReadonlyArray<RuntimeAdapter>, | ||
| 75 | ) => Effect.Effect<void, RuntimeAdapterRegistryError>; | ||
| 76 | |||
| 77 | readonly get: ( | ||
| 78 | runtimeKind: RuntimeAdapterKind, | ||
| 79 | ) => Effect.Effect<RuntimeAdapter, RuntimeAdapterRegistryError>; | ||
| 80 | |||
| 81 | readonly execute: ( | ||
| 82 | input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind }, | ||
| 83 | ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError, ToolProviderRegistryService>; | ||
| 84 | } | ||
| 85 | |||
| 86 | export class RuntimeAdapterRegistryService extends Context.Tag( | ||
| 87 | "@executor-v2/engine/RuntimeAdapterRegistryService", | ||
| 88 | )<RuntimeAdapterRegistryService, RuntimeAdapterRegistry>() {} | ||
| 89 | |||
| 90 | type DenoSubprocessRuntimeAdapterOptions = { | ||
| 91 | denoExecutable?: string; | ||
| 92 | defaultTimeoutMs?: number; | ||
| 93 | }; | ||
| 94 | |||
| 95 | const duplicateRuntimeAdapterError = ( | ||
| 96 | runtimeKind: RuntimeAdapterKind, | ||
| 97 | ): RuntimeAdapterRegistryError => | ||
| 98 | new RuntimeAdapterRegistryError({ | ||
| 99 | operation: "register", | ||
| 100 | runtimeKind, | ||
| 101 | message: `Runtime adapter already registered: ${runtimeKind}`, | ||
| 102 | }); | ||
| 103 | |||
| 104 | const runtimeAdapterNotFoundError = ( | ||
| 105 | operation: string, | ||
| 106 | runtimeKind: RuntimeAdapterKind, | ||
| 107 | ): RuntimeAdapterRegistryError => | ||
| 108 | new RuntimeAdapterRegistryError({ | ||
| 109 | operation, | ||
| 110 | runtimeKind, | ||
| 111 | message: `No runtime adapter registered for kind: ${runtimeKind}`, | ||
| 112 | }); | ||
| 113 | |||
| 114 | export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({ | ||
| 115 | kind: "local-inproc", | ||
| 116 | isAvailable: () => Effect.succeed(true), | ||
| 117 | execute: (input) => | ||
| 118 | executeJavaScriptWithTools({ | ||
| 119 | code: input.code, | ||
| 120 | tools: input.tools, | ||
| 121 | }), | ||
| 122 | }); | ||
| 123 | |||
| 124 | export const makeDenoSubprocessRuntimeAdapter = ( | ||
| 125 | options: DenoSubprocessRuntimeAdapterOptions = {}, | ||
| 126 | ): RuntimeAdapter => ({ | ||
| 127 | kind: "deno-subprocess", | ||
| 128 | isAvailable: () => Effect.succeed(true), | ||
| 129 | execute: (input) => | ||
| 130 | executeJavaScriptInDenoSubprocess({ | ||
| 131 | code: input.code, | ||
| 132 | tools: input.tools, | ||
| 133 | timeoutMs: input.timeoutMs ?? options.defaultTimeoutMs, | ||
| 134 | denoExecutable: options.denoExecutable, | ||
| 135 | }), | ||
| 136 | }); | ||
| 137 | |||
| 138 | export const makeCloudflareWorkerLoaderRuntimeAdapter = (): RuntimeAdapter => ({ | ||
| 139 | kind: "cloudflare-worker-loader", | ||
| 140 | isAvailable: () => Effect.succeed(false), | ||
| 141 | execute: () => | ||
| 142 | new RuntimeAdapterError({ | ||
| 143 | operation: "execute", | ||
| 144 | runtimeKind: "cloudflare-worker-loader", | ||
| 145 | message: "Cloudflare worker loader runtime adapter is not implemented yet", | ||
| 146 | details: null, | ||
| 147 | }), | ||
| 148 | }); | ||
| 149 | |||
| 150 | export const makeRuntimeAdapterRegistry = ( | ||
| 151 | initialAdapters: ReadonlyArray<RuntimeAdapter> = [], | ||
| 152 | ): RuntimeAdapterRegistry => { | ||
| 153 | const adaptersByKind = new Map<RuntimeAdapterKind, RuntimeAdapter>(); | ||
| 154 | |||
| 155 | const register = ( | ||
| 156 | adapter: RuntimeAdapter, | ||
| 157 | ): Effect.Effect<void, RuntimeAdapterRegistryError> => | ||
| 158 | Effect.gen(function* () { | ||
| 159 | if (adaptersByKind.has(adapter.kind)) { | ||
| 160 | return yield* duplicateRuntimeAdapterError(adapter.kind); | ||
| 161 | } | ||
| 162 | |||
| 163 | adaptersByKind.set(adapter.kind, adapter); | ||
| 164 | }); | ||
| 165 | |||
| 166 | const registerAll = ( | ||
| 167 | adapters: ReadonlyArray<RuntimeAdapter>, | ||
| 168 | ): Effect.Effect<void, RuntimeAdapterRegistryError> => | ||
| 169 | Effect.forEach(adapters, register, { discard: true }); | ||
| 170 | |||
| 171 | const get = ( | ||
| 172 | runtimeKind: RuntimeAdapterKind, | ||
| 173 | ): Effect.Effect<RuntimeAdapter, RuntimeAdapterRegistryError> => | ||
| 174 | Effect.gen(function* () { | ||
| 175 | const adapter = adaptersByKind.get(runtimeKind); | ||
| 176 | if (!adapter) { | ||
| 177 | return yield* runtimeAdapterNotFoundError("get", runtimeKind); | ||
| 178 | } | ||
| 179 | |||
| 180 | return adapter; | ||
| 181 | }); | ||
| 182 | |||
| 183 | const execute = ( | ||
| 184 | input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind }, | ||
| 185 | ): Effect.Effect< | ||
| 186 | unknown, | ||
| 187 | RuntimeExecuteError | RuntimeAdapterRegistryError, | ||
| 188 | ToolProviderRegistryService | ||
| 189 | > => | ||
| 190 | Effect.gen(function* () { | ||
| 191 | const adapter = yield* get(input.runtimeKind); | ||
| 192 | return yield* adapter.execute(input); | ||
| 193 | }); | ||
| 194 | |||
| 195 | for (const adapter of initialAdapters) { | ||
| 196 | if (!adaptersByKind.has(adapter.kind)) { | ||
| 197 | adaptersByKind.set(adapter.kind, adapter); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | return { | ||
| 202 | register, | ||
| 203 | registerAll, | ||
| 204 | get, | ||
| 205 | execute, | ||
| 206 | }; | ||
| 207 | }; | ||
| 208 | |||
| 209 | export const RuntimeAdapterRegistryLive = ( | ||
| 210 | initialAdapters: ReadonlyArray<RuntimeAdapter> = [], | ||
| 211 | ): Layer.Layer<RuntimeAdapterRegistryService> => | ||
| 212 | Layer.succeed(RuntimeAdapterRegistryService, makeRuntimeAdapterRegistry(initialAdapters)); | ||
| @@ -1,6 +1,7 @@ | |||
| 1 | import { describe, expect, test } from "bun:test"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { type Source, SourceSchema } from "@executor-v2/schema"; | 2 | import { type Source, SourceSchema } from "@executor-v2/schema"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Either from "effect/Either"; | ||
| 4 | import * as Schema from "effect/Schema"; | 5 | import * as Schema from "effect/Schema"; |
| 5 | 6 | ||
| 6 | import { | 7 | import { |
| @@ -29,103 +30,99 @@ const createOpenApiSource = (): Source => · 22 unmodified lines | |||
| 29 | }); | 30 | }); |
| 30 | 31 | ||
| 31 | describe("makeToolProviderRegistry", () => { | 32 | describe("makeToolProviderRegistry", () => { |
| 32 | test("routes discovery and invocation through provider kind", async () => { | 33 | it.effect("routes discovery and invocation through provider kind", () => |
| 33 | const openApiProvider: ToolProvider = { | 34 | Effect.gen(function* () { |
| 34 | kind: "openapi", | 35 | const openApiProvider: ToolProvider = { |
| 35 | discoverFromSource: (source) => | 36 | kind: "openapi", |
| 36 | Effect.succeed({ | 37 | discoverFromSource: (source) => |
| 37 | sourceHash: "hash_1", | 38 | Effect.succeed({ |
| 38 | tools: [ | 39 | sourceHash: "hash_1", |
| 39 | { | 40 | tools: [ |
| 40 | providerKind: "openapi", | 41 | { |
| 41 | sourceId: source.id, | 42 | providerKind: "openapi", |
| 42 | workspaceId: source.workspaceId, | 43 | sourceId: source.id, |
| 43 | toolId: "getHealth", | 44 | workspaceId: source.workspaceId, |
| 44 | name: "Get health", | 45 | toolId: "getHealth", |
| 45 | description: null, | 46 | name: "Get health", |
| 46 | invocationMode: "http", | 47 | description: null, |
| 47 | availability: "remote_capable", | 48 | invocationMode: "http", |
| 48 | providerPayload: { | 49 | availability: "remote_capable", |
| 49 | method: "get", | 50 | providerPayload: { |
| 50 | pathTemplate: "/healthz", | 51 | method: "get", |
| 51 | parameters: [], | 52 | pathTemplate: "/healthz", |
| 52 | requestBody: null, | 53 | parameters: [], |
| 54 | requestBody: null, | ||
| 55 | }, | ||
| 53 | }, | 56 | }, |
| 54 | }, | 57 | ], |
| 55 | ], | 58 | }), |
| 56 | }), | 59 | invoke: () => |
| 57 | invoke: () => | 60 | Effect.succeed({ |
| 58 | Effect.succeed({ | 61 | output: { ok: true }, |
| 59 | output: { ok: true }, | 62 | isError: false, |
| 60 | isError: false, | 63 | }), |
| 61 | }), | 64 | }; |
| 62 | }; | ||
| 63 | |||
| 64 | const registry = makeToolProviderRegistry([openApiProvider]); | ||
| 65 | |||
| 66 | const source = createOpenApiSource(); | ||
| 67 | const discovery = await Effect.runPromise(registry.discoverFromSource(source)); | ||
| 68 | |||
| 69 | expect(discovery.tools).toHaveLength(1); | ||
| 70 | expect(discovery.tools[0]?.toolId).toBe("getHealth"); | ||
| 71 | |||
| 72 | const result = await Effect.runPromise( | ||
| 73 | registry.invoke({ | ||
| 74 | source, | ||
| 75 | tool: discovery.tools[0]!, | ||
| 76 | args: { verbose: true }, | ||
| 77 | }), | ||
| 78 | ); | ||
| 79 | |||
| 80 | expect(result.isError).toBe(false); | ||
| 81 | expect(result.output).toEqual({ ok: true }); | ||
| 82 | }); | ||
| 83 | |||
| 84 | test("fails on duplicate provider registration", async () => { | ||
| 85 | const openApiProvider: ToolProvider = { | ||
| 86 | kind: "openapi", | ||
| 87 | invoke: () => Effect.succeed({ output: null, isError: false }), | ||
| 88 | }; | ||
| 89 | 65 | ||
| 90 | const registry = makeToolProviderRegistry([openApiProvider]); | 66 | const registry = makeToolProviderRegistry([openApiProvider]); |
| 91 | const result = await Effect.runPromise( | ||
| 92 | Effect.either(registry.register(openApiProvider)), | ||
| 93 | ); | ||
| 94 | 67 | ||
| 95 | if (result._tag === "Right") { | 68 | const source = createOpenApiSource(); |
| 96 | throw new Error("expected duplicate provider registration to fail"); | 69 | const discovery = yield* registry.discoverFromSource(source); |
| 97 | } | ||
| 98 | 70 | ||
| 99 | expect(result.left).toBeInstanceOf(ToolProviderRegistryError); | 71 | expect(discovery.tools).toHaveLength(1); |
| 100 | expect(result.left.message).toContain("Provider already registered"); | 72 | expect(discovery.tools[0]?.toolId).toBe("getHealth"); |
| 101 | }); | ||
| 102 | 73 | ||
| 103 | test("fails when provider cannot discover from source", async () => { | 74 | const result = yield* registry.invoke({ |
| 104 | const mcpProvider: ToolProvider = { | 75 | source, |
| 105 | kind: "mcp", | 76 | tool: discovery.tools[0]!, |
| 106 | invoke: () => | 77 | args: { verbose: true }, |
| 107 | Effect.fail( | 78 | }); |
| 79 | |||
| 80 | expect(result.isError).toBe(false); | ||
| 81 | expect(result.output).toEqual({ ok: true }); | ||
| 82 | }), | ||
| 83 | ); | ||
| 84 | |||
| 85 | it.effect("fails on duplicate provider registration", () => | ||
| 86 | Effect.gen(function* () { | ||
| 87 | const openApiProvider: ToolProvider = { | ||
| 88 | kind: "openapi", | ||
| 89 | invoke: () => Effect.succeed({ output: null, isError: false }), | ||
| 90 | }; | ||
| 91 | |||
| 92 | const registry = makeToolProviderRegistry([openApiProvider]); | ||
| 93 | const result = yield* Effect.either(registry.register(openApiProvider)); | ||
| 94 | |||
| 95 | expect(Either.isLeft(result)).toBe(true); | ||
| 96 | if (Either.isLeft(result)) { | ||
| 97 | expect(result.left).toBeInstanceOf(ToolProviderRegistryError); | ||
| 98 | expect(result.left.message).toContain("Provider already registered"); | ||
| 99 | } | ||
| 100 | }), | ||
| 101 | ); | ||
| 102 | |||
| 103 | it.effect("fails when provider cannot discover from source", () => | ||
| 104 | Effect.gen(function* () { | ||
| 105 | const mcpProvider: ToolProvider = { | ||
| 106 | kind: "mcp", | ||
| 107 | invoke: () => | ||
| 108 | new ToolProviderError({ | 108 | new ToolProviderError({ |
| 109 | operation: "invoke", | 109 | operation: "invoke", |
| 110 | providerKind: "mcp", | 110 | providerKind: "mcp", |
| 111 | message: "not implemented", | 111 | message: "not implemented", |
| 112 | details: null, | 112 | details: null, |
| 113 | }), | 113 | }), |
| 114 | ), | 114 | }; |
| 115 | }; | ||
| 116 | |||
| 117 | const registry = makeToolProviderRegistry([mcpProvider]); | ||
| 118 | const source = createOpenApiSource(); | ||
| 119 | 115 | ||
| 120 | const result = await Effect.runPromise( | 116 | const registry = makeToolProviderRegistry([mcpProvider]); |
| 121 | Effect.either(registry.discoverFromSource(source)), | 117 | const source = createOpenApiSource(); |
| 122 | ); | ||
| 123 | 118 | ||
| 124 | if (result._tag === "Right") { | 119 | const result = yield* Effect.either(registry.discoverFromSource(source)); |
| 125 | throw new Error("expected discovery to fail without provider"); | ||
| 126 | } | ||
| 127 | 120 | ||
| 128 | expect(result.left).toBeInstanceOf(ToolProviderRegistryError); | 121 | expect(Either.isLeft(result)).toBe(true); |
| 129 | expect(result.left.message).toContain("No provider registered"); | 122 | if (Either.isLeft(result)) { |
| 130 | }); | 123 | expect(result.left).toBeInstanceOf(ToolProviderRegistryError); |
| 124 | expect(result.left.message).toContain("No provider registered"); | ||
| 125 | } | ||
| 126 | }), | ||
| 127 | ); | ||
| 131 | }); | 128 | }); |
| @@ -5,4 +5,4 @@ Bare-minimum MCP gateway for Executor v2. · 4 unmodified lines | |||
| 5 | Current scaffold includes: | 5 | Current scaffold includes: |
| 6 | - MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts` | 6 | - MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts` |
| 7 | - streamable HTTP request handling (`handleMcpHttpRequest`) | 7 | - streamable HTTP request handling (`handleMcpHttpRequest`) |
| 8 | - one stub tool: `executor.ping` | 8 | - tools: `executor.ping` and `executor.execute` (the execute tool is host-wired via callback) |
| @@ -1,5 +1,8 @@ | |||
| 1 | export { | 1 | export { |
| 2 | handleMcpHttpRequest, | 2 | handleMcpHttpRequest, |
| 3 | type ExecuteToolInput, | ||
| 4 | type ExecuteToolResult, | ||
| 5 | type GatewayRuntimeKind, | ||
| 3 | type GatewayTarget, | 6 | type GatewayTarget, |
| 4 | type McpGatewayOptions, | 7 | type McpGatewayOptions, |
| 5 | } from "./server"; | 8 | } from "./server"; |
| @@ -4,20 +4,57 @@ import { z } from "zod"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | export type GatewayTarget = "local" | "remote"; | 5 | export type GatewayTarget = "local" | "remote"; |
| 6 | 6 | ||
| 7 | export type GatewayRuntimeKind = | ||
| 8 | | "local-inproc" | ||
| 9 | | "deno-subprocess" | ||
| 10 | | "cloudflare-worker-loader"; | ||
| 11 | |||
| 12 | export type ExecuteToolInput = { | ||
| 13 | code: string; | ||
| 14 | runtimeKind?: GatewayRuntimeKind; | ||
| 15 | }; | ||
| 16 | |||
| 17 | export type ExecuteToolResult = { | ||
| 18 | output?: unknown; | ||
| 19 | error?: string; | ||
| 20 | isError: boolean; | ||
| 21 | }; | ||
| 22 | |||
| 7 | export type McpGatewayOptions = { | 23 | export type McpGatewayOptions = { |
| 8 | target: GatewayTarget; | 24 | target: GatewayTarget; |
| 9 | serverName?: string; | 25 | serverName?: string; |
| 10 | serverVersion?: string; | 26 | serverVersion?: string; |
| 27 | execute?: (input: ExecuteToolInput) => Promise<ExecuteToolResult>; | ||
| 11 | }; | 28 | }; |
| 12 | 29 | ||
| 13 | const DEFAULT_SERVER_NAME = "executor-v2"; | 30 | const DEFAULT_SERVER_NAME = "executor-v2"; |
| 14 | const DEFAULT_SERVER_VERSION = "0.0.0"; | 31 | const DEFAULT_SERVER_VERSION = "0.0.0"; |
| 15 | const STUB_TOOL_NAME = "executor.ping"; | 32 | const STUB_TOOL_NAME = "executor.ping"; |
| 33 | const EXECUTE_TOOL_NAME = "executor.execute"; | ||
| 16 | 34 | ||
| 17 | const PingToolInput = z.object({ | 35 | const PingToolInput = z.object({ |
| 18 | message: z.string().optional(), | 36 | message: z.string().optional(), |
| 19 | }); | 37 | }); |
| 20 | 38 | ||
| 39 | const ExecuteToolInputSchema = z.object({ | ||
| 40 | code: z.string(), | ||
| 41 | runtimeKind: z | ||
| 42 | .enum(["local-inproc", "deno-subprocess", "cloudflare-worker-loader"]) | ||
| 43 | .optional(), | ||
| 44 | }); | ||
| 45 | |||
| 46 | const contentText = (value: unknown): string => { | ||
| 47 | if (typeof value === "string") { | ||
| 48 | return value; | ||
| 49 | } | ||
| 50 | |||
| 51 | try { | ||
| 52 | return JSON.stringify(value, null, 2); | ||
| 53 | } catch { | ||
| 54 | return String(value); | ||
| 55 | } | ||
| 56 | }; | ||
| 57 | |||
| 21 | const createStubMcpServer = (options: McpGatewayOptions): McpServer => { | 58 | const createStubMcpServer = (options: McpGatewayOptions): McpServer => { |
| 22 | const mcp = new McpServer({ | 59 | const mcp = new McpServer({ |
| 23 | name: options.serverName ?? DEFAULT_SERVER_NAME, | 60 | name: options.serverName ?? DEFAULT_SERVER_NAME, |
| @@ -47,6 +84,52 @@ const createStubMcpServer = (options: McpGatewayOptions): McpServer => { · 23 unmodified lines | |||
| 47 | }, | 84 | }, |
| 48 | ); | 85 | ); |
| 49 | 86 | ||
| 87 | mcp.registerTool( | ||
| 88 | EXECUTE_TOOL_NAME, | ||
| 89 | { | ||
| 90 | description: "Execute JavaScript against configured runtime adapter", | ||
| 91 | inputSchema: ExecuteToolInputSchema, | ||
| 92 | }, | ||
| 93 | async (input: ExecuteToolInput) => { | ||
| 94 | if (!options.execute) { | ||
| 95 | return { | ||
| 96 | content: [ | ||
| 97 | { | ||
| 98 | type: "text" as const, | ||
| 99 | text: "executor.execute is not configured for this gateway target", | ||
| 100 | }, | ||
| 101 | ], | ||
| 102 | isError: true, | ||
| 103 | }; | ||
| 104 | } | ||
| 105 | |||
| 106 | try { | ||
| 107 | const result = await options.execute(input); | ||
| 108 | return { | ||
| 109 | content: [ | ||
| 110 | { | ||
| 111 | type: "text" as const, | ||
| 112 | text: result.isError | ||
| 113 | ? result.error ?? "Execution failed" | ||
| 114 | : contentText(result.output), | ||
| 115 | }, | ||
| 116 | ], | ||
| 117 | isError: result.isError, | ||
| 118 | }; | ||
| 119 | } catch (cause) { | ||
| 120 | return { | ||
| 121 | content: [ | ||
| 122 | { | ||
| 123 | type: "text" as const, | ||
| 124 | text: cause instanceof Error ? cause.message : String(cause), | ||
| 125 | }, | ||
| 126 | ], | ||
| 127 | isError: true, | ||
| 128 | }; | ||
| 129 | } | ||
| 130 | }, | ||
| 131 | ); | ||
| 132 | |||
| 50 | return mcp; | 133 | return mcp; |
| 51 | }; | 134 | }; |
| 52 | 135 | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||
| @@ -6,11 +6,16 @@ · 5 unmodified lines | |||
| 6 | ".": "./src/index.ts" | 6 | ".": "./src/index.ts" |
| 7 | }, | 7 | }, |
| 8 | "scripts": { | 8 | "scripts": { |
| 9 | "test": "bunx vitest run --no-watch src/*.test.ts", | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 10 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 10 | }, | 11 | }, |
| 11 | "dependencies": { | 12 | "dependencies": { |
| 12 | "@executor-v2/persistence-ports": "workspace:*", | 13 | "@executor-v2/persistence-ports": "workspace:*", |
| 13 | "@executor-v2/schema": "workspace:*", | 14 | "@executor-v2/schema": "workspace:*", |
| 14 | "effect": "^3.19.8" | 15 | "effect": "^3.19.8" |
| 16 | }, | ||
| 17 | "devDependencies": { | ||
| 18 | "@effect/vitest": "^0.27.0", | ||
| 19 | "vitest": "^3.2.4" | ||
| 15 | } | 20 | } |
| 16 | } | 21 | } |
| @@ -1,73 +1,73 @@ | |||
| 1 | import { describe, expect, test } from "bun:test"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | import { extractOpenApiManifest } from "./openapi-extraction"; | 4 | import { extractOpenApiManifest } from "./openapi-extraction"; |
| 5 | 5 | ||
| 6 | describe("extractOpenApiManifest invocation metadata", () => { | 6 | describe("extractOpenApiManifest invocation metadata", () => { |
| 7 | test("extracts parameter and request body metadata", async () => { | 7 | it.effect("extracts parameter and request body metadata", () => |
| 8 | const openApiSpec = { | 8 | Effect.gen(function* () { |
| 9 | openapi: "3.1.0", | 9 | const openApiSpec = { |
| 10 | paths: { | 10 | openapi: "3.1.0", |
| 11 | "/users/{userId}": { | 11 | paths: { |
| 12 | parameters: [ | 12 | "/users/{userId}": { |
| 13 | { name: "userId", in: "path" }, | ||
| 14 | { name: "x-org-id", in: "header", required: true }, | ||
| 15 | ], | ||
| 16 | get: { | ||
| 17 | operationId: "getUser", | ||
| 18 | summary: "Get user", | ||
| 19 | parameters: [ | 13 | parameters: [ |
| 20 | { name: "include", in: "query" }, | 14 | { name: "userId", in: "path" }, |
| 21 | { name: "x-org-id", in: "header", required: false }, | 15 | { name: "x-org-id", in: "header", required: true }, |
| 22 | ], | 16 | ], |
| 23 | }, | 17 | get: { |
| 24 | post: { | 18 | operationId: "getUser", |
| 25 | summary: "Update user", | 19 | summary: "Get user", |
| 26 | requestBody: { | 20 | parameters: [ |
| 27 | required: true, | 21 | { name: "include", in: "query" }, |
| 28 | content: { | 22 | { name: "x-org-id", in: "header", required: false }, |
| 29 | "application/merge-patch+json": {}, | 23 | ], |
| 30 | "application/json": {}, | 24 | }, |
| 25 | post: { | ||
| 26 | summary: "Update user", | ||
| 27 | requestBody: { | ||
| 28 | required: true, | ||
| 29 | content: { | ||
| 30 | "application/merge-patch+json": {}, | ||
| 31 | "application/json": {}, | ||
| 32 | }, | ||
| 31 | }, | 33 | }, |
| 32 | }, | 34 | }, |
| 33 | }, | 35 | }, |
| 34 | }, | 36 | }, |
| 35 | }, | 37 | }; |
| 36 | }; | ||
| 37 | 38 | ||
| 38 | const manifest = await Effect.runPromise( | 39 | const manifest = yield* extractOpenApiManifest("test-source", openApiSpec); |
| 39 | extractOpenApiManifest("test-source", openApiSpec), | ||
| 40 | ); | ||
| 41 | 40 | ||
| 42 | const getTool = manifest.tools.find((tool) => tool.toolId === "getUser"); | 41 | const getTool = manifest.tools.find((tool) => tool.toolId === "getUser"); |
| 43 | expect(getTool).toBeDefined(); | 42 | expect(getTool).toBeDefined(); |
| 44 | if (!getTool) { | 43 | if (!getTool) { |
| 45 | throw new Error("expected getUser tool"); | 44 | throw new Error("expected getUser tool"); |
| 46 | } | 45 | } |
| 47 | 46 | ||
| 48 | expect(getTool.invocation.method).toBe("get"); | 47 | expect(getTool.invocation.method).toBe("get"); |
| 49 | expect(getTool.invocation.pathTemplate).toBe("/users/{userId}"); | 48 | expect(getTool.invocation.pathTemplate).toBe("/users/{userId}"); |
| 50 | expect(getTool.invocation.requestBody).toBeNull(); | 49 | expect(getTool.invocation.requestBody).toBeNull(); |
| 51 | 50 | ||
| 52 | expect(getTool.invocation.parameters).toEqual( | 51 | expect(getTool.invocation.parameters).toEqual( |
| 53 | expect.arrayContaining([ | 52 | expect.arrayContaining([ |
| 54 | { name: "userId", location: "path", required: true }, | 53 | { name: "userId", location: "path", required: true }, |
| 55 | { name: "x-org-id", location: "header", required: false }, | 54 | { name: "x-org-id", location: "header", required: false }, |
| 56 | { name: "include", location: "query", required: false }, | 55 | { name: "include", location: "query", required: false }, |
| 57 | ]), | 56 | ]), |
| 58 | ); | 57 | ); |
| 59 | 58 | ||
| 60 | const postTool = manifest.tools.find( | 59 | const postTool = manifest.tools.find( |
| 61 | (tool) => tool.toolId === "post_users_userid", | 60 | (tool) => tool.toolId === "post_users_userid", |
| 62 | ); | 61 | ); |
| 63 | expect(postTool).toBeDefined(); | 62 | expect(postTool).toBeDefined(); |
| 64 | if (!postTool) { | 63 | if (!postTool) { |
| 65 | throw new Error("expected post tool"); | 64 | throw new Error("expected post tool"); |
| 66 | } | 65 | } |
| 67 | 66 | ||
| 68 | expect(postTool.invocation.requestBody).toEqual({ | 67 | expect(postTool.invocation.requestBody).toEqual({ |
| 69 | required: true, | 68 | required: true, |
| 70 | contentTypes: ["application/json", "application/merge-patch+json"], | 69 | contentTypes: ["application/json", "application/merge-patch+json"], |
| 71 | }); | 70 | }); |
| 72 | }); | 71 | }), |
| 72 | ); | ||
| 73 | }); | 73 | }); |
| @@ -45,17 +45,17 @@ export class OpenApiExtractionError extends Data.TaggedError("OpenApiExtractionE · 44 unmodified lines | |||
| 45 | details: string | null; | 45 | details: string | null; |
| 46 | }> {} | 46 | }> {} |
| 47 | 47 | ||
| 48 | export const ExtractedToolParameterSchema = OpenApiToolParameterSchema; | 48 | const ExtractedToolParameterSchema = OpenApiToolParameterSchema; |
| 49 | export const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema; | 49 | const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema; |
| 50 | export const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema; | 50 | const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema; |
| 51 | export const ExtractedToolSchema = OpenApiExtractedToolSchema; | 51 | const ExtractedToolSchema = OpenApiExtractedToolSchema; |
| 52 | export const ToolManifestSchema = OpenApiToolManifestSchema; | 52 | const ToolManifestSchema = OpenApiToolManifestSchema; |
| 53 | 53 | ||
| 54 | export type ExtractedToolParameter = OpenApiToolParameter; | 54 | type ExtractedToolParameter = OpenApiToolParameter; |
| 55 | export type ExtractedToolRequestBody = OpenApiToolRequestBody; | 55 | type ExtractedToolRequestBody = OpenApiToolRequestBody; |
| 56 | export type ExtractedToolInvocation = OpenApiInvocationPayload; | 56 | type ExtractedToolInvocation = OpenApiInvocationPayload; |
| 57 | export type ExtractedTool = OpenApiExtractedTool; | 57 | type ExtractedTool = OpenApiExtractedTool; |
| 58 | export type ToolManifest = OpenApiToolManifest; | 58 | type ToolManifest = OpenApiToolManifest; |
| 59 | 59 | ||
| 60 | const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema); | 60 | const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema); |
| 61 | const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema); | 61 | const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema); |
| @@ -75,7 +75,7 @@ export type RefreshOpenApiArtifactResult = { · 13 unmodified lines | |||
| 75 | reused: boolean; | 75 | reused: boolean; |
| 76 | }; | 76 | }; |
| 77 | 77 | ||
| 78 | export type RefreshOpenApiArtifactInput = { | 78 | type RefreshOpenApiArtifactInput = { |
| 79 | source: Source; | 79 | source: Source; |
| 80 | openApiSpec: unknown; | 80 | openApiSpec: unknown; |
| 81 | artifactStore: ToolArtifactStore; | 81 | artifactStore: ToolArtifactStore; |
| @@ -0,0 +1 @@ | |||
| 1 | export {}; | ||