Skip to content

executor generate: export the tool catalog as OpenAPI (or a typed TS client), backed by REST tool invocation#1256

Open
RhysSullivan wants to merge 2 commits into
mainfrom
typed-api-proxy
Open

executor generate: export the tool catalog as OpenAPI (or a typed TS client), backed by REST tool invocation#1256
RhysSullivan wants to merge 2 commits into
mainfrom
typed-api-proxy

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

executor generate turns 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.ts

The 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 return error.code: "execution_paused" with an executionId and resumePath; unknown tools answer 404.

Prefer a ready-made client? --format typescript emits a single self-contained TypeScript file (dependency-free Proxy runtime + full per-tool input/output types), and --format both writes both artifacts. The TS client speaks the same invoke endpoint the OpenAPI document describes.

import { createExecutorClient } from "./executor.gen";
const executor = createExecutorClient();
const created = await executor.github.org.main.issues.create({ title: "Hi" });
if (created.ok) console.log(created.data.number); // fully typed

How

  • tools.export + GET /tools/export: bulk schema-bearing catalog read (input/output JSON schemas plus each connection's shared $defs trimmed 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 $defs hoisted once into components.schemas under 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 to unknown.
  • CLI: 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/emulate emulators (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.export returns everything in one read (~150ms),
  • generateOpenApiSpec emits the document with one path per tool,
  • the real openapi-typescript generator consumes that document and its output contains all 10k+ invoke paths — third-party interop proven, not assumed,
  • generateToolProxySource emits 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.export grouping/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's servers[0] derivation.
  • packages/plugins/openapi/src/sdk/typegen-scale.test.ts: the multi-spec 10k scale test above.
  • Verified live against a running daemon: generated the OpenAPI document via CLI, ran openapi-typescript on it, and made a spec-driven call (base URL and path taken from the document itself) that returned real data over POST /api/tools/invoke/....

Full gates run: format:check, lint, typecheck, test. Pre-existing flake note: host-selfhost's scope-isolation.test.ts timeout fails identically on a clean checkout without this branch.

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.
@mintlify

mintlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
executor 🟢 Ready View Preview Jul 2, 2026, 4:45 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Console https://executor-preview-pr-1256.executor-e2e.workers.dev
MCP https://executor-preview-pr-1256.executor-e2e.workers.dev/mcp
Deployed commit b409a57

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud b409a57 Jul 02 2026, 05:43 AM

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1256

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1256

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1256

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1256

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1256

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1256

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1256

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1256

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1256

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1256

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1256

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1256

executor

npm i https://pkg.pr.new/executor@1256

commit: b409a57

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces executor generate, which exports the tool catalog as either an OpenAPI 3.1 document or a ready-made self-contained TypeScript proxy client. The implementation spans a new GET /tools/export + GET /tools/export/openapi + POST /tools/invoke/:path REST surface, a chunked schema-compilation pipeline (compileToolChunkTypeScript) that keeps generation fast past 10k tools, a Proxy-based runtime template embedded verbatim in the generated file (zero dependencies), and a tools.export bulk read on the executor that avoids per-tool schema round trips.

  • New REST endpoints (handlers/tools.ts, tools/api.ts): export returns the schema-bearing catalog in one read; openapi derives a live OpenAPI document from it; invoke executes a tool by dotted path, routing through the execution engine so approval gates, policies, and credentials all apply.
  • TypeScript codegen (typegen.ts, schema-types.ts): chunked compiler passes with per-chunk error fallback keep generation time sub-second at catalog scale; the generated file is a single self-contained module with typed namespaces per connection, an ExecutorTools path-tree interface, and a dependency-free Proxy runtime.
  • OpenAPI codegen (specgen.ts): hoists shared $defs into namespaced components.schemas, produces one POST operation per tool, validated end-to-end against the real openapi-typescript generator in the scale test.

Confidence Score: 5/5

Safe 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

Filename Overview
packages/core/sdk/src/typegen.ts New file: generates a self-contained TypeScript client with a Proxy-based runtime and type-safe path-tree interface; one stale comment says the client calls /api/executions when it actually calls /api/tools/invoke/.
packages/core/api/src/handlers/tools.ts Adds export, openapi, and invoke REST handlers; serverUrlFromRequest may produce a malformed server URL when x-forwarded-proto contains multiple comma-separated values from a multi-proxy chain.
packages/core/sdk/src/specgen.ts New file: generates an OpenAPI 3.1 document from the tool catalog; the success-response schema marks data as required but the invoke handler can return { ok: true } without data, which would fail strict client-side validation.
packages/core/sdk/src/schema-types.ts Adds compileToolChunkTypeScript: compiles many tools per compiler pass using a single wrapped object schema, then extracts per-tool types via a forward-scan offset; relies on compiler insertion-order emission (documented in a comment).
packages/core/sdk/src/executor.ts Adds toolsExport: bulk schema-bearing catalog read, policy-filtered, grouped per connection with trimmed $defs; issues one findMany("definition") query per non-static connection group (N+1 pattern at connection scale).
apps/cli/src/main.ts Adds executor generate command with --format, --output, --integration, --connection, --include-static flags; the import hint uses path.basename which strips subdirectories from the output path (already flagged in a prior review).
packages/core/api/src/tools/api.ts Adds export, openapi, and invoke endpoint definitions to ToolsApi; the invoke payload is Schema.Unknown with handler-level shape validation, which is appropriate for dynamically-typed tool inputs.
apps/local/src/tools-invoke.test.ts New integration test: exercises the full invoke/export HTTP surface over a real in-process handler with a real upstream echo server — covers ok, paused, 404, and invalid-body cases.

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}"
Loading
%%{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}"
Loading

Reviews (2): Last reviewed commit: "Make executor generate OpenAPI-first, ba..." | Re-trigger Greptile

Comment thread apps/cli/src/main.ts Outdated
);
console.log("");
console.log("Use it:");
console.log(` import { createExecutorClient } from "./${path.basename(output, ".ts")}";`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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).
@RhysSullivan RhysSullivan changed the title Add executor generate: typed TypeScript client for the tool catalog executor generate: export the tool catalog as OpenAPI (or a typed TS client), backed by REST tool invocation Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant