History

feat: wire PM execute through runtime adapters and align v2 test/tooling

Rhys Sullivan committed 8 months ago

Commit aa5b69e1 parent f6167a735 files · +829 −258Browse files

Unified · Split

35 files
  1. v2/PLAN.md +25 −10
  2. v2/apps/cli/src/main.ts +1 −0
  3. v2/apps/convex/src/main.ts +1 −0
  4. v2/apps/pm/README.md +2 −1
  5. v2/apps/pm/package.json +3 −1
  6. v2/apps/pm/src/main.ts +67 −1
  7. v2/apps/web/src/main.ts +1 −0
  8. v2/bun.lock +8 −1
  9. v2/knip.json +3 −0
  10. v2/package.json +2 −1
  11. v2/packages/ai-sdk-adapter/src/index.ts +1 −0
  12. v2/packages/approvals/src/index.ts +1 −0
  13. v2/packages/confect/package.json +0 −1
  14. v2/packages/confect/src/rpc/server.ts +0 −1
  15. v2/packages/confect/src/rpc/telemetry.ts +1 −1
  16. v2/packages/domain/src/index.ts +1 −0
  17. v2/packages/engine/README.md +1 −0
  18. v2/packages/engine/package.json +4 −2
  19. v2/packages/engine/src/deno-subprocess-runner.test.ts +38 −38
  20. v2/packages/engine/src/index.ts +16 −0
  21. v2/packages/engine/src/openapi-execution.test.ts +98 −46
  22. v2/packages/engine/src/runtime-adapters.test.ts +96 −0
  23. v2/packages/engine/src/runtime-adapters.ts +212 −0
  24. v2/packages/engine/src/tool-providers.test.ts +83 −86
  25. v2/packages/mcp-gateway/README.md +1 −1
  26. v2/packages/mcp-gateway/src/index.ts +3 −0
  27. v2/packages/mcp-gateway/src/server.ts +83 −0
  28. v2/packages/oauth/src/index.ts +1 −0
  29. v2/packages/rpc/src/index.ts +1 −0
  30. v2/packages/sdk/src/index.ts +1 −0
  31. v2/packages/secrets/src/index.ts +1 −0
  32. v2/packages/source-manager/package.json +5 −0
  33. v2/packages/source-manager/src/openapi-extraction.test.ts +55 −55
  34. v2/packages/source-manager/src/openapi-extraction.ts +12 −12
  35. v2/packages/sync/src/index.ts +1 −0
