executor generate: export the tool catalog as OpenAPI (or a typed TS client), backed by REST tool invocation#1256
executor generate: export the tool catalog as OpenAPI (or a typed TS client), backed by REST tool invocation#1256RhysSullivan wants to merge 2 commits into
Conversation
Running executor generate against an instance writes one self-contained
TypeScript file: a dependency-free Proxy-based runtime client plus full
input/output types for every visible tool, so
client.github.org.main.issues.create({ title }) is typed end to end and
executes through the server's /api/executions endpoint (auth, policies,
approvals included).
New pieces:
- tools.export SDK surface + GET /tools/export: the bulk schema-bearing
catalog read (schemas + trimmed shared $defs, grouped per connection,
policy-filtered), one request regardless of catalog size.
- compileToolChunkTypeScript: compiles many tools per compiler pass. Per-tool
passes pay fixed overhead thousands of times and one whole-catalog pass is
super-linear (30s+ at 10k tools); 200-tool chunks generate a 10k-tool
catalog in ~400ms.
- generateToolProxySource: assembles the generated file (type-only namespaces
per connection, ExecutorTools path tree, embedded runtime).
- executor generate CLI command with --output/--integration/--connection/
--include-static, reusing the server-target and auth machinery.
Tested down to the wire: generated source is typechecked with the real
compiler (valid calls accept, invalid reject), the transpiled runtime is
imported and run against a fake fetch (completed/paused/injection paths),
and a scale test ingests a 10,000-operation OpenAPI spec through addSpec and
proves export + generate + strict typecheck hold up with regression
tripwires on time.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | b409a57 | Commit Preview URL Branch Preview URL |
Jul 02 2026, 05:44 AM |
Cloudflare preview
Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | b409a57 | Jul 02 2026, 05:43 AM |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Greptile SummaryThis PR introduces
Confidence Score: 5/5Safe to merge; all changed paths are well-tested and the new REST surface correctly routes through the existing execution engine for auth, policies, and approvals. The implementation is thorough and well-tested end-to-end (unit, integration, and 10k-scale tests). The three findings are all non-blocking: the x-forwarded-proto multi-value issue only corrupts the servers[0].url field in the generated document on multi-proxy deployments, the OpenAPI required-field mismatch only affects strict client-side response validators, and the stale comment is cosmetic. None affect invocation correctness, security, or data integrity. packages/core/sdk/src/specgen.ts (response schema shape) and packages/core/api/src/handlers/tools.ts (serverUrlFromRequest) are worth a second look before shipping to production deployments behind multi-layer proxies. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI as executor generate (CLI)
participant API as GET /tools/export
participant Executor as executor.tools.export()
participant DB as Storage (tool + definition rows)
participant Typegen as generateToolProxySource / generateOpenApiSpec
participant FS as File System
CLI->>API: "GET /tools/export?integration=..."
API->>Executor: toolsExport(filter)
Executor->>DB: findMany("tool", where)
DB-->>Executor: tool rows (with schemas)
Executor->>DB: findMany("definition", ...) [per connection group]
DB-->>Executor: $defs rows
Executor-->>API: ToolCatalogExport (grouped, trimmed $defs)
API-->>CLI: catalog JSON
CLI->>Typegen: generateOpenApiSpec(catalog)
Typegen-->>CLI: OpenAPI 3.1 document
CLI->>FS: write executor.openapi.json
CLI->>Typegen: generateToolProxySource(catalog)
Note over Typegen: compileToolChunkTypeScript in ~200-tool chunks
Typegen-->>CLI: TypeScript source (self-contained)
CLI->>FS: write executor.gen.ts
participant Client as Generated Client
participant Invoke as POST /tools/invoke/:path
participant Engine as ExecutionEngine
Client->>Invoke: POST /api/tools/invoke/github.org.main.issues.create
Invoke->>Engine: executeWithPause(buildInvokeCode(path, args))
Engine-->>Invoke: completed or paused
Invoke-->>Client: "{ok: true, data} or {ok: false, error}"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CLI as executor generate (CLI)
participant API as GET /tools/export
participant Executor as executor.tools.export()
participant DB as Storage (tool + definition rows)
participant Typegen as generateToolProxySource / generateOpenApiSpec
participant FS as File System
CLI->>API: "GET /tools/export?integration=..."
API->>Executor: toolsExport(filter)
Executor->>DB: findMany("tool", where)
DB-->>Executor: tool rows (with schemas)
Executor->>DB: findMany("definition", ...) [per connection group]
DB-->>Executor: $defs rows
Executor-->>API: ToolCatalogExport (grouped, trimmed $defs)
API-->>CLI: catalog JSON
CLI->>Typegen: generateOpenApiSpec(catalog)
Typegen-->>CLI: OpenAPI 3.1 document
CLI->>FS: write executor.openapi.json
CLI->>Typegen: generateToolProxySource(catalog)
Note over Typegen: compileToolChunkTypeScript in ~200-tool chunks
Typegen-->>CLI: TypeScript source (self-contained)
CLI->>FS: write executor.gen.ts
participant Client as Generated Client
participant Invoke as POST /tools/invoke/:path
participant Engine as ExecutionEngine
Client->>Invoke: POST /api/tools/invoke/github.org.main.issues.create
Invoke->>Engine: executeWithPause(buildInvokeCode(path, args))
Engine-->>Invoke: completed or paused
Invoke-->>Client: "{ok: true, data} or {ok: false, error}"
Reviews (2): Last reviewed commit: "Make executor generate OpenAPI-first, ba..." | Re-trigger Greptile |
| ); | ||
| console.log(""); | ||
| console.log("Use it:"); | ||
| console.log(` import { createExecutorClient } from "./${path.basename(output, ".ts")}";`); |
There was a problem hiding this comment.
When
output contains a subdirectory (e.g., src/executor.gen.ts), path.basename strips the directory, giving ./executor.gen instead of ./src/executor.gen. Using path.relative(process.cwd(), outputPath) without the .ts extension produces the correct relative import path from wherever the user runs the CLI.
| console.log(` import { createExecutorClient } from "./${path.basename(output, ".ts")}";`); | |
| const relImport = path.relative(process.cwd(), outputPath).replace(/\.ts$/, ""); | |
| console.log(` import { createExecutorClient } from "./${relImport}";`); |
The primary artifact is now an OpenAPI 3.1 document (executor.openapi.json
by default) describing every visible tool as one REST operation, so people
feed it to whatever client generator they already use (openapi-typescript,
openapi-generator, Kiota, ...) instead of being locked to our emitted
client. --format typescript keeps the self-contained TypeScript client;
--format both writes both.
The document's operations are real endpoints:
- POST /tools/invoke/{path} invokes one tool directly over HTTP through the
execution engine: 404 for unknown tools, uniform ok/error envelope, and
approval-gated calls return code execution_paused with an executionId and
resumePath (autoApprove=true opts the caller in as approver).
- GET /tools/export/openapi serves the live document, servers[0] derived
from the request so generated clients dial the right base.
The generated TypeScript client now calls the invoke endpoint too (instead
of posting execution code), so both artifact flavors speak the same wire
surface.
Scale requirement updated to its real shape: 10,000+ tools accumulated
across MANY specs, not one giant spec. The scale test ingests real service
specs from @executor-js/emulate emulators (github, stripe) by URL plus a
synthetic fleet topping the catalog past 10k, then proves the combined
export, OpenAPI generation, and TypeScript generation hold up, and that the
real openapi-typescript generator accepts the document with all 10k+
operations present.
New integration coverage in apps/local runs the invoke + export endpoints
over real HTTP handlers end to end (spec registered, connection created,
upstream echo server dialed with the rendered credential).
What
executor generateturns an Executor instance into a typed API proxy. The primary artifact is an OpenAPI 3.1 document describing every visible tool as one REST operation, so you plug your catalog into whatever client generator you already use:executor generate # writes executor.openapi.json npx openapi-typescript executor.openapi.json -o executor-api.tsThe same document is served live at
GET /api/tools/export/openapi(bearer auth), and its operations are real endpoints:POST /api/tools/invoke/{tool path}runs the tool through Executor's execution engine, so credentials, policies, and approvals all apply. Responses are a uniform envelope:{ ok: true, data }or{ ok: false, error }; approval-gated calls returnerror.code: "execution_paused"with anexecutionIdandresumePath; unknown tools answer 404.Prefer a ready-made client?
--format typescriptemits a single self-contained TypeScript file (dependency-free Proxy runtime + full per-tool input/output types), and--format bothwrites both artifacts. The TS client speaks the same invoke endpoint the OpenAPI document describes.How
tools.export+GET /tools/export: bulk schema-bearing catalog read (input/output JSON schemas plus each connection's shared$defstrimmed to the referenced subset, policy-filtered). One request regardless of catalog size.generateOpenApiSpec(sdk): catalog → OpenAPI 3.1. One POST operation per tool at/tools/invoke/{path}, request body from the tool's input schema, envelope responses, shared$defshoisted once intocomponents.schemasunder per-connection namespaces (a 10k catalog references each shared schema once instead of inlining 10k copies), unique identifier-safe operationIds, per-connection tags.POST /tools/invoke/:path: direct REST invocation through the execution engine (same pause/approval semantics as every other host surface), with a sandbox sentinel so a missing tool surfaces as HTTP 404 instead of a buried execution error string.compileToolChunkTypeScript(sdk): compiles ~200 tools per json-schema-to-typescript pass for the TS artifact. Whole-catalog single passes are super-linear (30s+ at 10k tools); chunking does the same work in ~400ms, and a failing chunk retries tool-by-tool so one broken schema degrades tounknown.executor generate --format openapi|typescript|both(default openapi) with--output,--integration,--connection,--include-static, on the existing server-target/auth machinery.Scale (per the requirement: 10,000+ tools toward the total, multiple specs, emulate)
The scale test builds the catalog the way real instances do: many specs, not one giant one. It registers real service specs served by
@executor-js/emulateemulators (github, stripe) by URL — the exact path a user adding those integrations takes — plus a synthetic fleet of 8 specs topping the combined catalog past 10,000 tools. Then, over the combined catalog:tools.exportreturns everything in one read (~150ms),generateOpenApiSpecemits the document with one path per tool,openapi-typescriptgenerator consumes that document and its output contains all 10k+ invoke paths — third-party interop proven, not assumed,generateToolProxySourceemits the TS client and the whole file typechecks under--strict, including a consumer snippet against a mid-fleet tool.Time tripwires on export/spec-gen/ts-gen catch any regression to per-tool or whole-catalog compilation.
Testing
packages/core/sdk/src/specgen.test.ts: operation shapes, namespaced component refs, cross-connection name isolation, operationId dedupe.packages/core/sdk/src/typegen.test.ts: generated TS typechecked with the real compiler (valid calls accept, wrong types reject); the transpiled runtime run against a fake fetch (ok unwrap, paused error with approval URL, path-injection rejected before any request); naming edge cases;tools.exportgrouping/trimming/policy filtering.apps/local/src/tools-invoke.test.ts: invoke + export endpoints over real HTTP handlers end to end — spec registered, connection created, a real upstream echo server dialed with the rendered credential; pause-with-resume-coordinates; 404; non-object body rejection; served document'sservers[0]derivation.packages/plugins/openapi/src/sdk/typegen-scale.test.ts: the multi-spec 10k scale test above.openapi-typescripton it, and made a spec-driven call (base URL and path taken from the document itself) that returned real data overPOST /api/tools/invoke/....Full gates run: format:check, lint, typecheck, test. Pre-existing flake note:
host-selfhost'sscope-isolation.test.tstimeout fails identically on a clean checkout without this branch.