v2/PLAN.md+25−10
@@ -47,6 +47,12 @@ v2 changes the default: · 46 unmodified lines
4747 - `cloud` and `self-hosted` are not separate architecture branches.
4848 - They are both **remote Convex targets**; only URL/auth/deployment ownership differs.
4949
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+
5056 ---
5157
5258 ## 4) Control + execution model
@@ -391,27 +397,36 @@ Implemented in `v2` so far: · 338 unmodified lines
391397 - monorepo app/package skeleton
392398 - `confect` imported from quickhub
393399 - schema package scaffold with Effect models and event envelope
400+- schema-driven runtime tool models (canonical descriptor + OpenAPI invocation/manifest)
394401 - domain ID groundwork in `schema/src/ids.ts`
395402 - Convex table ID groundwork in `persistence-convex/src/convex-ids.ts`
396403 - shared persistence ports for `SourceStore` and `ToolArtifactStore`
397404 - local persistence adapters for source/tool artifacts
398405 - local-only snapshot/WAL contract and persistence implementation
399406 - 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
401415
402416 Not implemented yet:
403−- provider registry and canonical tool descriptor routing in engine/runtime
404−- execute lifecycle through PM beyond stub MCP tool
405−- MCP/GraphQL providers and in-memory tool registration/invocation path
417+- `tools.executor.sources.add/list/remove` end-to-end PM + MCP gateway wiring
418+- approval adapter/state machine integration (pending/resume/deny persistence) in v2 runtime 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
406422 - full rpc/sdk adapter wiring and sync engine
407423
408424 ---
409425
410426 ## 19) Immediate next steps
411427
412−1. Define canonical provider tool descriptor + provider service contracts (`discover` / `invoke`).
413−2. Implement engine-side provider registry and routing for tool invocation.
414−3. Wire minimal `apps/pm` execute flow (single run lifecycle) using provider routing.
415−4. Implement first end-to-end providers: `openapi` (network) and `in_memory` (AI-SDK style).
416−5. Add first `tools.executor.sources.add/list/remove` control operations in PM gateway.
417−6. Add provider conformance tests and one vertical test: OpenAPI spec -> manifest -> execute tool call.
428+1. Add first `tools.executor.sources.add/list/remove` control operations end-to-end in PM + MCP gateway.
429+2. Implement approval adapter contract with persisted pending/resume/deny flow for runtime tool calls.
430+3. Add provider/runtime conformance tests (invoke success/failure, timeout, pending approval, cancel).
431+4. Add Cloudflare worker loader adapter integration against the runtime adapter contract.
432+5. Extend PM execute lifecycle from single-run scaffold to persisted run state + streaming status.
v2/apps/cli/src/main.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/apps/convex/src/main.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/apps/pm/README.md+2−1
@@ -5,4 +5,5 @@ Local Process Manager app scaffold for Executor v2. · 4 unmodified lines
55 Current scaffold includes:
66 - local MCP endpoint at `POST /mcp`
77 - 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)
v2/apps/pm/package.json+3−1
@@ -8,6 +8,8 @@ · 7 unmodified lines
88 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
99 },
1010 "dependencies": {
11− "@executor-v2/mcp-gateway": "workspace:*"
11+ "@executor-v2/engine": "workspace:*",
12+ "@executor-v2/mcp-gateway": "workspace:*",
13+ "effect": "^3.19.8"
1214 }
1315 }
v2/apps/pm/src/main.ts+67−1
@@ -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";
216
317 const port = Number(Bun.env.PORT ?? 8787);
418
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+
570 const handleMcp = async (request: Request): Promise<Response> =>
671 handleMcpHttpRequest(request, {
772 target: "local",
873 serverName: "executor-v2-pm",
974 serverVersion: "0.0.0",
75+ execute: executeTool,
1076 });
1177
1278 const server = Bun.serve({
v2/apps/web/src/main.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/bun.lock+8−1
@@ -25,7 +25,9 @@ · 24 unmodified lines
2525 "apps/pm": {
2626 "name": "@executor-v2/app-pm",
2727 "dependencies": {
28+ "@executor-v2/engine": "workspace:*",
2829 "@executor-v2/mcp-gateway": "workspace:*",
30+ "effect": "^3.19.8",
2931 },
3032 },
3133 "apps/web": {
@@ -50,7 +52,6 @@ · 18 unmodified lines
5052 "next": "^16.1.6",
5153 },
5254 "devDependencies": {
53− "@effect/vitest": "^0.27.0",
5455 "@types/node": "^20.19.9",
5556 "typescript": "^5.9.2",
5657 "vitest": "^3.2.4",
@@ -67,7 +68,9 @@ · 10 unmodified lines
6768 "effect": "^3.19.8",
6869 },
6970 "devDependencies": {
71+ "@effect/vitest": "^0.27.0",
7072 "@executor-v2/source-manager": "workspace:*",
73+ "vitest": "^3.2.4",
7174 },
7275 },
7376 "packages/mcp-gateway": {
@@ -102,6 +105,10 @@ · 28 unmodified lines
102105 "@executor-v2/schema": "workspace:*",
103106 "effect": "^3.19.8",
104107 },
108+ "devDependencies": {
109+ "@effect/vitest": "^0.27.0",
110+ "vitest": "^3.2.4",
111+ },
105112 },
106113 "packages/state-contracts": {
107114 "name": "@executor-v2/persistence-ports",
v2/knip.json+3−0
@@ -0,0 +1,3 @@
1+{
2+ "$schema": "https://unpkg.com/knip@latest/schema.json"
3+}
v2/package.json+2−1
@@ -9,7 +9,8 @@ · 8 unmodified lines
99 ],
1010 "scripts": {
1111 "prepare": "effect-language-service patch",
12− "typecheck": "tsc --noEmit"
12+ "typecheck": "tsc --noEmit",
13+ "knip": "bunx knip --config knip.json"
1314 },
1415 "devDependencies": {
1516 "@effect/language-service": "^0.76.0",
v2/packages/ai-sdk-adapter/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/approvals/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/confect/package.json+0−1
@@ -27,7 +27,6 @@ · 26 unmodified lines
2727 "next": "^16.1.6"
2828 },
2929 "devDependencies": {
30− "@effect/vitest": "^0.27.0",
3130 "@types/node": "^20.19.9",
3231 "typescript": "^5.9.2",
3332 "vitest": "^3.2.4"
v2/packages/confect/src/rpc/server.ts+0−1
@@ -44,7 +44,6 @@ export { Rpc, RpcGroup, RpcMiddleware }; · 43 unmodified lines
4444
4545 export {
4646 WrapperTypeId,
47− type WrapperTypeId as WrapperTypeIdType,
4847 type Wrapper,
4948 isWrapper,
5049 wrap,
v2/packages/confect/src/rpc/telemetry.ts+1−1
@@ -9,7 +9,7 @@ export const convexRequestIdHeader = "convex-request-id"; · 8 unmodified lines
99
1010 export type RpcClientKind = "query" | "mutation" | "action";
1111
12−export interface RpcTelemetryContext {
12+interface RpcTelemetryContext {
1313 readonly traceparent: string;
1414 readonly b3?: string;
1515 readonly tracestate?: string;
v2/packages/domain/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/engine/README.md+1−0
@@ -8,4 +8,5 @@ Current scaffold includes: · 7 unmodified lines
88 - OpenAPI provider invocation and manifest-to-descriptor conversion helpers
99 - minimal in-process JavaScript runner with `tools.*` proxy dispatch into provider registry
1010 - 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)
1112 - vertical integration test covering OpenAPI extraction -> descriptor conversion -> code execution -> HTTP call
v2/packages/engine/package.json+4−2
@@ -6,7 +6,7 @@ · 5 unmodified lines
66 ".": "./src/index.ts"
77 },
88 "scripts": {
9− "test": "bun test src/*.test.ts",
9+ "test": "bunx vitest run --no-watch src/*.test.ts",
1010 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
1111 },
1212 "dependencies": {
@@ -15,6 +15,8 @@ · 2 unmodified lines
1515 "effect": "^3.19.8"
1616 },
1717 "devDependencies": {
18− "@executor-v2/source-manager": "workspace:*"
18+ "@effect/vitest": "^0.27.0",
19+ "@executor-v2/source-manager": "workspace:*",
20+ "vitest": "^3.2.4"
1921 }
2022 }
v2/packages/engine/src/deno-subprocess-runner.test.ts+38−38
@@ -1,9 +1,10 @@
1+import { describe, expect, it } from "@effect/vitest";
12 import {
23 CanonicalToolDescriptorSchema,
34 type CanonicalToolDescriptor,
45 } from "@executor-v2/schema";
5−import { describe, expect, test } from "bun:test";
66 import * as Effect from "effect/Effect";
7+import * as Either from "effect/Either";
78 import * as Schema from "effect/Schema";
89
910 import {
@@ -33,30 +34,30 @@ const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor · 23 unmodified lines
3334 });
3435
3536 describe("executeJavaScriptInDenoSubprocess", () => {
36− test("runs code in Deno subprocess and proxies tool calls", async () => {
37− const sumProvider: ToolProvider = {
38− kind: "in_memory",
39− invoke: (input) =>
40− Effect.gen(function* () {
41− if (input.tool.toolId !== "sum") {
37+ it.effect("runs code in Deno subprocess and proxies tool calls", () =>
38+ Effect.gen(function* () {
39+ const sumProvider: ToolProvider = {
40+ kind: "in_memory",
41+ invoke: (input) =>
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 };
4251 return {
43− output: `unknown tool: ${input.tool.toolId}`,
44− isError: true,
52+ output: args.a + args.b,
53+ isError: false,
4554 } as const;
46− }
47−
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+ }),
56+ };
5557
56− const registry = makeToolProviderRegistry([sumProvider]);
58+ const registry = makeToolProviderRegistry([sumProvider]);
5759
58− const result = await Effect.runPromise(
59− executeJavaScriptInDenoSubprocess({
60+ const result = yield* executeJavaScriptInDenoSubprocess({
6061 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",
6162 tools: [
6263 {
@@ -65,31 +66,30 @@ describe("executeJavaScriptInDenoSubprocess", () => { · 2 unmodified lines
6566 },
6667 ],
6768 timeoutMs: 10_000,
68− }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),
69− );
69+ }).pipe(Effect.provideService(ToolProviderRegistryService, registry));
7070
71− expect(result).toBe(5);
72− });
71+ expect(result).toBe(5);
72+ }),
73+ );
7374
74− test("returns a typed error when Deno executable is missing", async () => {
75− const registry = makeToolProviderRegistry([]);
75+ it.effect("returns a typed error when Deno executable is missing", () =>
76+ Effect.gen(function* () {
77+ const registry = makeToolProviderRegistry([]);
7678
77− const result = await Effect.runPromise(
78− Effect.either(
79+ const result = yield* Effect.either(
7980 executeJavaScriptInDenoSubprocess({
8081 code: "return 1;",
8182 tools: [],
8283 denoExecutable: "/definitely-missing-deno-binary",
8384 timeoutMs: 1_000,
8485 }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),
85− ),
86− );
87−
88− if (result._tag === "Right") {
89− throw new Error("expected missing Deno executable to fail");
90− }
86+ );
9187
92− expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError);
93− expect(result.left.operation).toBe("spawn");
94− });
88+ expect(Either.isLeft(result)).toBe(true);
89+ if (Either.isLeft(result)) {
90+ expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError);
91+ expect(result.left.operation).toBe("spawn");
92+ }
93+ }),
94+ );
9595 });
v2/packages/engine/src/index.ts+16−0
@@ -11,6 +11,22 @@ export { · 10 unmodified lines
1111 type ExecuteJavaScriptInput,
1212 type RunnableTool,
1313 } 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";
1430 export {
1531 makeOpenApiToolProvider,
1632 openApiToolDescriptorsFromManifest,
v2/packages/engine/src/openapi-execution.test.ts+98−46
@@ -1,9 +1,10 @@
1−import {
2− type ToolArtifactStore,
3−} from "@executor-v2/persistence-ports";
1+import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2+
3+import { describe, expect, it } from "@effect/vitest";
4+import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
45 import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";
56 import { makeSourceManagerService } from "@executor-v2/source-manager";
6−import { describe, expect, test } from "bun:test";
7+import * as Data from "effect/Data";
78 import * as Effect from "effect/Effect";
89 import * as Option from "effect/Option";
910 import * as Schema from "effect/Schema";
@@ -27,51 +28,104 @@ type TestServer = { · 17 unmodified lines
2728 query: string;
2829 apiKey: string | null;
2930 }>;
30− server: Bun.Server<unknown>;
31+ close: () => Promise<void>;
3132 };
3233
33−const makeTestServer = Effect.acquireRelease(
34− Effect.sync<TestServer>(() => {
35− const requests: TestServer["requests"] = [];
36−
37− const server = Bun.serve({
38− port: 0,
39− fetch: async (request) => {
40− const url = new URL(request.url);
41−
42− if (url.pathname === "/users/u123") {
43− requests.push({
44− path: url.pathname,
45− query: url.search,
46− apiKey: request.headers.get("x-api-key"),
47− });
34+class TestServerReleaseError extends Data.TaggedError("TestServerReleaseError")<{
35+ message: string;
36+}> {}
37+
38+const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => {
39+ res.statusCode = statusCode;
40+ res.setHeader("content-type", "application/json");
41+ res.end(JSON.stringify(body));
42+};
43+
44+const getHeaderValue = (
45+ req: IncomingMessage,
46+ key: string,
47+): string | null => {
48+ const value = req.headers[key];
49+ if (typeof value === "string") {
50+ return value;
51+ }
4852
49− return Response.json({
50− id: "u123",
51− verbose: url.searchParams.get("verbose") === "true",
52− apiKey: request.headers.get("x-api-key"),
53+ if (Array.isArray(value)) {
54+ return value[0] ?? null;
55+ }
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+ }),
53112 });
54− }
55−
56− return Response.json({ error: "not found" }, { status: 404 });
57− },
58− });
59−
60− return {
61− baseUrl: `http://127.0.0.1:${server.port}`,
62− requests,
63− server,
64− };
65− }),
66− ({ server }) =>
67− Effect.sync(() => {
68− server.stop(true);
69− }),
113+ });
114+ })
115+ ),
116+ (testServer) =>
117+ Effect.tryPromise({
118+ try: () => testServer.close(),
119+ catch: (cause) =>
120+ new TestServerReleaseError({
121+ message: cause instanceof Error ? cause.message : String(cause),
122+ }),
123+ }).pipe(Effect.orDie),
70124 );
71125
72126 describe("OpenAPI execution vertical slice", () => {
73− test("extracts OpenAPI tools and executes code against provider", async () => {
74− const program = Effect.gen(function* () {
127+ it.scoped("extracts OpenAPI tools and executes code against provider", () =>
128+ Effect.gen(function* () {
75129 const testServer = yield* makeTestServer;
76130
77131 const openApiSpec = {
@@ -185,8 +239,6 @@ return await tools.getUser({ · 107 unmodified lines
185239 expect(testServer.requests[0]?.path).toBe("/users/u123");
186240 expect(testServer.requests[0]?.query).toBe("?verbose=true");
187241 expect(testServer.requests[0]?.apiKey).toBe("sk_test");
188− });
189−
190− await Effect.runPromise(program.pipe(Effect.scoped));
191− });
242+ }),
243+ );
192244 });
v2/packages/engine/src/runtime-adapters.test.ts+96−0
@@ -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+});
v2/packages/engine/src/runtime-adapters.ts+212−0
@@ -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));
v2/packages/engine/src/tool-providers.test.ts+83−86
@@ -1,6 +1,7 @@
1−import { describe, expect, test } from "bun:test";
1+import { describe, expect, it } from "@effect/vitest";
22 import { type Source, SourceSchema } from "@executor-v2/schema";
33 import * as Effect from "effect/Effect";
4+import * as Either from "effect/Either";
45 import * as Schema from "effect/Schema";
56
67 import {
@@ -29,103 +30,99 @@ const createOpenApiSource = (): Source => · 22 unmodified lines
2930 });
3031
3132 describe("makeToolProviderRegistry", () => {
32− test("routes discovery and invocation through provider kind", async () => {
33− const openApiProvider: ToolProvider = {
34− kind: "openapi",
35− discoverFromSource: (source) =>
36− Effect.succeed({
37− sourceHash: "hash_1",
38− tools: [
39− {
40− providerKind: "openapi",
41− sourceId: source.id,
42− workspaceId: source.workspaceId,
43− toolId: "getHealth",
44− name: "Get health",
45− description: null,
46− invocationMode: "http",
47− availability: "remote_capable",
48− providerPayload: {
49− method: "get",
50− pathTemplate: "/healthz",
51− parameters: [],
52− requestBody: null,
33+ it.effect("routes discovery and invocation through provider kind", () =>
34+ Effect.gen(function* () {
35+ const openApiProvider: ToolProvider = {
36+ kind: "openapi",
37+ discoverFromSource: (source) =>
38+ Effect.succeed({
39+ sourceHash: "hash_1",
40+ tools: [
41+ {
42+ providerKind: "openapi",
43+ sourceId: source.id,
44+ workspaceId: source.workspaceId,
45+ toolId: "getHealth",
46+ name: "Get health",
47+ description: null,
48+ invocationMode: "http",
49+ availability: "remote_capable",
50+ providerPayload: {
51+ method: "get",
52+ pathTemplate: "/healthz",
53+ parameters: [],
54+ requestBody: null,
55+ },
5356 },
54− },
55− ],
56− }),
57− invoke: () =>
58− Effect.succeed({
59− output: { ok: true },
60− isError: false,
61− }),
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− };
57+ ],
58+ }),
59+ invoke: () =>
60+ Effect.succeed({
61+ output: { ok: true },
62+ isError: false,
63+ }),
64+ };
8965
90− const registry = makeToolProviderRegistry([openApiProvider]);
91− const result = await Effect.runPromise(
92− Effect.either(registry.register(openApiProvider)),
93− );
66+ const registry = makeToolProviderRegistry([openApiProvider]);
9467
95− if (result._tag === "Right") {
96− throw new Error("expected duplicate provider registration to fail");
97− }
68+ const source = createOpenApiSource();
69+ const discovery = yield* registry.discoverFromSource(source);
9870
99− expect(result.left).toBeInstanceOf(ToolProviderRegistryError);
100− expect(result.left.message).toContain("Provider already registered");
101− });
71+ expect(discovery.tools).toHaveLength(1);
72+ expect(discovery.tools[0]?.toolId).toBe("getHealth");
10273
103− test("fails when provider cannot discover from source", async () => {
104− const mcpProvider: ToolProvider = {
105− kind: "mcp",
106− invoke: () =>
107− Effect.fail(
74+ const result = yield* registry.invoke({
75+ source,
76+ tool: discovery.tools[0]!,
77+ args: { verbose: true },
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: () =>
108108 new ToolProviderError({
109109 operation: "invoke",
110110 providerKind: "mcp",
111111 message: "not implemented",
112112 details: null,
113113 }),
114− ),
115− };
116−
117− const registry = makeToolProviderRegistry([mcpProvider]);
118− const source = createOpenApiSource();
114+ };
119115
120− const result = await Effect.runPromise(
121− Effect.either(registry.discoverFromSource(source)),
122− );
116+ const registry = makeToolProviderRegistry([mcpProvider]);
117+ const source = createOpenApiSource();
123118
124− if (result._tag === "Right") {
125− throw new Error("expected discovery to fail without provider");
126− }
119+ const result = yield* Effect.either(registry.discoverFromSource(source));
127120
128− expect(result.left).toBeInstanceOf(ToolProviderRegistryError);
129− expect(result.left.message).toContain("No provider registered");
130− });
121+ expect(Either.isLeft(result)).toBe(true);
122+ if (Either.isLeft(result)) {
123+ expect(result.left).toBeInstanceOf(ToolProviderRegistryError);
124+ expect(result.left.message).toContain("No provider registered");
125+ }
126+ }),
127+ );
131128 });
v2/packages/mcp-gateway/README.md+1−1
@@ -5,4 +5,4 @@ Bare-minimum MCP gateway for Executor v2. · 4 unmodified lines
55 Current scaffold includes:
66 - MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts`
77 - 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)
v2/packages/mcp-gateway/src/index.ts+3−0
@@ -1,5 +1,8 @@
11 export {
22 handleMcpHttpRequest,
3+ type ExecuteToolInput,
4+ type ExecuteToolResult,
5+ type GatewayRuntimeKind,
36 type GatewayTarget,
47 type McpGatewayOptions,
58 } from "./server";
v2/packages/mcp-gateway/src/server.ts+83−0
@@ -4,20 +4,57 @@ import { z } from "zod"; · 3 unmodified lines
44
55 export type GatewayTarget = "local" | "remote";
66
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+
723 export type McpGatewayOptions = {
824 target: GatewayTarget;
925 serverName?: string;
1026 serverVersion?: string;
27+ execute?: (input: ExecuteToolInput) => Promise<ExecuteToolResult>;
1128 };
1229
1330 const DEFAULT_SERVER_NAME = "executor-v2";
1431 const DEFAULT_SERVER_VERSION = "0.0.0";
1532 const STUB_TOOL_NAME = "executor.ping";
33+const EXECUTE_TOOL_NAME = "executor.execute";
1634
1735 const PingToolInput = z.object({
1836 message: z.string().optional(),
1937 });
2038
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+
2158 const createStubMcpServer = (options: McpGatewayOptions): McpServer => {
2259 const mcp = new McpServer({
2360 name: options.serverName ?? DEFAULT_SERVER_NAME,
@@ -47,6 +84,52 @@ const createStubMcpServer = (options: McpGatewayOptions): McpServer => { · 23 unmodified lines
4784 },
4885 );
4986
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+
50133 return mcp;
51134 };
52135
v2/packages/oauth/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/rpc/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/sdk/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/secrets/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};
v2/packages/source-manager/package.json+5−0
@@ -6,11 +6,16 @@ · 5 unmodified lines
66 ".": "./src/index.ts"
77 },
88 "scripts": {
9+ "test": "bunx vitest run --no-watch src/*.test.ts",
910 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
1011 },
1112 "dependencies": {
1213 "@executor-v2/persistence-ports": "workspace:*",
1314 "@executor-v2/schema": "workspace:*",
1415 "effect": "^3.19.8"
16+ },
17+ "devDependencies": {
18+ "@effect/vitest": "^0.27.0",
19+ "vitest": "^3.2.4"
1520 }
1621 }
v2/packages/source-manager/src/openapi-extraction.test.ts+55−55
@@ -1,73 +1,73 @@
1−import { describe, expect, test } from "bun:test";
1+import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
33
44 import { extractOpenApiManifest } from "./openapi-extraction";
55
66 describe("extractOpenApiManifest invocation metadata", () => {
7− test("extracts parameter and request body metadata", async () => {
8− const openApiSpec = {
9− openapi: "3.1.0",
10− paths: {
11− "/users/{userId}": {
12− parameters: [
13− { name: "userId", in: "path" },
14− { name: "x-org-id", in: "header", required: true },
15− ],
16− get: {
17− operationId: "getUser",
18− summary: "Get user",
7+ it.effect("extracts parameter and request body metadata", () =>
8+ Effect.gen(function* () {
9+ const openApiSpec = {
10+ openapi: "3.1.0",
11+ paths: {
12+ "/users/{userId}": {
1913 parameters: [
20− { name: "include", in: "query" },
21− { name: "x-org-id", in: "header", required: false },
14+ { name: "userId", in: "path" },
15+ { name: "x-org-id", in: "header", required: true },
2216 ],
23− },
24− post: {
25− summary: "Update user",
26− requestBody: {
27− required: true,
28− content: {
29− "application/merge-patch+json": {},
30− "application/json": {},
17+ get: {
18+ operationId: "getUser",
19+ summary: "Get user",
20+ parameters: [
21+ { name: "include", in: "query" },
22+ { name: "x-org-id", in: "header", required: false },
23+ ],
24+ },
25+ post: {
26+ summary: "Update user",
27+ requestBody: {
28+ required: true,
29+ content: {
30+ "application/merge-patch+json": {},
31+ "application/json": {},
32+ },
3133 },
3234 },
3335 },
3436 },
35− },
36− };
37+ };
3738
38− const manifest = await Effect.runPromise(
39− extractOpenApiManifest("test-source", openApiSpec),
40− );
39+ const manifest = yield* extractOpenApiManifest("test-source", openApiSpec);
4140
42− const getTool = manifest.tools.find((tool) => tool.toolId === "getUser");
43− expect(getTool).toBeDefined();
44− if (!getTool) {
45− throw new Error("expected getUser tool");
46− }
41+ const getTool = manifest.tools.find((tool) => tool.toolId === "getUser");
42+ expect(getTool).toBeDefined();
43+ if (!getTool) {
44+ throw new Error("expected getUser tool");
45+ }
4746
48− expect(getTool.invocation.method).toBe("get");
49− expect(getTool.invocation.pathTemplate).toBe("/users/{userId}");
50− expect(getTool.invocation.requestBody).toBeNull();
47+ expect(getTool.invocation.method).toBe("get");
48+ expect(getTool.invocation.pathTemplate).toBe("/users/{userId}");
49+ expect(getTool.invocation.requestBody).toBeNull();
5150
52− expect(getTool.invocation.parameters).toEqual(
53− expect.arrayContaining([
54− { name: "userId", location: "path", required: true },
55− { name: "x-org-id", location: "header", required: false },
56− { name: "include", location: "query", required: false },
57− ]),
58− );
51+ expect(getTool.invocation.parameters).toEqual(
52+ expect.arrayContaining([
53+ { name: "userId", location: "path", required: true },
54+ { name: "x-org-id", location: "header", required: false },
55+ { name: "include", location: "query", required: false },
56+ ]),
57+ );
5958
60− const postTool = manifest.tools.find(
61− (tool) => tool.toolId === "post_users_userid",
62− );
63− expect(postTool).toBeDefined();
64− if (!postTool) {
65− throw new Error("expected post tool");
66− }
59+ const postTool = manifest.tools.find(
60+ (tool) => tool.toolId === "post_users_userid",
61+ );
62+ expect(postTool).toBeDefined();
63+ if (!postTool) {
64+ throw new Error("expected post tool");
65+ }
6766
68− expect(postTool.invocation.requestBody).toEqual({
69− required: true,
70− contentTypes: ["application/json", "application/merge-patch+json"],
71− });
72− });
67+ expect(postTool.invocation.requestBody).toEqual({
68+ required: true,
69+ contentTypes: ["application/json", "application/merge-patch+json"],
70+ });
71+ }),
72+ );
7373 });
v2/packages/source-manager/src/openapi-extraction.ts+12−12
@@ -45,17 +45,17 @@ export class OpenApiExtractionError extends Data.TaggedError("OpenApiExtractionE · 44 unmodified lines
4545 details: string | null;
4646 }> {}
4747
48−export const ExtractedToolParameterSchema = OpenApiToolParameterSchema;
49−export const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema;
50−export const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema;
51−export const ExtractedToolSchema = OpenApiExtractedToolSchema;
52−export const ToolManifestSchema = OpenApiToolManifestSchema;
53−
54−export type ExtractedToolParameter = OpenApiToolParameter;
55−export type ExtractedToolRequestBody = OpenApiToolRequestBody;
56−export type ExtractedToolInvocation = OpenApiInvocationPayload;
57−export type ExtractedTool = OpenApiExtractedTool;
58−export type ToolManifest = OpenApiToolManifest;
48+const ExtractedToolParameterSchema = OpenApiToolParameterSchema;
49+const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema;
50+const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema;
51+const ExtractedToolSchema = OpenApiExtractedToolSchema;
52+const ToolManifestSchema = OpenApiToolManifestSchema;
53+
54+type ExtractedToolParameter = OpenApiToolParameter;
55+type ExtractedToolRequestBody = OpenApiToolRequestBody;
56+type ExtractedToolInvocation = OpenApiInvocationPayload;
57+type ExtractedTool = OpenApiExtractedTool;
58+type ToolManifest = OpenApiToolManifest;
5959
6060 const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema);
6161 const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema);
@@ -75,7 +75,7 @@ export type RefreshOpenApiArtifactResult = { · 13 unmodified lines
7575 reused: boolean;
7676 };
7777
78−export type RefreshOpenApiArtifactInput = {
78+type RefreshOpenApiArtifactInput = {
7979 source: Source;
8080 openApiSpec: unknown;
8181 artifactStore: ToolArtifactStore;
v2/packages/sync/src/index.ts+1−0
@@ -0,0 +1 @@
1+export {};