# Executor Documentation

> Reference for Executor, the open-source integration layer that exposes one tool catalog across CLI, HTTP API, MCP, and TypeScript SDK runtimes. Covers local daemon, hosted cloud, self-host deployments, credentials, policies, and code execution.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052)
- [GitHub repository](https://github.com/RhysSullivan/executor)

## Repository Metadata

- Repository: RhysSullivan/executor

- Generated: 2026-06-23T19:29:14.099Z
- Updated: 2026-06-23T19:30:14.094Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 24

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/01-overview.md) - What Executor exposes (CLI, web UI, HTTP API, MCP, SDK), runtime forms (local, cloud, self-host), and the shortest path from install to first tool call.
- 02. [Installation](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/02-installation.md) - Install the published CLI globally, bootstrap a development checkout, and verify the background service starts with expected ports and data directories.
- 03. [Quickstart](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/03-quickstart.md) - Run `executor install`, open the web UI, add a first integration, search and call a tool, and resume a paused execution.
- 04. [MCP proxy](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/04-mcp-proxy.md) - How Executor acts as a single MCP endpoint in front of OpenAPI, GraphQL, and upstream MCP integrations with shared auth and per-tool policies.
- 05. [Tools](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/05-tools.md) - Tool addresses, discovery, schema inspection, invocation paths, and how tools are produced per connection across CLI, HTTP API, and MCP surfaces.
- 06. [Integrations](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/06-integrations.md) - Tenant-level catalog identities for OpenAPI specs, GraphQL endpoints, MCP servers, and plugin-registered sources; detection, registration, and auth method descriptors.
- 07. [Connections](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/07-connections.md) - Owner-scoped credentials bound to integrations, credential provider resolution, OAuth minting, and the `(owner, integration, name)` identity model.
- 08. [Policies](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/08-policies.md) - Per-tool allow, require-approval, and block actions; pattern matching, effective policy resolution, and default annotations derived from integration specs.
- 09. [Executions](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/09-executions.md) - Code-mode execution via QuickJS, paused states for auth and approval, `execute` and `resume` HTTP routes, and MCP elicitation handling.
- 10. [Add integrations](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/10-add-integrations.md) - Add OpenAPI, GraphQL, and MCP sources from the web UI or CLI, including spec URLs, namespaces, base URLs, and post-add verification with `tools sources`.
- 11. [Connect MCP clients](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/11-connect-mcp-clients.md) - Wire Cursor, Claude Code, OpenCode, and other MCP clients via stdio (`executor mcp`) or streamable HTTP, including `add-mcp` setup and client restart requirements.
- 12. [Configure credentials](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/12-configure-credentials.md) - Credential providers (file secrets, keychain, 1Password, encrypted stores), connection creation payloads, OAuth flows, and placement-based auth templates.
- 13. [Manage policies](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/13-manage-policies.md) - Create and update owner-scoped tool policies, interpret effective policy precedence, and handle approval pauses from the web UI, MCP, and CLI resume flow.
- 14. [Embed with the SDK](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/14-embed-with-the-sdk.md) - Compose `createExecutor` with plugins and credential providers, register integrations, create connections, list and invoke tools, and shut down cleanly in application code.
- 15. [Deploy self-hosted](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/15-deploy-self-hosted.md) - Run Executor in Docker or on Cloudflare Workers: image defaults, volume mounts, bootstrap admin env vars, TLS/public-origin requirements, and sandbox network constraints.
- 16. [Executor Cloud](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/16-executor-cloud.md) - Hosted Executor Cloud entry points, sign-in flow, shared MCP endpoint usage, and how cloud deployments differ from local daemon and self-host runtimes.
- 17. [CLI reference](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/17-cli-reference.md) - All `executor` subcommands and flags: `install`, `web`, `daemon`, `service`, `mcp`, `call`, `resume`, `tools`, `server`, `login`, and auto-start behavior.
- 18. [HTTP API reference](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/18-http-api-reference.md) - Core Executor HTTP API groups: `tools`, `integrations`, `connections`, `providers`, `executions`, `oauth`, and `policies` routes, payloads, and error shapes.
- 19. [SDK reference](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/19-sdk-reference.md) - `@executor-js/sdk` exports: `createExecutor`, plugin wiring, tool listing and invocation, connection APIs, policy helpers, typed IDs, and promise-mode entry points.
- 20. [Configuration reference](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/20-configuration-reference.md) - Environment variables and runtime paths: `EXECUTOR_DATA_DIR`, `EXECUTOR_SCOPE_DIR`, `EXECUTOR_WEB_BASE_URL`, secret keys, bootstrap admin, ports, and client-specific overrides.
- 21. [SDK quickstart example](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/21-sdk-quickstart-example.md) - Walkthrough of `examples/docs-sdk-quickstart`: in-memory credential provider, OpenAPI `addSpec`, connection creation, tool listing, schema inspection, and shutdown.
- 22. [Plugin catalog](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/22-plugin-catalog.md) - Published protocol and provider plugins (OpenAPI, GraphQL, MCP, file-secrets, keychain, 1Password, Google, WorkOS Vault) and how `examples/all-plugins` wires them together.
- 23. [Develop locally](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/23-develop-locally.md) - Monorepo bootstrap, turbo dev servers, package boundaries, targeted Vitest runs, e2e boot recipes, and release-check scripts for contributors.
- 24. [Troubleshooting](https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/24-troubleshooting.md) - Common failure modes: daemon port conflicts, unreachable local server, OAuth login state behind proxies, stale Vite caches, missing bootstrap builds, and recovery commands.

## Source File Index

- `AGENTS.md`
- `apps/cli/bin/executor.ts`
- `apps/cli/package.json`
- `apps/cli/src/daemon-state.ts`
- `apps/cli/src/daemon.ts`
- `apps/cli/src/installation.ts`
- `apps/cli/src/integrations.ts`
- `apps/cli/src/local-server-manifest.ts`
- `apps/cli/src/main.ts`
- `apps/cli/src/server-connection.ts`
- `apps/cli/src/server-profile.ts`
- `apps/cli/src/service.ts`
- `apps/cloud/executor.config.ts`
- `apps/cloud/package.json`
- `apps/docs/concepts/connections.mdx`
- `apps/docs/concepts/integrations.mdx`
- `apps/docs/concepts/policies.mdx`
- `apps/docs/hosted/cloud.mdx`
- `apps/docs/hosted/cloudflare.mdx`
- `apps/docs/hosted/docker.mdx`
- `apps/docs/index.mdx`
- `apps/docs/local/cli.mdx`
- `apps/docs/mcp-proxy.mdx`
- `apps/host-cloudflare/executor.config.ts`
- `apps/host-cloudflare/wrangler.jsonc`
- `apps/host-selfhost/.env.example`
- `apps/host-selfhost/docker-compose.yml`
- `apps/host-selfhost/Dockerfile`
- `apps/host-selfhost/executor.config.ts`
- `apps/local/package.json`
- `e2e/AGENTS.md`
- `e2e/setup/cloud.globalsetup.ts`
- `e2e/src/ports.ts`
- `examples/all-plugins/package.json`
- `examples/all-plugins/src/main.ts`
- `examples/docs-sdk-quickstart/package.json`
- `examples/docs-sdk-quickstart/src/main.ts`
- `package.json`
- `packages/core/api/src/api.ts`
- `packages/core/api/src/connections/api.ts`
- `packages/core/api/src/executions/api.ts`
- `packages/core/api/src/handlers/connections.ts`
- `packages/core/api/src/handlers/executions.ts`
- `packages/core/api/src/handlers/integrations.ts`
- `packages/core/api/src/handlers/oauth.ts`
- `packages/core/api/src/handlers/policies.ts`
- `packages/core/api/src/handlers/tools.ts`
- `packages/core/api/src/integrations/api.ts`
- `packages/core/api/src/oauth/api.ts`
- `packages/core/api/src/policies/api.ts`
- `packages/core/api/src/server/mcp-build.ts`
- `packages/core/api/src/tools/api.ts`
- `packages/core/execution/src/engine.ts`
- `packages/core/integrations-registry/package.json`
- `packages/core/sdk/package.json`
- `packages/core/sdk/src/connection.ts`
- `packages/core/sdk/src/elicitation.ts`
- `packages/core/sdk/src/errors.ts`
- `packages/core/sdk/src/executor.ts`
- `packages/core/sdk/src/http-auth.ts`
- `packages/core/sdk/src/ids.ts`
- `packages/core/sdk/src/integration.ts`
- `packages/core/sdk/src/oauth-service.ts`
- `packages/core/sdk/src/policies.ts`
- `packages/core/sdk/src/promise.ts`
- `packages/core/sdk/src/public-origin.ts`
- `packages/hosts/mcp/src/browser-approval-store.ts`
- `packages/hosts/mcp/src/browser-approval.ts`
- `packages/hosts/mcp/src/index.ts`
- `packages/hosts/mcp/src/tool-server.ts`
- `packages/kernel/runtime-quickjs/src/index.ts`
- `packages/plugins/file-secrets/package.json`
- `packages/plugins/file-secrets/src/sdk/plugin.ts`
- `packages/plugins/graphql/package.json`
- `packages/plugins/graphql/src/sdk/plugin.ts`
- `packages/plugins/keychain/src/sdk/plugin.ts`
- `packages/plugins/mcp/package.json`
- `packages/plugins/mcp/src/sdk/plugin.ts`
- `packages/plugins/onepassword/src/sdk/plugin.ts`
- `packages/plugins/openapi/package.json`
- `packages/plugins/openapi/README.md`
- `packages/plugins/openapi/src/promise.ts`
- `packages/plugins/openapi/src/sdk/plugin.ts`
- `README.md`
- `RELEASING.md`
- `RUNNING.md`
- `scripts/bootstrap.ts`
- `scripts/generate-doc-snippets.ts`
- `scripts/reap-dev-servers.ts`
- `turbo.json`
- `vision.md`
- `vitest.config.ts`

---

## 01. Overview

> What Executor exposes (CLI, web UI, HTTP API, MCP, SDK), runtime forms (local, cloud, self-host), and the shortest path from install to first tool call.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/01-overview.md
- Generated: 2026-06-23T19:21:51.500Z

### Source Files

- `README.md`
- `vision.md`
- `apps/docs/index.mdx`
- `package.json`
- `AGENTS.md`

---
title: "Overview"
description: "What Executor exposes (CLI, web UI, HTTP API, MCP, SDK), runtime forms (local, cloud, self-host), and the shortest path from install to first tool call."
---

Executor is an open-source integration layer: one catalog for every tool, shared across every agent you use. Configure integrations once (OpenAPI specs, GraphQL endpoints, upstream MCP servers), attach credentials and per-tool policies, then reach the same catalog from a CLI, web UI, HTTP API, MCP endpoint, or embedded SDK.

Executor is not AI-specific and not code-mode specific. It is a category of source and a way to interop between them. The product ships primitives (tools, integrations, connections, policies, plugins) that hosts compose into local, cloud, or self-hosted runtimes.

## Core concepts

| Concept | What it is | Identity |
| --- | --- | --- |
| **Tool** | A callable operation with optional JSON input and output schemas | Address: `tools.<integration>.<owner>.<connection>.<tool>` |
| **Integration** | Tenant-level catalog entry for an API surface (OpenAPI, GraphQL, MCP, plugin-registered source) | `slug` (integration slug) |
| **Connection** | Owner-scoped credential bound to one integration; produces that integration's tools | `(owner, integration, name)` |
| **Policy** | Per-tool gate: allow, require approval, or block | Owner-scoped pattern + action |

Tools are produced per connection. An integration can have many connections. Policies start from sensible defaults derived from the imported spec (for example, GET operations on an OpenAPI integration are allowed by default) and can be overridden per owner.

## What Executor exposes

All runtimes share the same core HTTP API groups and the same React web console. Hosts add plugin-specific routes on top.

| Surface | Entry point | Typical use |
| --- | --- | --- |
| **CLI** | `executor` global binary | Install a background service, open the web UI, call tools, manage daemon/service lifecycle |
| **Web UI** | `executor web` or hosted origin | Add integrations, configure connections, manage policies, approve paused executions |
| **HTTP API** | `{origin}/api` | Programmatic catalog, connection, execution, and policy management |
| **MCP** | `executor mcp` (stdio) or `{origin}/mcp` (streamable HTTP) | Single MCP endpoint in front of all integrations for Cursor, Claude Code, and other MCP clients |
| **SDK** | `@executor-js/sdk` (`createExecutor`) | Embed Executor in application code with plugins and credential providers |

### CLI

The published `executor` package is the primary local entry point. Root subcommands include:

| Command | Purpose |
| --- | --- |
| `install` | Register and start the OS-supervised background service |
| `web` | Open the running web UI (or start a foreground runtime with `--foreground`) |
| `mcp` | Start an MCP server over stdio |
| `call` | Invoke a tool by path segments |
| `resume` | Resume a paused execution (auth or approval) |
| `tools` | Search, list sources, and describe tool schemas |
| `daemon` | Manage the local daemon (`run`, `status`, `stop`, `restart`) |
| `service` | Manage the OS service unit (`install`, `uninstall`, `status`, `restart`) |
| `server` | Manage CLI server connection profiles (local or remote) |
| `login` / `logout` / `whoami` | Device login against hosted Executor |

`executor call`, `executor resume`, and `executor tools` auto-start a local daemon when needed. If the default port is busy, the CLI picks an available local port and records it in the server manifest.

### HTTP API

The core API (`ExecutorApi`) is composed from these groups:

| Group | Routes (prefix) | Responsibility |
| --- | --- | --- |
| `tools` | `/tools`, `/tools/schema` | List and inspect tool metadata and schemas |
| `integrations` | `/integrations` | Tenant catalog: detect, register, list, remove integrations |
| `connections` | `/connections` | Owner-scoped credentials bound to integrations |
| `providers` | `/providers` | Credential provider discovery and item browsing |
| `executions` | `/executions` | Code-mode execute and resume (QuickJS); paused state for auth and approval |
| `oauth` | `/oauth` | OAuth client and token flows |
| `policies` | `/policies` | Owner-scoped tool policies |

Protocol plugins (OpenAPI, GraphQL, MCP, and others) register additional route groups on the same API surface.

### MCP proxy

Executor acts as a single MCP endpoint in front of OpenAPI, GraphQL, and upstream MCP integrations. Agents connect to Executor; Executor attaches connection credentials, enforces policies, and forwards calls upstream. Credentials stay in Executor, not in the agent sandbox.

Two transport options:

- **stdio**: `executor mcp` (launched by the MCP client)
- **streamable HTTP**: `{origin}/mcp` (shown on the Connect card in the web UI)

Use `npx add-mcp` to write client config. Most MCP clients load servers at startup, so restart the client after adding Executor.

### SDK

`@executor-js/sdk` exports `createExecutor`, plugin wiring, typed IDs (`IntegrationSlug`, `ToolAddress`, `Owner`, and others), connection and tool APIs, and policy helpers. A promise-mode entry point (`@executor-js/sdk/promise`) is available for scripts. See `examples/docs-sdk-quickstart` for a minimal end-to-end walkthrough.

## Runtime forms

All forms expose the same functionality, packaged for different deployment models.

| Form | Package / entry | Data location | Best for |
| --- | --- | --- | --- |
| **Local CLI** | `executor` + `@executor-js/local` | `~/.executor` (or `EXECUTOR_DATA_DIR`) | Headless or server environments; durable background service |
| **Desktop app** | `@executor-js/desktop` | Same `~/.executor` path as CLI | Native Mac, Windows, Linux with a GUI |
| **Executor Cloud** | `@executor-js/cloud` (Cloudflare Workers) | Hosted Postgres + blob storage | Fastest start; share catalog across cloud and local agents |
| **Self-host (Docker)** | `@executor-js/host-selfhost` | SQLite under `EXECUTOR_DATA_DIR` | Full control on your infrastructure |
| **Self-host (Cloudflare)** | `@executor-js/host-cloudflare` | Cloudflare Durable Objects / storage | Edge-deployed multi-tenant hosting |

Local CLI and desktop share state on the same machine via the same data directory, so integrations, connections, and policies configured in one are visible in the other.

The supervised background service binds loopback by default (port `4789` unless overridden). Clients discover the live origin and bearer token from `server.json` under the data directory.

## Architecture

```mermaid
flowchart TB
  subgraph clients [Clients]
    CLI[CLI]
    Web[Web UI]
    MCP[MCP clients]
    App[Embedded SDK app]
  end

  subgraph executor [Executor runtime]
    API[HTTP API /api]
    MCPsrv[MCP /mcp]
    Core[createExecutor + plugins]
    Exec[QuickJS executions]
  end

  subgraph upstream [Upstream integrations]
    OAS[OpenAPI APIs]
    GQL[GraphQL endpoints]
    UMCP[Upstream MCP servers]
  end

  CLI --> API
  Web --> API
  MCP --> MCPsrv
  App --> Core
  API --> Core
  MCPsrv --> Core
  Core --> Exec
  Core --> OAS
  Core --> GQL
  Core --> UMCP
```

Monorepo package roles:

- `packages/core/sdk`: contracts, plugin wiring, scopes, secrets, policies
- `packages/core/api`: HTTP API definition and server composition
- `packages/plugins/*`: protocol and credential provider plugins
- `packages/hosts/mcp`: MCP serving envelope (`/mcp`)
- `packages/kernel/*`: execution runtimes (QuickJS, dynamic worker)
- `apps/local`, `apps/cloud`, `apps/cli`, `apps/desktop`: product entry points

Published plugins include OpenAPI, GraphQL, MCP, file-secrets, keychain, 1Password, Google, Microsoft, encrypted-secrets, and WorkOS Vault (cloud).

## Shortest path to a first tool call

<Steps>
  <Step title="Install the CLI">

Requires Node.js 20 or newer.

<CodeGroup>

```bash npm
npm install -g executor
```

```bash pnpm
pnpm add -g executor
```

```bash bun
bun add -g executor
```

</CodeGroup>

  </Step>
  <Step title="Start the background service">

```bash
executor install
```

This registers Executor with the OS service manager (launchd, systemd, or Task Scheduler) so the HTTP runtime survives restarts. For a temporary foreground server instead:

```bash
executor web --foreground
```

  </Step>
  <Step title="Open the web UI">

```bash
executor web
```

The CLI prints a signed-in URL (with `?_token=` when bearer auth is enabled) and opens your browser.

  </Step>
  <Step title="Add an integration">

In the web UI, go to **Add Source** and paste an OpenAPI, GraphQL, or MCP URL. Executor detects the type, indexes tools, and surfaces auth method descriptors.

Or from the CLI:

```bash
executor call executor openapi addSource '{
  "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
  "namespace": "petstore",
  "baseUrl": "https://petstore3.swagger.io/api/v3"
}'
```

  </Step>
  <Step title="Call a tool">

Verify tools are indexed:

```bash
executor tools sources
executor tools search "list pets"
```

Invoke by path:

```bash
executor call petstore findPetsByStatus '{"status":"available"}'
```

If execution pauses for auth or approval:

```bash
executor resume --execution-id <id>
```

  </Step>
  <Step title="Optional: connect an MCP client">

```bash
npx add-mcp "executor mcp" --name executor
```

Or use the streamable HTTP endpoint from the Connect card:

```bash
npx add-mcp http://127.0.0.1:<port>/mcp --transport http --name executor
```

Restart the MCP client so Executor tools appear.

  </Step>
</Steps>

## Configuration essentials

| Variable / path | Default | Purpose |
| --- | --- | --- |
| `EXECUTOR_DATA_DIR` | `~/.executor` | Runtime data, SQLite DB, `auth.json`, `server.json` |
| `EXECUTOR_WEB_BASE_URL` | `http://localhost:<port>` | Public origin for OAuth redirects and absolute links |
| `PORT` | `4788` (daemon), `4789` (supervised service) | HTTP listen port |

See the configuration reference for bootstrap admin env vars, secret keys, scope directories, and client-specific overrides.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Install the published CLI globally, bootstrap a development checkout, and verify the background service starts.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    End-to-end walkthrough: install, open the web UI, add an integration, call a tool, and resume a paused execution.
  </Card>
  <Card title="MCP proxy" href="/mcp-proxy">
    How Executor fronts OpenAPI, GraphQL, and upstream MCP with shared auth and per-tool policies.
  </Card>
  <Card title="Connect MCP clients" href="/connect-mcp-clients">
    Wire Cursor, Claude Code, and other clients via stdio or streamable HTTP.
  </Card>
  <Card title="Embed with the SDK" href="/embed-sdk">
    Compose `createExecutor` with plugins and credential providers in application code.
  </Card>
  <Card title="Hosted cloud" href="/hosted-cloud">
    Sign in to Executor Cloud and use the shared hosted MCP endpoint.
  </Card>
</CardGroup>

---

## 02. Installation

> Install the published CLI globally, bootstrap a development checkout, and verify the background service starts with expected ports and data directories.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/02-installation.md
- Generated: 2026-06-23T19:21:43.625Z

### Source Files

- `apps/docs/local/cli.mdx`
- `apps/cli/package.json`
- `apps/cli/bin/executor.ts`
- `apps/cli/src/installation.ts`
- `scripts/bootstrap.ts`
- `RUNNING.md`

---
title: "Installation"
description: "Install the published CLI globally, bootstrap a development checkout, and verify the background service starts with expected ports and data directories."
---

The published `executor` npm package ships a Node.js launcher plus a per-platform compiled binary. `executor install` registers an OS-supervised background daemon that binds loopback, writes a `server.json` manifest under the data directory, and survives reboots. Development checkouts use `bun run bootstrap` and run the CLI from source instead of the global binary.

## Prerequisites

| Audience | Requirement |
| --- | --- |
| CLI users | Node.js 20 or newer (`engines.node` on the published package) |
| Contributors | [Bun](https://bun.sh) 1.3.x (repo `packageManager`: `bun@1.3.11`) |

<Note>
The global CLI resolves a platform binary from npm `optionalDependencies`. Supported targets are `darwin`/`linux`/`win32` on `x64` and `arm64` (Linux also publishes `musl` variants). If install reports it cannot locate a platform binary, your OS/CPU combination is not in the published matrix.
</Note>

## Install the published CLI

<Tabs>
  <Tab title="npm">
    ```bash
    npm install -g executor
    ```
  </Tab>
  <Tab title="pnpm">
    ```bash
    pnpm add -g executor
    ```
  </Tab>
  <Tab title="bun">
    ```bash
    bun add -g executor
    ```
  </Tab>
  <Tab title="yarn">
    ```bash
    yarn global add executor
    ```
  </Tab>
</Tabs>

Confirm the launcher resolves:

```bash
executor --help
```

A successful install prints subcommand help without `could not locate a platform binary` or `ENOENT`.

## Start the background service

`executor install` is an alias for `executor service install`. Both register the supervised daemon and wait up to 45 seconds for a reachable `server.json` manifest.

<Steps>
  <Step title="Install and start the service">
    ```bash
    executor install
    ```

    On success, output includes the service manager name, web UI origin, data directory, and log path. The command is idempotent: if the service is already running at the requested port with the current binary version, it prints the running origin and exits.

    <Warning>
    On Windows, run `executor install` from an **Administrator** PowerShell. Task Scheduler registration requires elevation.
    </Warning>
  </Step>

  <Step title="Open the web UI">
    ```bash
    executor web
    ```

    This reads the active `server.json` manifest and opens the browser to the running origin, including a `?_token=` query when a bearer token is available. For a throwaway foreground server instead of the supervised service, use `executor web --foreground`.
  </Step>

  <Step title="Verify health and status">
    ```bash
    executor service status
    ```

    Expect `Registered: yes`, `Running: yes`, and a `Serving:` line with the live origin.

    Probe liveness without credentials:

    ```bash
    curl -s http://127.0.0.1:4789/api/health
    ```

    <ResponseExample>
    ```text
    ok
    ```
    </ResponseExample>

    If the supervised daemon uses a non-default port, substitute that port in the URL. Clients discover the live port from `server.json`, not from a fixed default.
  </Step>
</Steps>

## Ports and service managers

| Surface | Default port | Bind address | Notes |
| --- | --- | --- | --- |
| `executor install` / `executor service install` | `4789` | `127.0.0.1` | OS-supervised daemon (`sh.executor.daemon`) |
| `executor daemon run` (auto-start, foreground) | `4788` | `127.0.0.1` | Ephemeral or detached daemon, not OS-supervised |
| `executor web --foreground` | `4788` | `127.0.0.1` | Temporary in-terminal server |

Override the supervised port:

```bash
executor install --port 4790
```

<ParamField body="port" type="integer">
Port the supervised daemon binds. Defaults to `4789`. Loopback only.
</ParamField>

| Platform | Service manager | Service label / task |
| --- | --- | --- |
| macOS | launchd LaunchAgent | `sh.executor.daemon` |
| Linux | systemd `--user` + linger | `sh.executor.daemon.service` |
| Windows | Task Scheduler (S4U / AtStartup) | `ExecutorDaemon` |

<Tip>
On Linux, `executor service status` warns when user lingering is off. Without linger, the daemon starts on login but not at boot. Run `loginctl enable-linger $USER` to fix it.
</Tip>

## Data directory layout

By default, local state lives in `~/.executor`. Override with `EXECUTOR_DATA_DIR`.

:::files
~/.executor/
├── data.db                 # SQLite integrations, connections, policies
├── server-control/
│   ├── auth.json           # Stable bearer token (mode 0600)
│   ├── server.json         # Active server manifest (origin, auth, pid)
│   └── startup.lock        # In-flight startup guard
├── logs/
│   ├── daemon.log
│   └── daemon.error.log
├── daemon-localhost-<port>.json
└── daemon-active-localhost-<scope-hash>.json
:::

<ParamField body="EXECUTOR_DATA_DIR" type="string">
Root directory for database, auth, manifests, and logs. Default: `~/.executor`.
</ParamField>

<ParamField body="EXECUTOR_SCOPE_DIR" type="string">
Scope directory for tool execution context. The supervised service sets this to `EXECUTOR_DATA_DIR` when unset. Foreground daemons default to the current working directory.
</ParamField>

The supervised unit never embeds secrets. The daemon mints or loads the bearer token from `server-control/auth.json` on boot; `server.json` carries a copy for clients.

## Bootstrap a development checkout

Contributors clone the monorepo and run bootstrap before dev servers or tests.

<Steps>
  <Step title="Clone and bootstrap">
    ```bash
    git clone https://github.com/RhysSullivan/executor.git
    cd executor
    bun run bootstrap
    ```

    Bootstrap is idempotent. It runs `bun install` (whose `prepare` hook builds `@executor-js/vite-plugin` and `packages/react`, artifacts Vite dev servers require) and installs Playwright Chromium for e2e.
  </Step>

  <Step title="Run the CLI from source">
    ```bash
    bun run dev:cli -- --help
    ```

    This sets `EXECUTOR_DEV=1` and `EXECUTOR_DATA_DIR=apps/local/.executor-dev` by default.

    <Warning>
    `executor install` and `executor service install` require the **compiled** binary. In a dev checkout they fail with a message to use `bun run apps/cli/src/main.ts daemon run --foreground` instead.
    </Warning>
  </Step>

  <Step title="Start a foreground daemon for local work">
    ```bash
    bun run dev:cli -- daemon run --foreground
    ```

    Default port is `4788`. Open the UI against the running manifest:

    ```bash
    bun run dev:cli -- web
    ```
  </Step>

  <Step title="Run the full dev stack (optional)">
    ```bash
    bun run dev
    ```

    Starts turbo dev servers for apps and packages (excluding desktop and cloud by default). See the develop-locally page for package boundaries, targeted tests, and e2e boot recipes.
  </Step>
</Steps>

## Verification checklist

| Signal | Expected result |
| --- | --- |
| `executor --help` | Exit 0, subcommand list |
| `executor install` | Prints origin like `http://127.0.0.1:4789`, data dir, logs path |
| `executor service status` | `Registered: yes`, `Running: yes`, `Serving:` with origin |
| `GET /api/health` on the serving origin | Body exactly `ok`, no auth header |
| `~/.executor/server-control/server.json` | Exists after first successful boot |
| `~/.executor/logs/daemon.error.log` | Empty or only startup lines on clean boot |

If install times out after 45 seconds, check `~/.executor/logs/daemon.error.log` and re-run `executor service status`. Version drift (running binary older than CLI) is flagged in status output; re-run `executor install` to repoint the unit and restart.

## Common failure modes

<AccordionGroup>
  <Accordion title="Port already in use">
    Another process may hold the default port. Pick a free port with `executor install --port <n>`, or stop the conflicting listener and reinstall. On Windows, orphaned `executor.exe` listeners can survive a stopped scheduled task; `executor service uninstall` attempts cleanup.
  </Accordion>
  <Accordion title="Dev checkout cannot run executor install">
    Expected. OS service managers need the compiled binary path (`process.execPath`). Use `daemon run --foreground` from the dev CLI, or build a local binary with `bun run --cwd apps/cli build` and install from `apps/cli/dist/executor`.
  </Accordion>
  <Accordion title="Fresh worktree Vite errors about @executor-js/vite-plugin">
    Run `bun run bootstrap` from the repo root. Skipping bootstrap leaves dev servers without the prepare-built artifacts.
  </Accordion>
  <Accordion title="Linux daemon stops after logout">
    Enable lingering: `loginctl enable-linger $USER`. Without it, the user systemd manager (and the daemon) stops when you log out.
  </Accordion>
</AccordionGroup>

<Info>
Self-hosted Docker uses port `17888` and a different runtime (`apps/host-selfhost`). That path is not the local CLI daemon described here.
</Info>

## Next

<CardGroup>
  <Card title="Quickstart" href="/quickstart">
    Run `executor install`, open the web UI, add a first integration, and call a tool.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    All `executor` subcommands, flags, and auto-start behavior.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Environment variables, paths, ports, and client overrides.
  </Card>
  <Card title="Develop locally" href="/develop-locally">
    Monorepo bootstrap, turbo dev, Vitest, and e2e for contributors.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Port conflicts, stale manifests, OAuth behind proxies, and recovery commands.
  </Card>
</CardGroup>

---

## 03. Quickstart

> Run `executor install`, open the web UI, add a first integration, search and call a tool, and resume a paused execution.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/03-quickstart.md
- Generated: 2026-06-23T19:22:06.165Z

### Source Files

- `README.md`
- `apps/docs/local/cli.mdx`
- `apps/cli/src/main.ts`
- `apps/local/package.json`
- `packages/core/api/src/tools/api.ts`
- `packages/core/api/src/executions/api.ts`

---
title: "Quickstart"
description: "Run `executor install`, open the web UI, add a first integration, search and call a tool, and resume a paused execution."
---

`executor install` registers a loopback HTTP runtime as an OS-supervised background service (launchd on macOS, systemd user unit on Linux, Task Scheduler on Windows). The supervised daemon binds `127.0.0.1` on port `4789` by default, persists state under `~/.executor`, and publishes a `server.json` manifest that CLI commands and `executor web` read for the live origin and bearer token.

<Note>
Requires Node.js 20 or newer and the published `executor` npm package. `executor install` needs the compiled binary; in a dev checkout, run `executor daemon run --foreground` instead.
</Note>

## What you will set up

| Step | Command or surface | Outcome |
| --- | --- | --- |
| Install | `executor install` | Durable local daemon with web UI and HTTP API |
| Browse | `executor web` | Signed-in browser session at `{origin}/?_token=…` |
| Integrate | Web **Connect** dialog or `executor call … addSource` | First OpenAPI, GraphQL, or MCP integration indexed |
| Invoke | `executor tools search`, `executor call` | Tool discovery and execution via QuickJS sandbox |
| Resume | Web `/resume/{id}` or `executor resume` | Continue a paused auth or approval execution |

## Install the background service

<Tabs>
  <Tab title="npm">
    ```bash
    npm install -g executor
    executor install
    ```
  </Tab>
  <Tab title="pnpm">
    ```bash
    pnpm add -g executor
    executor install
    ```
  </Tab>
  <Tab title="bun">
    ```bash
    bun add -g executor
    executor install
    ```
  </Tab>
</Tabs>

<Steps>
  <Step title="Run install">
    ```bash
    executor install
    ```

    Optional: pick a different loopback port.

    ```bash
    executor install --port 4790
    ```

    <ParamField body="--port" type="integer" default="4789">
      Port the supervised daemon binds on loopback. Clients discover the live port from `~/.executor/server-control/server.json` when it differs.
    </ParamField>

    Expected output includes the service manager name, web UI origin, data directory, and log path:

    ```text
    Installing Executor as a background service...
    Service manager: launchd
    Web UI:          http://127.0.0.1:4789
    Data directory:  /Users/you/.executor
    Logs:            /Users/you/.executor/logs
    ...
    Executor is now running as a background service at http://127.0.0.1:4789.
    Open it in your browser, already signed in, with:  executor web
    ```
  </Step>

  <Step title="Verify the service">
    ```bash
    executor service status
    ```

    A healthy install reports `Registered: yes`, `Running: yes`, and a `Serving:` line with the manifest origin.

    The unauthenticated liveness probe is `GET /api/health` returning `ok`.
  </Step>
</Steps>

<Warning>
If the default port is taken during auto-started daemon boot (for example when `executor call` spawns a foreground daemon), the CLI selects the next available port and prints a notice. Always read the origin from `server.json` or `executor service status` when ports may differ.
</Warning>

## Open the web UI

```bash
executor web
```

By default, `executor web` does not start a new server. It reads the active local `server.json` manifest and opens `{origin}/?_token={bearer}` in your default browser so you arrive already authenticated.

| Command | Behavior |
| --- | --- |
| `executor web` | Open the installed background service |
| `executor open` | Same as `executor web` |
| `executor web --foreground` | Start a temporary server in the current terminal (default port `4788`) |

<Tip>
If nothing is running, `executor web` prints install instructions. For a one-off session without OS registration, use `executor web --foreground`.
</Tip>

From the web UI you can browse **Integrations**, inspect **Tools**, configure policies, and use the **Connect** card to wire MCP clients.

## Add a first integration

Executor registers tenant-scoped integrations (OpenAPI specs, GraphQL endpoints, MCP servers) and indexes their tools per connection. Add one from the web UI or CLI.

<Steps>
  <Step title="Add from the web UI">
    1. Open the **Integrations** page.
    2. Click **Connect** (or use the command palette: **Add integration**).
    3. In the **Connect an integration** dialog, paste a spec or endpoint URL, or pick a preset.
    4. Executor auto-detects the type (OpenAPI, GraphQL, MCP, Google Discovery) and routes you to the plugin add form with the URL prefilled.
    5. Submit the form. On success you land on the integration detail page with indexed tools.

    Supported detection kinds map to plugins: `openapi`, `graphql`, `mcp`, and `googleDiscovery`.
  </Step>

  <Step title="Add from the CLI (Petstore example)">
    Register the Swagger Petstore OpenAPI spec:

    ```bash
    executor call executor openapi addSource '{
      "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
      "namespace": "petstore",
      "baseUrl": "https://petstore3.swagger.io/api/v3"
    }'
    ```

    Use `baseUrl` when the OpenAPI document declares relative `servers` entries (for example `"/api/v3"`).

    Confirm the integration is live:

    ```bash
    executor tools sources
    ```

    Expected output lists configured sources with tool counts for each integration slug.
  </Step>
</Steps>

<Info>
Integrations are catalog identities; connections bind owner-scoped credentials to them. The Petstore demo needs no auth template. Integrations that require OAuth or API keys need a connection before tools can run. See [Configure credentials](/configure-credentials).
</Info>

## Search and call a tool

CLI tool commands compile TypeScript snippets and run them through `POST /executions`. `executor call`, `executor resume`, and `executor tools …` auto-start a localhost daemon when no supervised service is reachable and no explicit remote credential is configured.

### Search by intent

```bash
executor tools search "list pets"
```

Optional filters:

<ParamField body="--namespace" type="string">
  Restrict search to one integration slug (for example `petstore`).
</ParamField>

<ParamField body="--limit" type="integer" default="12">
  Maximum matches returned.
</ParamField>

### Browse namespaces

```bash
executor call petstore --help
executor tools describe petstore.org.main.pets.listPets
```

`executor call <path…> --help` walks the tool tree. Add `--match "<text>"` and `--limit <n>` to narrow large namespaces.

### Invoke a tool

```bash
executor call petstore org main pets listPets '{}'
```

Path segments map to the sandbox `tools` proxy (`tools.petstore.org.main.pets.listPets`). Pass a JSON object as the final argument.

<RequestExample>
```bash
executor call petstore org main pets listPets '{}'
```
</RequestExample>

<ResponseExample>
```json
[
  { "id": 1, "name": "doggie", "status": "available" }
]
```
</ResponseExample>

### Tool addresses

Persisted tools use connection-aware addresses:

```text
tools.<integration>.<owner>.<connection>.<tool>
```

Example: `tools.petstore.org.main.pets.listPets`. The HTTP catalog exposes these via `GET /tools` and `GET /tools/schema?address=…`.

## Resume a paused execution

Tool calls that need user auth, form input, or policy approval suspend inside the QuickJS sandbox. The executions API returns `status: "paused"` with an `executionId` and interaction metadata. Resume through the web UI or CLI.

```mermaid
stateDiagram-v2
  [*] --> Running: POST /executions
  Running --> Paused: tool elicitation or approval
  Paused --> Running: POST /executions/:id/resume
  Running --> Completed: sandbox finishes
  Paused --> Completed: decline or cancel
  Completed --> [*]
```

### When a call pauses

A paused CLI invocation prints the pause reason, a browser approval URL, and CLI fallback commands:

```text
Approval required for tools.petstore.org.main.pets.addPet.

Approve in browser:
  http://127.0.0.1:4789/resume/exec_abc123

CLI fallback:
  executor resume --execution-id exec_abc123 --action accept
  executor resume --execution-id exec_abc123 --action decline
  executor resume --execution-id exec_abc123 --action cancel
```

Form elicitation also prints a `--content` JSON template when the interaction carries a `requestedSchema`.

### Resume in the browser

Open `/resume/{executionId}` on your Executor origin. The approval page loads the paused execution via `GET /executions/:executionId` and submits `POST /executions/:executionId/resume` when you approve, decline, or cancel.

### Resume from the CLI

```bash
executor resume --execution-id exec_abc123 --action accept
```

<ParamField body="--execution-id" type="string" required>
  Execution ID from the paused response.
</ParamField>

<ParamField body="--action" type="accept | decline | cancel" default="accept">
  Interaction response sent to the executions resume endpoint.
</ParamField>

<ParamField body="--content" type="string">
  JSON object for `action=accept` when the pause requested structured input.
</ParamField>

:::endpoint POST /executions/:executionId/resume
Resume a paused sandbox execution.

**Payload**

| Field | Type | Description |
| --- | --- | --- |
| `action` | `"accept"` \| `"decline"` \| `"cancel"` | User decision |
| `content` | object (optional) | Structured input when accepting a form elicitation |

**Response**: Same union as execute: `completed` with `text`, `structured`, `isError`, or another `paused` result if a subsequent approval is required.
:::

<Check>
After resume completes, the CLI prints the final tool output and exits `0`. If another approval gate appears, it prints the next `/resume/{id}` URL.
</Check>

## Quick reference

| Task | Command |
| --- | --- |
| Install service | `executor install` |
| Open UI | `executor web` |
| List integrations | `executor tools sources` |
| Search tools | `executor tools search "<query>"` |
| Call tool | `executor call <path…> '<json>'` |
| Resume pause | `executor resume --execution-id <id>` |
| Service health | `executor service status` |
| Foreground dev server | `executor web --foreground` |

<AccordionGroup>
  <Accordion title="executor web says Executor is not running">
    Run `executor install`, then `executor web` again. For a throwaway session, use `executor web --foreground`.
  </Accordion>
  <Accordion title="Install fails in a dev checkout">
    `executor install` requires the compiled binary. From source, run `executor daemon run --foreground` and open the printed origin manually.
  </Accordion>
  <Accordion title="Resume says execution not found">
    Paused executions are session-scoped. The pause may have already been resolved, or the daemon restarted. Re-run the original tool call to generate a fresh execution ID.
  </Accordion>
  <Accordion title="Override data directory or auth">
    Set `EXECUTOR_DATA_DIR` before install to relocate `~/.executor`. Remote targets accept `EXECUTOR_API_KEY` or `EXECUTOR_AUTH_TOKEN`. See [Configuration reference](/configuration-reference).
  </Accordion>
</AccordionGroup>

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    Global install paths, dev bootstrap, ports, and data directory layout.
  </Card>
  <Card title="Add integrations" href="/add-integrations">
    OpenAPI, GraphQL, and MCP registration from the UI and CLI with post-add verification.
  </Card>
  <Card title="Tools" href="/tools">
    Tool addresses, discovery, schema inspection, and invocation across surfaces.
  </Card>
  <Card title="Executions" href="/executions">
    Code-mode execution, pause reasons, HTTP routes, and MCP elicitation handling.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full `executor` subcommands, flags, and auto-start behavior.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Port conflicts, unreachable servers, OAuth behind proxies, and recovery commands.
  </Card>
</CardGroup>

---

## 04. MCP proxy

> How Executor acts as a single MCP endpoint in front of OpenAPI, GraphQL, and upstream MCP integrations with shared auth and per-tool policies.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/04-mcp-proxy.md
- Generated: 2026-06-23T19:21:30.023Z

### Source Files

- `apps/docs/mcp-proxy.mdx`
- `packages/hosts/mcp/src/tool-server.ts`
- `packages/hosts/mcp/src/index.ts`
- `apps/cli/src/main.ts`
- `packages/core/api/src/server/mcp-build.ts`
- `apps/docs/concepts/integrations.mdx`

---
title: "MCP proxy"
description: "How Executor acts as a single MCP endpoint in front of OpenAPI, GraphQL, and upstream MCP integrations with shared auth and per-tool policies."
---

Executor exposes one MCP endpoint (`execute` and `resume`) that fronts every configured integration. Agents connect once; upstream OpenAPI APIs, GraphQL endpoints, and MCP servers are reached through the execution engine inside a QuickJS sandbox, with connection credentials and per-tool policies applied on each call.

## Architecture

```mermaid
flowchart TB
  subgraph clients["MCP clients"]
    A[Cursor / Claude Code / OpenCode / SDK]
  end

  subgraph executor["Executor MCP host"]
    E["/mcp or executor mcp"]
    TS["createExecutorMcpServer"]
    ENG["ExecutionEngine"]
    POL["Policy resolution"]
    CONN["Connections + credentials"]
  end

  subgraph upstream["Integrations"]
    M[MCP servers]
    O[OpenAPI APIs]
    G[GraphQL endpoints]
  end

  A -->|MCP stdio or streamable HTTP| E
  E --> TS
  TS -->|execute / resume| ENG
  ENG --> POL
  ENG --> CONN
  ENG -->|HTTP or MCP wire| M
  ENG -->|HTTP| O
  ENG -->|HTTP| G
```

The MCP surface does not mirror every upstream tool as a separate MCP tool. Clients call `execute` with TypeScript that invokes tools through the `tools.*` namespace (for example `tools.github.org.main.getRepo(...)`). The `execute` tool description is built dynamically from saved [connections](/connections) and [integrations](/integrations).

| Layer | Package / path | Role |
| --- | --- | --- |
| Serving envelope | `@executor-js/host-mcp` (`McpServingRoutes`) | Auth, CORS, session dispatch on `/mcp` |
| Tool factory | `@executor-js/host-mcp/tool-server` (`createExecutorMcpServer`) | Registers `execute` and `resume`, elicitation bridge |
| Session store | `McpSessionStore` seam | Per-session transport, ownership, browser approval |
| Local runtime | `apps/local/src/mcp.ts` | In-process streamable HTTP + stdio |
| Multi-user hosts | `packages/core/api/src/server/mcp-build.ts` | Per-principal engine via `makeMcpBuildServer` |

## MCP tools exposed to clients

Executor registers two MCP tools (three when elicitation mode is `native`):

| Tool | Input | Behavior |
| --- | --- | --- |
| `execute` | `code` (non-empty string) | Runs TypeScript in QuickJS with `tools.*` access to all connections |
| `resume` | `executionId`, `action`, `content?` | Resumes a paused execution (`model` mode only) |
| `resume` | `executionId` | Waits for browser approval (`browser` mode only) |

In `native` mode, elicitation is handled through MCP SDK `elicitInput` during `execute`; the separate `resume` tool is not registered.

The dynamic `execute` description lists connection prefixes, search/describe workflow, and sandbox rules. It is assembled from `executor.connections.list()` and `executor.integrations.list()` at session creation time.

## How integrations are proxied

An [integration](/integrations) defines the tool catalog (OpenAPI spec, GraphQL endpoint, or upstream MCP server). A [connection](/connections) binds owner-scoped credentials to that integration. Callable addresses follow:

```
tools.<integration>.<owner>.<connection>.<tool>
```

On each sandbox tool call, `makeExecutorToolInvoker` maps `tools.<path>(args)` to `executor.execute(address, args)` with credentials attached from the connection. Upstream MCP servers are registered through the MCP plugin (`packages/plugins/mcp`); their tools join the same catalog and are invoked through `execute`, not as additional MCP tools on Executor's endpoint.

<Note>
Credentials never reach the agent. The sandbox calls Executor's tool layer; Executor resolves the connection and attaches auth to the upstream request.
</Note>

## Policy enforcement

[Policies](/policies) apply per tool address during execution:

| Action | Effect at call time |
| --- | --- |
| `block` | Tool hidden from list; `ToolBlockedError` if invoked |
| `require_approval` | Execution pauses for elicitation; user or model must approve |
| `approve` | Call proceeds without an approval prompt |
| (none) | Falls through to integration default annotations |

Blocked tools surface in the sandbox as `{ ok: false, error: { code: "tool_blocked", ... } }`. Approval pauses return structured content with `executionId` and interaction metadata for `resume`.

## Transport and endpoints

<Tabs>
  <Tab title="Streamable HTTP">
    Executor serves MCP at `/mcp` on the local daemon (default port from install; the web UI Connect card shows the exact URL).

    <ParamField body="mcp-session-id" type="string">
      Required on `GET` for SSE. Omitted on the initial `POST` that opens a session.
    </ParamField>

    Allowed methods: `GET`, `POST`, `DELETE`, `OPTIONS`. Other methods return JSON-RPC `405`.

    Cloud deployments may scope the path to `/<organizationSlug>/mcp` (legacy `/<organizationId>/mcp` also accepted).

    <RequestExample>
```bash
curl -X POST http://127.0.0.1:17888/mcp \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'
```
    </RequestExample>

    Local `/api` and `/mcp` require a bearer token minted at server start. OAuth discovery routes are provider-specific and served outside the envelope.

  </Tab>
  <Tab title="Stdio">
    `executor mcp` starts an MCP server over stdio. It boots a local HTTP server in the background (for the web UI and browser approval URLs), then connects `StdioServerTransport` to `createExecutorMcpServer`.

    <CodeGroup>
```bash title="Default (model resume)"
executor mcp
```
```bash title="Browser approval"
executor mcp --elicitation-mode browser
```
```bash title="Scoped workspace"
executor mcp --scope /path/to/workspace
```
    </CodeGroup>

    Stdio mode redirects `stdout` to `stderr` so JSON-RPC on stdout stays clean for MCP clients.

  </Tab>
</Tabs>

## Elicitation modes

Control how paused executions (OAuth, approval gates) are resumed:

| Mode | How to set | `resume` behavior |
| --- | --- | --- |
| `model` (default) | Omit query param or `--elicitation-mode model` | Model calls `resume` with `action` and optional `content` |
| `browser` | `?elicitation_mode=browser` or `--elicitation-mode browser` | `execute` returns `approvalUrl`; user approves in browser; model calls `resume` to poll |
| `native` | `?elicitation_mode=native` | MCP SDK `elicitInput` during `execute`; no `resume` tool |

HTTP mode reads `?elicitation_mode=` from the session-open request. Legacy `?allow_model_resume=true` maps to `model`.

Browser approval URLs point at `/resume/<executionId>?mcp_session_id=<sessionId>`. The console posts the decision to `/api/mcp-sessions/<sessionId>/executions/<executionId>/resume`.

## Authentication

| Runtime | MCP auth |
| --- | --- |
| Local daemon | Bearer token on every `/mcp` and `/api` request (except OAuth callbacks and `/api/health`) |
| Self-host / Cloud | `McpAuthProvider` seam: OAuth bearer, RFC 9728 `WWW-Authenticate` on `401`, per-request principal resolution |
| Stdio (`executor mcp`) | No MCP-level auth; local server uses its own bearer internally |

The authenticated `Principal` (`accountId`, `organizationId`, roles) scopes connections, policies, and engine construction in multi-user hosts.

## Connect an MCP client

Use `npx add-mcp` to register Executor in Cursor, Claude Code, or OpenCode:

<CodeGroup>
```bash title="HTTP (local)"
npx add-mcp http://127.0.0.1:17888/mcp --transport http --name executor
```
```bash title="HTTP with bearer"
npx add-mcp http://127.0.0.1:17888/mcp --transport http --name executor \
  --header 'Authorization: Bearer <token>'
```
```bash title="Stdio"
npx add-mcp "executor mcp" --name executor
```
```bash title="Browser approval (HTTP)"
npx add-mcp 'http://127.0.0.1:17888/mcp?elicitation_mode=browser' --transport http --name executor
```
</CodeGroup>

The web UI Connect card generates these commands with the correct origin, token, and org slug. Restart the MCP client after adding Executor.

<Warning>
Adding or removing integrations does not require reconfiguring the MCP client. New tools appear in the `execute` description on the next session. An existing MCP session may need reconnecting to pick up an updated description.
</Warning>

## Proxy an upstream MCP server

<Steps>
  <Step title="Add the MCP integration">
    Register the upstream MCP server URL as an integration from the web UI or CLI. See [Add integrations](/add-integrations).
  </Step>
  <Step title="Create a connection">
    Bind credentials (API key, OAuth, or upstream bearer) to the integration. See [Configure credentials](/configure-credentials).
  </Step>
  <Step title="Verify the catalog">
    Confirm tools appear under the connection prefix:

```bash
executor tools sources
```
  </Step>
  <Step title="Call through execute">
    From an MCP client, use `execute` with sandbox code that calls `tools.<integration>.<owner>.<connection>.<tool>(args)`.
  </Step>
</Steps>

Upstream MCP tool annotations are preserved in persisted metadata and flow through invocation unchanged where the plugin supports them.

## Session lifecycle (HTTP)

```mermaid
sequenceDiagram
  participant C as MCP client
  participant E as /mcp envelope
  participant S as McpSessionStore
  participant T as Transport + McpServer

  C->>E: POST /mcp (no mcp-session-id)
  E->>E: authenticate
  E->>S: dispatch (create)
  S->>T: createExecutorMcpServer + connect
  T-->>C: Response + mcp-session-id

  C->>E: POST /mcp (mcp-session-id)
  E->>S: dispatch (forward)
  S->>T: handleRequest
  T-->>C: JSON-RPC result

  C->>E: DELETE /mcp (mcp-session-id)
  E->>S: dispatch (close)
  S->>T: dispose session
```

Cross-bearer reuse of a session id returns `403` (`MCP session does not belong to the current bearer`). Unknown session ids return `404`.

## Debugging and errors

<ParamField body="EXECUTOR_MCP_DEBUG" type="string">
Set to `1` or `true` to enable verbose MCP capability and elicitation logging on stderr.
</ParamField>

Infrastructure defects during `execute` return opaque `Internal tool error [<correlationId>]` with cause logged server-side. JSON-RPC errors use a canonical body: `{ "jsonrpc": "2.0", "error": { "code", "message" }, "id": null }`.

Common JSON-RPC codes from the envelope:

| HTTP | Code | Meaning |
| --- | --- | --- |
| 401 | -32001 | Unauthorized (missing or invalid bearer) |
| 403 | -32001 / -32003 | Forbidden or session ownership mismatch |
| 404 | -32001 | Session not found |
| 405 | -32001 | Method not allowed on `/mcp` |
| 500 | -32603 | Internal server error |

Paused executions expire after a few minutes or when the session runtime restarts. `resume` with a stale `executionId` returns `execution_not_found` with recovery guidance to re-run `execute`.

## Runtime differences

| Runtime | MCP entry | Session backend |
| --- | --- | --- |
| Local daemon | `http://127.0.0.1:<port>/mcp` | In-process (`createMcpRequestHandler`) |
| `executor mcp` | stdio | Shared engine across stdio session |
| Self-host | `/mcp` via `McpServingRoutes` | In-process `McpSessionStore` |
| Executor Cloud | `/<org-slug>/mcp` | Durable Object per session |

All variants build the per-session `McpServer` through `createExecutorMcpServer` and the execution stack (`makeMcpBuildServer` in multi-user hosts).

## Related pages

<CardGroup>
  <Card title="Connect MCP clients" href="/connect-mcp-clients">
    Wire Cursor, Claude Code, and OpenCode via stdio or streamable HTTP, including `add-mcp` setup.
  </Card>
  <Card title="Tools" href="/tools">
    Tool addresses, discovery, schema inspection, and how tools are produced per connection.
  </Card>
  <Card title="Executions" href="/executions">
    Code-mode execution, paused states, and MCP elicitation handling.
  </Card>
  <Card title="Policies" href="/policies">
    Per-tool allow, require-approval, and block actions with effective policy resolution.
  </Card>
  <Card title="Hosted cloud" href="/hosted-cloud">
    Shared MCP endpoint usage on Executor Cloud and how it differs from local.
  </Card>
</CardGroup>

---

## 05. Tools

> Tool addresses, discovery, schema inspection, invocation paths, and how tools are produced per connection across CLI, HTTP API, and MCP surfaces.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/05-tools.md
- Generated: 2026-06-23T19:23:05.061Z

### Source Files

- `packages/core/api/src/tools/api.ts`
- `packages/core/api/src/handlers/tools.ts`
- `packages/core/sdk/src/executor.ts`
- `packages/core/sdk/src/ids.ts`
- `apps/cli/src/main.ts`
- `packages/hosts/mcp/src/tool-server.ts`

---
title: "Tools"
description: "Tool addresses, discovery, schema inspection, invocation paths, and how tools are produced per connection across CLI, HTTP API, and MCP surfaces."
---

Executor exposes a per-connection tool catalog: each saved [connection](/connections) produces a persisted set of callable tools, keyed by a dotted address. Discovery surfaces (`tools.list`, sandbox `tools.search`, CLI `tools`) return metadata; `tools.schema` returns full JSON Schema and TypeScript previews; invocation routes through `executor.execute` (SDK) or code-mode `POST /executions` (HTTP, CLI, MCP).

## Tool addresses

Every dynamic tool has a branded `ToolAddress` with five logical segments:

```
tools.<integration>.<owner>.<connection>.<tool>
```

| Segment | Type | Meaning |
| --- | --- | --- |
| `tools` | prefix | Fixed namespace root for connection-scoped API tools |
| `<integration>` | `IntegrationSlug` | Catalog slug (e.g. `github`, `vercel`) |
| `<owner>` | `Owner` | `org` (tenant-shared) or `user` (acting member) |
| `<connection>` | `ConnectionName` | Connection name under that integration and owner |
| `<tool>` | `ToolName` | Tool name; may itself contain dots |

The `<tool>` segment is everything after the fourth dot. OpenAPI operations with dotted paths (e.g. `aliases.deleteAlias`) address naturally as `tools.github.org.main.aliases.deleteAlias`.

Connection handles omit the final segment:

```
tools.<integration>.<owner>.<connection>
```

Static tools contributed by plugins use a fully qualified id (`fqid`) instead of the five-segment form. Core configuration tools mount under `executor`, for example `executor.coreTools.connections.list`.

<Note>
Sandbox code strips the leading `tools.` prefix. A call like `tools.github.org.main.getRepo(args)` maps back to the full address `tools.github.org.main.getRepo` at invoke time.
</Note>

## How tools are produced

Tools are **persisted per connection**, not resolved live on every list. Production follows this lifecycle:

```mermaid
flowchart TD
  subgraph triggers [Production triggers]
    A[connections.create]
    B[connections.refresh]
    C[oauth.complete]
    D[Stale catalog sync on tools.list]
  end
  subgraph plugin [Plugin layer]
    E[resolveTools]
  end
  subgraph storage [Storage]
    F[(tool rows)]
    G[(definition rows)]
  end
  A --> E
  B --> E
  C --> E
  D --> E
  E --> F
  E --> G
```

When a plugin implements `resolveTools`, the executor:

1. Calls the plugin with the integration config, connection ref, and credential resolvers.
2. Receives `ToolDef` entries (name, description, schemas, annotations) plus shared `$defs`.
3. Deletes prior rows for that connection, writes new `tool` and `definition` rows, and stamps `tools_synced_at`.

OpenAPI plugins derive tools from the integration spec. MCP plugins dial the connection's upstream server. Connections without bound credentials (non-OAuth, non-`none` template, empty `item_ids`) produce zero tools. Plugins without `resolveTools` clear any existing rows and return an empty catalog.

Static tools register at executor startup via each plugin's `staticSources` and live in an in-memory map keyed by `fqid`. They have no backing connection row but carry owner/connection metadata for UI grouping.

## Discovery surfaces

Executor separates lightweight catalog reads from schema-heavy inspection.

| Surface | Path | Returns | Schema bytes |
| --- | --- | --- | --- |
| `tools.list` | SDK / `GET /tools` | Metadata per tool | Omitted (projected columns) |
| `tools.schema` | SDK / `GET /tools/schema` | Full `ToolSchemaView` | Included |
| `tools.search` | Sandbox builtin | Ranked, paginated paths | Via `describe.tool` |
| `tools.describe.tool` | Sandbox builtin | Compact TypeScript shapes | Preview strings |
| `tools.executor.sources.list` | Sandbox builtin | Integrations with `toolCount` | No |

### `tools.list` filters

<ParamField query="integration" type="IntegrationSlug">
Filter to one integration slug.
</ParamField>

<ParamField query="owner" type="Owner">
Filter to `org` or `user`. Omit to merge both owner partitions.
</ParamField>

<ParamField query="connection" type="ConnectionName">
Filter to one connection name.
</ParamField>

<ParamField query="query" type="string">
Case-insensitive substring match against `name` or `description`.
</ParamField>

<ParamField query="includeAnnotations" type="string">
Pass `"true"` to resolve plugin-derived annotations. HTTP query param; handler coerces from string.
</ParamField>

<ParamField query="includeBlocked" type="string">
Pass `"false"` to omit blocked tools. Defaults to including blocked tools on the HTTP surface; SDK defaults to `false`.
</ParamField>

<ResponseField name="address" type="ToolAddress">
Full callable address.
</ResponseField>

<ResponseField name="owner" type="Owner">
`org` or `user`.
</ResponseField>

<ResponseField name="integration" type="IntegrationSlug">
Integration slug.
</ResponseField>

<ResponseField name="connection" type="ConnectionName">
Connection name.
</ResponseField>

<ResponseField name="name" type="string">
Final tool name segment.
</ResponseField>

<ResponseField name="pluginId" type="string">
Owning plugin id.
</ResponseField>

<ResponseField name="description" type="string">
Human-readable description.
</ResponseField>

<ResponseField name="mayElicit" type="boolean">
Plugin annotation: tool may trigger elicitation during invoke.
</ResponseField>

<ResponseField name="requiresApproval" type="boolean">
Plugin-derived default approval annotation. Surfaces as the default policy when no user `tool_policy` rule matches.
</ResponseField>

<ResponseField name="approvalDescription" type="string">
Custom approval prompt text from the plugin.
</ResponseField>

<ResponseField name="static" type="boolean">
True for plugin-contributed static tools (e.g. core configuration tools).
</ResponseField>

On each list read, the executor also runs a best-effort stale-catalog sync: connections whose `tools_synced_at` predates the integration's `config_revised_at` are re-produced.

### `tools.schema`

Returns a `ToolSchemaView` for one address:

<ResponseField name="inputSchema" type="object">
JSON Schema root for tool input.
</ResponseField>

<ResponseField name="outputSchema" type="object">
JSON Schema root for tool output.
</ResponseField>

<ResponseField name="schemaDefinitions" type="object">
Shared `$defs` referenced by the tool's schemas.
</ResponseField>

<ResponseField name="inputTypeScript" type="string">
Compact TypeScript type string for input.
</ResponseField>

<ResponseField name="outputTypeScript" type="string">
Compact TypeScript type string for output.
</ResponseField>

<ResponseField name="typeScriptDefinitions" type="object">
Named TypeScript definitions referenced by the preview strings.
</ResponseField>

Returns `null` (HTTP 404 `ToolNotFoundError`) when the address does not resolve.

## HTTP API

:::endpoint GET /tools
List persisted tool metadata. Mirrors `executor.tools.list`. Query params carry the branded address as plain strings because dotted addresses are not path segments.
:::

:::endpoint GET /tools/schema
Return the full schema view for one tool. Requires `address` query param (`ToolAddress`).
:::

Tool invocation has no direct HTTP route. Callers post JavaScript/TypeScript to the executions group:

:::endpoint POST /executions
Execute code in the QuickJS sandbox. Payload: `{ "code": "..." }`. Returns `completed` or `paused` status with text and structured fields.
:::

:::endpoint POST /executions/:executionId/resume
Resume a paused execution. Payload: `{ "action": "accept" | "decline" | "cancel", "content?: unknown }`.
:::

:::endpoint GET /executions/:executionId
Fetch paused execution info for browser approval flows.
:::

<RequestExample>
```bash title="List GitHub org tools"
curl "$EXECUTOR_ORIGIN/tools?integration=github&owner=org"
```
</RequestExample>

<RequestExample>
```bash title="Inspect schema"
curl "$EXECUTOR_ORIGIN/tools/schema?address=tools.github.org.main.getRepo"
```
</RequestExample>

## Invocation paths

All surfaces converge on `executor.execute(address, args)` inside the SDK. External clients reach it through code-mode execution.

```mermaid
sequenceDiagram
  participant Client as CLI / HTTP / MCP
  participant Exec as Execution engine
  participant SDK as executor.execute
  participant Plugin as plugin.invokeTool
  participant Upstream as Upstream API

  Client->>Exec: POST /executions { code }
  Exec->>Exec: QuickJS sandbox
  Exec->>SDK: tools.<path>(args)
  SDK->>SDK: Policy + approval gate
  SDK->>Plugin: invokeTool(toolRow, credential, args)
  Plugin->>Upstream: Authenticated request
  Upstream-->>Plugin: Response
  Plugin-->>SDK: ToolResult
  SDK-->>Exec: Result envelope
  Exec-->>Client: completed / paused
```

### SDK

```typescript title="Direct invoke"
const result = await executor.execute(
  ToolAddress.make("tools.github.org.main.getRepo"),
  { owner: "octocat", repo: "hello-world" },
);
```

```typescript title="List and inspect"
const tools = await executor.tools.list({ integration: "github", owner: "org" });
const schema = await executor.tools.schema(tools[0].address);
```

The `Executor` surface exposes `tools.list` and `tools.schema` as read methods. Invoke uses top-level `execute`.

### CLI

| Command | Behavior |
| --- | --- |
| `executor call <path> [args]` | Builds sandbox code calling `tools.<path>(args)`, posts to `/executions` |
| `executor call --help <prefix>` | Browse namespace children and print schema help |
| `executor tools search <query>` | Runs `tools.search({ query, limit })` in the sandbox |
| `executor tools describe <path>` | Runs `tools.describe.tool({ path })` |
| `executor tools sources` | Runs `tools.executor.sources.list({})` with optional query filter |
| `executor resume --execution-id <id>` | Posts to `/executions/:id/resume` |

<CodeGroup>
```bash title="Invoke a tool"
executor call github.org.main.getRepo '{"owner":"octocat","repo":"hello-world"}'
```

```bash title="Search the catalog"
executor tools search "github issues" --namespace github --limit 12
```

```bash title="Describe schema"
executor tools describe github.org.main.getRepo
```
</CodeGroup>

### MCP

The MCP host (`executor mcp`) registers two protocol tools:

| MCP tool | Purpose |
| --- | --- |
| `execute` | Run sandbox code with access to the `tools` proxy |
| `resume` | Resume paused executions (model-managed or browser-approval mode) |

Upstream integration tools are **not** registered as individual MCP tools. Agents discover and call them through code inside `execute`, using the lazy `tools` proxy (`tools.search`, `tools.describe.tool`, then `tools.<path>(args)`). The `execute` tool description is built dynamically from the connection inventory.

<Info>
MCP elicitation modes (`model`, `browser`, `native`) control how approval pauses surface to the client. See [Executions](/executions) and [MCP proxy](/mcp-proxy) for the full pause/resume flow.
</Info>

### Sandbox builtins

Built-in discovery tools live outside the per-connection catalog:

| Path | Input | Output |
| --- | --- | --- |
| `search` | `{ query, namespace?, limit?, offset? }` | `{ items, total, hasMore, nextOffset }` |
| `describe.tool` | `{ path }` | TypeScript shapes, or `error: { code: "tool_not_found", suggestions }` |
| `executor.sources.list` | `{ query?, limit?, offset? }` | Integrations with `toolCount` |
| `executor.coreTools.*` | Varies | Configuration operations (connections, integrations, policies, OAuth) |

The `tools` object is a lazy proxy. `Object.keys(tools)` does not enumerate the catalog; use `tools.search()` or `tools.executor.coreTools.connections.list({})`.

## Policy effects

Before invoke, the executor resolves the effective [policy](/policies) for the tool:

1. Match owner-scoped `tool_policy` rows by pattern (`<integration>.<owner>.<connection>.<tool>`).
2. Fall back to plugin `requiresApproval` annotation as the default.

| Policy action | List behavior | Invoke behavior |
| --- | --- | --- |
| `allow` | Visible (unless `includeBlocked: false` and overridden) | Proceeds |
| `require_approval` | Visible | Pauses for approval elicitation |
| `block` | Omitted by default (`includeBlocked: false`) | `ToolBlockedError` |

Agent-facing surfaces (MCP, default `tools.list`) omit blocked tools silently.

## Invoke result shape

Sandbox tool calls return a uniform envelope:

```typescript
{ ok: true; data: T; http?: { status: number; headers: Record<string, string> } }
| { ok: false; error: { code: string; message: string; status?: number; details?: unknown; retryable?: boolean } }
```

HTTP-backed OpenAPI tools attach transport metadata in `http` beside `data`. File outputs use `ToolFile` values (`{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`).

## End-to-end workflow

<Steps>
<Step title="Add integration and connection">
Register an [integration](/integrations), then create a [connection](/connections) with credentials. Tool production runs automatically on create.
</Step>

<Step title="Verify the catalog">
```bash
executor tools sources
executor tools search "your intent" --limit 12
```
Confirm the integration appears with a non-zero `toolCount` and search returns ranked paths.
</Step>

<Step title="Inspect schema">
```bash
executor tools describe <integration>.<owner>.<connection>.<tool>
```
Or `GET /tools/schema?address=tools.<integration>.<owner>.<connection>.<tool>`.
</Step>

<Step title="Call the tool">
```bash
executor call <integration>.<owner>.<connection>.<tool> '{"key":"value"}'
```
If execution pauses for approval or OAuth, use `executor resume --execution-id <id>`.
</Step>
</Steps>

<Warning>
A `ToolNotFoundError` includes up to five suggestions from the same connection. Use the suggested path rather than retrying a guessed address.
</Warning>

## Related pages

<CardGroup>
<Card title="Connections" href="/connections">
Owner-scoped credentials that produce per-connection tool catalogs.
</Card>

<Card title="Integrations" href="/integrations">
Tenant-level catalog identities and plugin `resolveTools` sources.
</Card>

<Card title="Policies" href="/policies">
Per-tool allow, require-approval, and block rules that gate list and invoke.
</Card>

<Card title="Executions" href="/executions">
Code-mode sandbox, pause/resume states, and elicitation handling.
</Card>

<Card title="MCP proxy" href="/mcp-proxy">
Single MCP endpoint exposing the unified catalog through `execute`.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full route groups including `tools` and `executions`.
</Card>

<Card title="CLI reference" href="/cli-reference">
`call`, `tools`, `resume`, and server profile flags.
</Card>

<Card title="SDK reference" href="/sdk-reference">
`createExecutor`, `tools.list`, `tools.schema`, and `execute`.
</Card>
</CardGroup>

---

## 06. Integrations

> Tenant-level catalog identities for OpenAPI specs, GraphQL endpoints, MCP servers, and plugin-registered sources; detection, registration, and auth method descriptors.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/06-integrations.md
- Generated: 2026-06-23T19:23:03.790Z

### Source Files

- `apps/docs/concepts/integrations.mdx`
- `packages/core/api/src/integrations/api.ts`
- `packages/core/api/src/handlers/integrations.ts`
- `packages/core/sdk/src/integration.ts`
- `packages/plugins/openapi/src/sdk/plugin.ts`
- `packages/plugins/mcp/src/sdk/plugin.ts`

---
title: "Integrations"
description: "Tenant-level catalog identities for OpenAPI specs, GraphQL endpoints, MCP servers, and plugin-registered sources; detection, registration, and auth method descriptors."
---

An **integration** is a tenant-level catalog row keyed by `slug` that names an API surface (OpenAPI spec, GraphQL endpoint, MCP server, or plugin-registered source). Core stores only catalog identity (`slug`, `name`, `description`, owning `kind`) plus an opaque `config` blob the owning plugin writes and reads. Integrations do not carry credentials; owner-scoped [connections](/connections) bind auth templates to credential providers and materialize per-connection tools.

## Integration vs connection

| Concern | Integration | Connection |
| --- | --- | --- |
| Scope | Tenant catalog (shared) | Owner-scoped (`org` or `user`) |
| Identity | `slug` | `(owner, integration, name)` |
| Holds secrets | No | Yes (via credential provider) |
| Produces tools | Declares the surface; tool timing depends on plugin | Creates or refreshes tool rows for that owner |
| Auth selection | Declares `authMethods` (derived from config) | Binds one `template` slug from those methods |

<Info>
The built-in `executor` integration (`kind: "built-in"`, `canRemove: false`) ships in every workspace and exposes core control tools (for example `executor.integrations.list`) without connection setup.
</Info>

## Catalog model

Persisted integrations live in the `integration` table. The HTTP and SDK surfaces project a public `Integration` type with no credentials and no plugin-internal config.

<ResponseField name="slug" type="IntegrationSlug">
Stable catalog identifier within the tenant. Branded string; used as the `<integration>` segment in tool addresses.
</ResponseField>

<ResponseField name="name" type="string">
Display name. Falls back to `description` then `slug` on legacy rows.
</ResponseField>

<ResponseField name="description" type="string">
Agent-visible context: what the API is and when to use it. Editable via `PATCH /integrations/:slug` without touching plugin config.
</ResponseField>

<ResponseField name="kind" type="string">
Owning plugin id (for example `openapi`, `graphql`, `mcp`, `built-in`).
</ResponseField>

<ResponseField name="canRemove" type="boolean">
`false` for static or built-in integrations registered at plugin boot. `true` for user-added catalog entries.
</ResponseField>

<ResponseField name="canRefresh" type="boolean">
`true` when the owning plugin supports `connections.refresh` (re-resolve tools for existing connections).
</ResponseField>

<ResponseField name="authMethods" type="AuthMethodDescriptor[]">
Derived projection of the plugin's `authenticationTemplate`. Always present, possibly empty. Not stored as a DB column.
</ResponseField>

<ResponseField name="displayUrl" type="string (optional)">
Non-secret URL for catalog UI (favicons). Projected by the plugin's `describeIntegrationDisplay` hook.
</ResponseField>

Plugin-owned config (`IntegrationConfig`) is opaque to core: auth templates, spec references, MCP endpoints, static headers, and similar fields. Only the owning plugin decodes it at register, configure, and execute time.

```text
Tenant catalog                         Owner partition
+---------------------------+          +---------------------------+
| integration (slug)        |          | connection                |
|  plugin_id -> kind        |  1..n    |  (owner, integration,     |
|  config (opaque JSON)     | <------> |   name)                   |
|  can_remove, can_refresh  |          |  template -> auth method  |
+---------------------------+          |  provider -> credential     |
                                       +---------------------------+
                                                |
                                                v
                                       tool rows (per connection)
```

## Integration kinds

| `kind` | Registration entrypoint | Tool production timing | Typical `config` contents |
| --- | --- | --- | --- |
| `openapi` | `executor.openapi.addSpec` / `openapi.addSpec` tool | At add (`putOperations`); `updateSpec` rebuilds all connections | `specHash`, `sourceUrl`, `authenticationTemplate`, optional `baseUrl` / headers |
| `graphql` | `executor.graphql.addIntegration` / `graphql.addIntegration` tool | At connection create or refresh (introspection deferred) | `endpoint`, `authenticationTemplate`, optional `introspectionJson` |
| `mcp` | `executor.mcp.addServer` / `mcp.addServer` tool | At connection create or refresh (live discovery) | `transport` (`remote` or `stdio`), `endpoint` or `command`, `authenticationTemplate` |
| `built-in` | Plugin `staticSources` at boot | Static tools under `executor.*` namespace | None (not persisted) |

Multi-API providers (for example Google, Microsoft) bundle several APIs into one integration so one credential covers the whole provider.

## Registration

Plugins register integrations through `ctx.core.integrations.register` inside their extension methods. Core's `register` **upserts** on slug collision (idempotent boot re-registration), but explicit add flows (`addSpec`, `addServer`, `addIntegration`) **reject** duplicate slugs with `IntegrationAlreadyExistsError`.

<Steps>
<Step title="Choose a slug and describe the surface">
Pick a stable `slug` (for example `stripe`, `github_mcp`). Set `name` and `description` for humans and agents. MCP and GraphQL default the slug from the endpoint when omitted.
</Step>

<Step title="Register via the owning plugin">
Use the plugin extension or its static control tools:

<CodeGroup>
```bash title="OpenAPI (HTTP API)"
POST /openapi/specs
# body: spec, slug, optional baseUrl, authenticationTemplate
```

```bash title="MCP (HTTP API)"
POST /mcp/servers
# body: transport, name, endpoint, optional authenticationTemplate
```

```bash title="GraphQL (HTTP API)"
POST /graphql/integrations
# body: endpoint, optional slug, authenticationTemplate
```
</CodeGroup>
</Step>

<Step title="Verify catalog projection">
List or fetch the integration and confirm `kind`, `authMethods`, and `canRefresh`:

```bash
GET /integrations
GET /integrations/:slug
```
</Step>

<Step title="Create a connection">
Registering an integration does not authenticate. Create an owner-scoped connection that picks an `authMethods[].template` and supplies credential values or completes OAuth. See [Configure credentials](/configure-credentials).
</Step>
</Steps>

### Auth template merge

Plugins that carry a slugged `authenticationTemplate` array (OpenAPI, GraphQL, MCP) use `mergeAuthTemplates`: an incoming entry with a matching slug replaces in place; slug-less entries receive a generated `custom_<id>` slug. Configure flows (`openapi.configure`, `mcp.configureAuth`, GraphQL `configureAuth`) support `mode: "merge"` (default) or `mode: "replace"`.

### Integration presets

Each plugin may declare `integrationPresets` (popular integrations in the web UI). Presets are catalog metadata only; registering still goes through the plugin add flow. Available presets are exposed via `ctx.core.integrations.presets()` aggregated across loaded plugins.

## URL detection

`POST /integrations/detect` (SDK: `executor.integrations.detect(url)`) asks every loaded plugin with a `detect` hook whether the URL belongs to it. Results are returned as an array; the UI ranks by `confidence` (`high` > `medium` > `low`).

<ParamField body="url" type="string" required>
URL to probe. Trimmed before dispatch. Max length 2048 characters.
</ParamField>

<ResponseField name="kind" type="string">
Plugin id that recognized the URL (`openapi`, `graphql`, `mcp`, ...).
</ResponseField>

<ResponseField name="confidence" type="high | medium | low">
Match tier. Multiple plugins may claim the same URL; the UI picks the best rank.
</ResponseField>

<ResponseField name="endpoint" type="string">
Normalized endpoint the plugin will use if the user proceeds.
</ResponseField>

<ResponseField name="name" type="string">
Suggested display name (spec title, hostname, etc.).
</ResponseField>

<ResponseField name="slug" type="string">
Suggested catalog slug.
</ResponseField>

| Plugin | High-confidence signal | Low-confidence fallback |
| --- | --- | --- |
| `openapi` | URL resolves to parseable OpenAPI document | None |
| `graphql` | Live introspection succeeds | URL contains `graphql` token |
| `mcp` | Unauthenticated tool discovery or wire-shape probe confirms MCP | URL contains `mcp` token |

<RequestExample>
```bash
curl -X POST "$EXECUTOR_BASE/integrations/detect" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://api.example.com/openapi.json"}'
```
</RequestExample>

<ResponseExample>
```json
[
  {
    "kind": "openapi",
    "confidence": "high",
    "endpoint": "https://api.example.com/openapi.json",
    "name": "Example API",
    "slug": "example_api"
  }
]
```
</ResponseExample>

## Auth method descriptors

`authMethods` is a **derived projection** of each plugin's stored `authenticationTemplate`. Core calls the plugin's `describeAuthMethods(record)` when serving `integrations.list` or `integrations.get`. Malformed config degrades to `[]` rather than failing the catalog read.

| `kind` | Meaning | Typical fields |
| --- | --- | --- |
| `oauth` | OAuth2 authorization code or discovered MCP OAuth | `oauth.authorizationUrl`, `oauth.tokenUrl`, `oauth.scopes`, `oauth.discoveryUrl`, `oauth.supportsDynamicRegistration` |
| `apikey` | API key or bearer rendered via placements | `placements[]` with `carrier` (`header` or `query`), `name`, `prefix`, `variable` |
| `header` | Custom header auth (alias of apikey projection) | Same as `apikey` |
| `none` | No credential required | Empty placements |

<ParamField body="id" type="string">
Stable id within the integration (usually the template slug).
</ParamField>

<ParamField body="template" type="string">
Auth-template slug a connection binds against (`AuthTemplateSlug`).
</ParamField>

<ParamField body="placements" type="PlacementDescriptor[]">
Where credential values render on outbound requests. `variable` names the connection input (`token` when omitted). `literal` renders a static value with no credential input.
</ParamField>

<Note>
OpenAPI derives methods from declared security schemes when `authenticationTemplate` is omitted at add time. MCP OAuth methods carry `discoveryUrl` (the MCP endpoint) and `supportsDynamicRegistration: true`; authorize/token endpoints are discovered at connect time.
</Note>

## HTTP API

Integrations are served under the v2 catalog group (formerly `sources`). Routes carry no scope segment; tenant binding comes from request auth.

| Method | Path | Summary |
| --- | --- | --- |
| `GET` | `/integrations` | List all integrations (persisted + static sources) |
| `GET` | `/integrations/:slug` | Fetch one integration |
| `PATCH` | `/integrations/:slug` | Update `name` and/or `description` only |
| `DELETE` | `/integrations/:slug` | Remove when `canRemove` is true |
| `POST` | `/integrations/detect` | Probe a URL across plugins |

:::endpoint GET /integrations/:slug
Fetch the public integration projection including derived `authMethods`.

**Params:** `slug` (IntegrationSlug)

**Success:** `IntegrationResponse`

**Errors:** `404` IntegrationNotFoundError
:::

:::endpoint PATCH /integrations/:slug
Update user-editable catalog metadata. Does not modify plugin `config`.

<ParamField body="name" type="string">
New display name.
</ParamField>

<ParamField body="description" type="string">
New agent-visible description.
</ParamField>

**Success:** Updated `IntegrationResponse`

**Errors:** `404` IntegrationNotFoundError
:::

:::endpoint DELETE /integrations/:slug
Remove an integration and cascade-delete its connections, tools, and definitions.

**Success:** `{ "removed": true }`

**Errors:** `409` IntegrationRemovalNotAllowedError when `canRemove` is false
:::

## SDK and plugin surfaces

```typescript
// Public catalog read
const all = await executor.integrations.list();
const one = await executor.integrations.get(IntegrationSlug.make("stripe"));

// URL detection
const matches = await executor.integrations.detect("https://mcp.example.com/mcp");

// Plugin-side registration (inside extension)
yield* ctx.core.integrations.register({
  slug: IntegrationSlug.make("my_api"),
  name: "My API",
  description: "Internal billing API",
  config: { /* opaque plugin config */ },
  canRemove: true,
  canRefresh: true,
});
```

Plugin extension methods exposed on `executor`:

| Plugin | Key methods | Static control tools |
| --- | --- | --- |
| OpenAPI | `addSpec`, `updateSpec`, `previewSpec`, `configure` | `executor.openapi.previewSpec`, `executor.openapi.addSpec` |
| GraphQL | `addIntegration`, `configure`, `configureAuth` | `executor.graphql.addIntegration`, `executor.graphql.getIntegration` |
| MCP | `addServer`, `probeEndpoint`, `configureAuth` | `executor.mcp.probeEndpoint`, `executor.mcp.addServer` |

Core also exposes catalog helpers on `ctx.core.integrations` inside plugins: `register`, `update`, `list`, `get`, `remove`, `detect`, `presets`, `configureSchemas`.

## Lifecycle and refresh

- **Remove:** Allowed only when `canRemove: true`. Deletes all connections, tool rows, and definitions for that `slug`. Built-in and boot-registered static integrations return `409`.
- **Refresh:** When `canRefresh: true`, `connections.refresh` re-runs the plugin's `resolveTools` hook. OpenAPI `updateSpec` additionally refreshes every connection after swapping stored operations.
- **Config revision:** Updating integration `config` stamps `config_revised_at`. Connections whose `tools_synced_at` is older lazily rebuild on next read (cross-subject owner policy).

<Warning>
Re-adding an existing slug via explicit add flows fails with `integration_already_exists`. To change auth methods or spec content, use the plugin's update/configure paths (`updateSpec`, `configure`, `configureAuth`) instead of registering again.
</Warning>

## Related pages

<CardGroup>
<Card title="Add integrations" href="/add-integrations">
Step-by-step add flows for OpenAPI, GraphQL, and MCP from the web UI or CLI.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credentials bound to integration auth templates.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Credential providers, OAuth minting, and placement-based auth templates.
</Card>
<Card title="Tools" href="/tools">
How integrations and connections produce discoverable, invocable tools.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full route payloads and error shapes for the integrations group.
</Card>
</CardGroup>

---

## 07. Connections

> Owner-scoped credentials bound to integrations, credential provider resolution, OAuth minting, and the `(owner, integration, name)` identity model.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/07-connections.md
- Generated: 2026-06-23T19:23:05.428Z

### Source Files

- `apps/docs/concepts/connections.mdx`
- `packages/core/api/src/connections/api.ts`
- `packages/core/api/src/handlers/connections.ts`
- `packages/core/sdk/src/connection.ts`
- `packages/core/sdk/src/oauth-service.ts`
- `examples/docs-sdk-quickstart/src/main.ts`

---
title: "Connections"
description: "Owner-scoped credentials bound to integrations, credential provider resolution, OAuth minting, and the `(owner, integration, name)` identity model."
---

A connection is the saved credential for one integration: owner-scoped, bound 1:1 to an integration slug, with its secret value stored in a `CredentialProvider` and applied lazily per tool call. Creating a connection (`executor.connections.create` or `POST /connections`) writes the routing row, stores pasted values in the default writable provider (or references an external provider item), and produces that connection's tool catalog. OAuth connections are minted through `executor.oauth.start` / `complete` instead.

## Identity model

Every connection is uniquely identified by three fields:

| Field | Type | Role |
| --- | --- | --- |
| `owner` | `"org"` \| `"user"` | Workspace-shared (`org`) or personal (`user`) partition |
| `integration` | `IntegrationSlug` | The integration this credential is for |
| `name` | `ConnectionName` | Distinct name under that owner + integration |

The SDK type is `ConnectionRef`:

```ts
interface ConnectionRef {
  readonly owner: Owner;
  readonly name: ConnectionName;
  readonly integration: IntegrationSlug;
}
```

Connection names are normalized to JS-callable identifiers on create. Free-form input like `my-api-key` becomes `myApiKey` via `connectionIdentifier`.

<Note>
`owner: "user"` writes require a signed-in subject. Creating a personal connection without a user subject returns `InvalidConnectionInputError`.
</Note>

### Address format

Each connection exposes a callable address:

```
tools.<integration>.<owner>.<connection>
```

Append `.<tool>` for a full tool address:

```
tools.<integration>.<owner>.<connection>.<tool>
```

Example: `tools.inventory.org.default.listItems`.

## Connection record

The `Connection` shape returned by list, get, create, and OAuth mint:

<ResponseField name="owner" type="Owner">
Workspace (`org`) or personal (`user`) owner.
</ResponseField>

<ResponseField name="name" type="ConnectionName">
Normalized connection name.
</ResponseField>

<ResponseField name="integration" type="IntegrationSlug">
Integration slug this credential is bound to.
</ResponseField>

<ResponseField name="template" type="AuthTemplateSlug">
Which auth method on the integration this credential applies through (for example `apiKey`, `bearer`, or `none`).
</ResponseField>

<ResponseField name="provider" type="ProviderKey">
Credential backend key (`memory`, `default`, `1password`, `keychain`, etc.). Routing only; never the secret itself.
</ResponseField>

<ResponseField name="address" type="ConnectionAddress">
Callable handle: `tools.<integration>.<owner>.<connection>`.
</ResponseField>

<ResponseField name="identityLabel" type="string | null">
Optional human label (which account). Not load-bearing.
</ResponseField>

<ResponseField name="description" type="string | null">
User-curated, agent-visible context (for example "staging CRM, reads only"). Surfaces in tool inventory and `connections.list`.
</ResponseField>

<ResponseField name="expiresAt" type="number | null">
Epoch ms when an OAuth access token expires. Null for static credentials.
</ResponseField>

<ResponseField name="oauthClient" type="OAuthClientSlug | null">
OAuth app slug that minted this connection. Null for static credentials.
</ResponseField>

<ResponseField name="oauthClientOwner" type="Owner | null">
Owner of the OAuth app (may differ from the connection owner when a personal connection uses a workspace app).
</ResponseField>

<ResponseField name="oauthScope" type="string | null">
Space-delimited scopes the provider granted at connect/refresh. Compared against the integration's declared scopes to decide whether reconnect is needed.
</ResponseField>

## Born wired

A connection is created already bound to one integration. There is no separate "connect" step and no unwired state. On create:

1. Validate the integration exists and credential inputs are well-formed.
2. Resolve value origins to one provider and an `item_ids` map (`variable → provider item id`).
3. Upsert the `connection` row (re-create with the same `(owner, integration, name)` updates credentials in place).
4. Call the integration plugin's `resolveTools` and persist the connection's tool rows.

Credentials are applied to the integration's auth template lazily at invoke time, never pre-baked into headers or URLs ahead of time.

## Owner scopes

| Owner | Visibility | Typical use |
| --- | --- | --- |
| `org` | Tenant-shared; `subject` is empty | Team API keys, shared OAuth accounts |
| `user` | Scoped to the acting member's subject | Personal tokens, BYO credentials |

One integration can have many connections under the same owner (for example `default` and `staging` on `inventory`). The same connection name can exist on different integrations because `integration` is part of the identity key.

## Credential origins

Static connections accept exactly one origin form. The HTTP API enforces this with `"Expected exactly one credential origin"`.

<ParamField body="value" type="string">
Sugar for a single `token` input. Written to the default writable provider.
</ParamField>

<ParamField body="values" type="Record<string, string>">
Multi-input map (one entry per named variable). All values go to the default writable provider.
</ParamField>

<ParamField body="from" type="{ provider: ProviderKey; id: ProviderItemId }">
External provider reference. The value is resolved on demand via `provider.get(id)` and is never copied into core storage.
</ParamField>

<ParamField body="inputs" type="Record<string, ConnectionInputOrigin>">
Canonical per-variable origin map. Each entry is `{ value }` or `{ from: { provider, id } }`. SDK-only; mixes pasted and external in one map.
</ParamField>

### Provider resolution rules

| Rule | Behavior |
| --- | --- |
| Single provider per connection | All inputs of one connection share one `provider` key on the row |
| Pasted inputs | Stored in the first registered writable provider (`defaultWritableProvider`) |
| External inputs | Routed to the named provider; `item_ids` holds opaque provider item ids |
| Mixed origins | Rejected: cannot mix pasted `value`/`values` with external `from` |
| Multiple external providers | Rejected: all external inputs must use the same provider |
| Missing writable provider | `CredentialProviderNotRegisteredError` (HTTP 409) |

Pasted values are stored under ids like `connection:{owner}:{integration}:{name}:{variable}`. OAuth access tokens use `oauth:{owner}:{integration}:{name}` with refresh tokens at `{id}:refresh`.

At invoke time, `resolveConnectionValues` loads each `item_ids` entry through the registered provider. OAuth connections refresh the access token first when `expiresAt` is within the skew window, then return `{ token: <access> }`.

<Warning>
External `from` references that resolve to `null` are allowed at create time. The failure surfaces at invoke time as `connection_value_missing`, not during create.
</Warning>

## No-auth connections

Integrations with no credential requirement use `template: "none"` (`NO_AUTH_TEMPLATE`). These connections legitimately bind zero inputs; the persisted `item_ids` map is empty.

```ts
await executor.connections.create({
  owner: "org",
  name: "public",
  integration: "context7",
  template: "none",
  // No value, from, values, or inputs required
});
```

Credentialed templates reject empty `values: {}` or `inputs: {}` at create with `InvalidConnectionInputError` (HTTP 400). An empty-string `value` is allowed when a binding must exist but carries no secret.

## Create a static connection

<Steps>
<Step title="Register the integration">
Add the integration first via `openapi.addSpec`, `graphql.addIntegration`, `mcp.addServer`, or a plugin extension. The integration declares auth templates (where credentials render on requests).
</Step>

<Step title="Register credential providers">
Pass writable providers to `createExecutor({ providers: [...] })`. The first writable provider in registration order receives pasted values.
</Step>

<Step title="Create the connection">
Call `executor.connections.create` with `owner`, `name`, `integration`, `template`, and one credential origin.
</Step>

<Step title="Verify tools">
List tools filtered by integration. Each tool address includes the connection segment.
</Step>
</Steps>

<CodeGroup>
```ts SDK (inline value)
await executor.connections.create({
  owner: "org",
  name: "default",
  integration: "inventory",
  template: "apiKey",
  value: "inventory-api-key",
});
```

```ts SDK (external reference)
await executor.connections.create({
  owner: "org",
  name: "prod",
  integration: "inventory",
  template: "apiKey",
  from: {
    provider: "1password",
    id: "op://vault/item/field",
  },
});
```

```http HTTP API
POST /connections
Content-Type: application/json

{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key"
}
```
</CodeGroup>

<RequestExample>
```json
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key",
  "description": "Production inventory API, read-only"
}
```
</RequestExample>

<ResponseExample>
```json
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "provider": "memory",
  "address": "tools.inventory.org.default",
  "identityLabel": null,
  "description": "Production inventory API, read-only",
  "expiresAt": null,
  "oauthClient": null,
  "oauthClientOwner": null,
  "oauthScope": null
}
```
</ResponseExample>

## OAuth minting

OAuth is a credential mechanism, not an integration type. Do not use `connections.create` for OAuth tokens. Instead:

1. Register an owner-scoped OAuth app (`oauth.createClient` or `oauth.registerDynamicClient`).
2. Start a flow (`oauth.start`) for a target `(owner, integration, name, template)`.
3. Complete the flow (`oauth.complete`) after the user authorizes (or receive an immediate `connected` result for `client_credentials`).

```mermaid
sequenceDiagram
  participant UI as Web UI / Agent
  participant API as Executor runtime
  participant AS as Authorization server
  participant CP as Credential provider

  UI->>API: oauth.start(client, clientOwner, owner, integration, name, template)
  alt client_credentials grant
    API->>AS: Token exchange
    AS-->>API: access_token
    API->>CP: set oauth:owner:integration:name
    API-->>UI: { status: "connected", connection }
  else authorization_code grant
    API->>API: Persist oauth_session (PKCE + state)
    API-->>UI: { status: "redirect", authorizationUrl, state }
    UI->>AS: User authorizes
    AS-->>UI: Redirect with code
    UI->>API: oauth.complete(state, code)
    API->>AS: Code exchange
    AS-->>API: access_token (+ refresh_token)
    API->>CP: set access + refresh item ids
    API->>API: mintOAuthConnection (row + tools)
    API-->>UI: Connection
  end
```

### OAuth start result

| `status` | Meaning | Next step |
| --- | --- | --- |
| `connected` | Token exchanged inline (`client_credentials`) | Connection is ready; tools are produced |
| `redirect` | User must visit `authorizationUrl` | Complete with `state` + `code` after callback |

<ParamField body="client" type="OAuthClientSlug" required>
OAuth app slug to run the flow through.
</ParamField>

<ParamField body="clientOwner" type="Owner" required>
Owner of the OAuth app. A workspace connection (`owner: "org"`) must use a workspace app (`clientOwner: "org"`). A personal connection may use a shared workspace app.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owner under which the minted connection is saved.
</ParamField>

<Tip>
Requested OAuth scopes come from the integration's declared auth template, not from the OAuth client row. Reusing a narrow client on a broad integration still requests the integration's full declared scope set.
</Tip>

OAuth-minted connections carry `oauthClient`, `oauthClientOwner`, `expiresAt`, and `oauthScope` on the connection row. Token refresh runs automatically inside `resolveConnectionValues` before each tool invocation when the access token is expired or near expiry.

Hosts must configure `redirectUri` on `createExecutor` (derived as `${webBaseUrl}${mountPrefix}/oauth/callback`). Redirect-requiring flows fail loudly when it is missing.

## SDK surface

`executor.connections` exposes:

| Method | Input | Returns |
| --- | --- | --- |
| `create` | `CreateConnectionInput` | `Connection` |
| `list` | `{ integration?, owner? }` | `Connection[]` |
| `get` | `ConnectionRef` | `Connection \| null` |
| `update` | `ConnectionRef`, `UpdateConnectionInput` | `Connection` |
| `remove` | `ConnectionRef` | `void` |
| `refresh` | `ConnectionRef` | `Tool[]` |

`UpdateConnectionInput` only accepts `description` and `identityLabel`. Credentials and OAuth lifecycle fields are not editable through update; recreate or refresh instead.

Plugins resolve credentials through `ctx.connections.resolveValue(ref)` (primary `token` variable) or `resolveValues(ref)` (full variable map).

## HTTP API

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/connections` | List connections (`?integration=`, `?owner=`) |
| `POST` | `/connections` | Create or replace a static connection |
| `GET` | `/connections/:owner/:integration/:name` | Get one connection |
| `PATCH` | `/connections/:owner/:integration/:name` | Update metadata |
| `DELETE` | `/connections/:owner/:integration/:name` | Remove connection and its tools |
| `POST` | `/connections/:owner/:integration/:name/refresh` | Re-resolve and persist tools |

:::endpoint POST /connections
Create or upsert a static (non-OAuth) connection. Payload must include exactly one of `value`, `values`, or `from`.
:::

:::endpoint GET /connections/:owner/:integration/:name
Fetch a single connection by its `(owner, integration, name)` key. Returns 404 `ConnectionNotFoundError` when absent.
:::

:::endpoint POST /connections/:owner/:integration/:name/refresh
Re-run the integration plugin's `resolveTools` for this connection and return the updated tool list.
:::

OAuth routes live under `/oauth/*` (`/oauth/start`, `/oauth/complete`, `/oauth/clients`, etc.) and mint connections through a separate path. See the HTTP API reference for full OAuth payloads.

## Errors

| Error | HTTP | When |
| --- | --- | --- |
| `IntegrationNotFoundError` | 404 | Integration slug does not exist |
| `ConnectionNotFoundError` | 404 | `(owner, integration, name)` not found |
| `InvalidConnectionInputError` | 400 | Empty credential on credentialed template, mixed origins, missing user subject, or multiple origin fields |
| `CredentialProviderNotRegisteredError` | 409 | Referenced provider is not registered, or no writable provider for pasted values |
| `OAuthStartError` | 400 | Missing client, workspace/personal app mismatch, missing `redirectUri` |
| `OAuthCompleteError` | 400 | Code exchange failed or session corrupt |
| `OAuthSessionNotFoundError` | 404 | Unknown or expired OAuth session state |

At invoke time, missing resolved values surface as `connection_value_missing` through the auth tool failure path.

## Security model

- Agents and sandboxes never receive raw credential values. Connections store routing (`provider` + `item_ids`); values resolve inside the executor at call time.
- OAuth client secrets and access tokens live in the credential provider, not in connection API responses.
- Removing a connection deletes its tool and definition rows. External provider items are left intact when the provider is read-only; writable providers may delete stored items on OAuth client removal.

## Related pages

<CardGroup>
<Card title="Integrations" href="/integrations">
Tenant-level catalog identities that connections bind to.
</Card>

<Card title="Configure credentials" href="/configure-credentials">
Credential providers, OAuth flows, and placement-based auth templates.
</Card>

<Card title="Tools" href="/tools">
How connections produce callable tool addresses.
</Card>

<Card title="SDK quickstart example" href="/sdk-quickstart-example">
End-to-end `connections.create` walkthrough with an in-memory provider.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full `/connections` and `/oauth` route payloads and error shapes.
</Card>
</CardGroup>

---

## 08. Policies

> Per-tool allow, require-approval, and block actions; pattern matching, effective policy resolution, and default annotations derived from integration specs.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/08-policies.md
- Generated: 2026-06-23T19:23:07.664Z

### Source Files

- `apps/docs/concepts/policies.mdx`
- `packages/core/api/src/policies/api.ts`
- `packages/core/api/src/handlers/policies.ts`
- `packages/core/sdk/src/policies.ts`
- `packages/hosts/mcp/src/browser-approval.ts`
- `packages/core/sdk/src/errors.ts`

---
title: "Policies"
description: "Per-tool allow, require-approval, and block actions; pattern matching, effective policy resolution, and default annotations derived from integration specs."
---

Executor gates every tool call through a layered policy model: user-authored rules in the `tool_policy` table, plugin-derived `requiresApproval` annotations on persisted tool rows, and runtime enforcement in `tools.list` and `execute`. Rules are owner-scoped (`org` or `user`), matched by dotted patterns against tool identity, and resolved to the most restrictive action across owners.

## Policy actions

Each rule assigns one of three actions to matching tools.

| Action | SDK value | Runtime behavior |
|--------|-----------|------------------|
| Always run | `approve` | Tool invokes without an approval prompt, even when the plugin marks `requiresApproval: true`. |
| Require approval | `require_approval` | Execution pauses for human approval before the handler runs. |
| Block | `block` | Tool is hidden from `tools.list` by default; direct `execute` returns `ToolBlockedError`. |

Restriction rank (highest wins when owners disagree): `block` (3) > `require_approval` (2) > `approve` (1).

<Note>
The web UI labels `approve` as **Always run** and `block` as **Block** in menus, or **Blocked** when showing current state.
</Note>

## Owner scope and precedence

Policies are v2 owner-scoped rows, not legacy scope stacks. Each row carries `owner` (`org` | `user`), a `pattern`, an `action`, and a fractional-index `position` (lower lex order = higher precedence within that owner).

Resolution proceeds in two stages:

1. **Per owner:** walk rules sorted by `position` (then `id` for ties). The first pattern that matches the tool wins for that owner.
2. **Across owners:** compare each owner's winning match and keep the most restrictive action. Org rules are the outer guardrail; a user `approve` cannot weaken an org `block`, but a user `require_approval` can strengthen an org `approve`.

```mermaid
flowchart TD
  subgraph inputs [Inputs]
    P[tool_policy rows]
    A[tool.annotations.requiresApproval]
    T[tool id for matching]
  end

  subgraph perOwner [Per-owner match]
    S[Sort by owner rank then position]
    M[First matching pattern per owner]
  end

  subgraph resolve [Effective policy]
    R[Most restrictive action wins]
    F{User rule matched?}
    U[user source + pattern + policyId]
    D[plugin-default source]
  end

  P --> S --> M --> R
  T --> M
  R --> F
  F -->|yes| U
  F -->|no| D
  A --> D
```

<ParamField body="owner" type="org | user" required>
Who owns the rule. `org` applies tenant-wide; `user` applies to the bound subject only. Creating `owner: "user"` rules requires an executor with a user subject.
</ParamField>

## Pattern matching

Patterns match against the tool identity string `<integration>.<owner>.<connection>.<tool>`. Static tools (for example `executor.coreTools.*`) match against the full `ToolAddress` instead.

| Pattern form | Example | Matches |
|--------------|---------|---------|
| Universal | `*` | Every tool id |
| Exact | `vercel.org.main.deploy` | Only that four-segment id |
| Plugin-wide | `vercel.*` | Any tool under `vercel` |
| Subtree (trailing `*`) | `vercel.dns.*` | `vercel.dns.create`, `vercel.dns.zones.list`, etc. |
| Mid-segment `*` | `github.*.*.repos.list` | Wildcards exactly one segment per `*`; targets a tool name across all connections |

Validation (`isValidPattern`) rejects empty patterns, leading/trailing dots, `..`, leading `*` (except bare `*`), and partial wildcards like `me*` or `a.b*`.

<Info>
The web UI authors patterns from display ids like `integration.tool`. It expands them to `integration.*.*.tool` so a rule applies across all connections. Patterns that already end in `*` (`integration.*`, `*`) pass through unchanged.
</Info>

### Pattern examples

```text
vercel.dns.create          → exact match only
vercel.dns.*               → vercel.dns.create, vercel.dns.delete, vercel.dns.zones.list
vercel.*                   → any vercel tool at any depth
github.*.*.repos.list      → github.org.acme.repos.list, github.user.alice.repos.list
*                          → everything
```

## Plugin default annotations

When no user rule matches, Executor falls back to the tool row's `annotations.requiresApproval`. Plugins stamp these defaults when tools are produced or refreshed.

| Plugin | Default `requiresApproval` | `approvalDescription` |
|--------|---------------------------|----------------------|
| OpenAPI | `true` for `POST`, `PUT`, `PATCH`, `DELETE` operations | `"METHOD /pathTemplate"` |
| GraphQL | `true` for mutation bindings | `"mutation fieldName"` |
| MCP | `true` when upstream `destructiveHint` is set | upstream tool title or name |

The effective policy source is `plugin-default` in this case. `approve` from the plugin default means the tool runs without prompting; `require_approval` triggers elicitation.

User rules with source `user` always override plugin defaults when they match, regardless of annotation.

## Enforcement surfaces

### `tools.list`

By default, blocked tools are omitted. Pass `includeBlocked=true` on the HTTP query or SDK filter to surface them for admin views.

The list response includes `requiresApproval` and `approvalDescription` from plugin annotations. The UI combines stored policies with annotations via `effectivePolicyFromSorted` to show the effective action per tool.

### `execute`

On every invocation, Executor resolves the effective policy, then:

| Effective action | Behavior |
|------------------|----------|
| `block` | Returns `ToolBlockedError` with `address` and matched `pattern`. |
| `approve` | Skips approval even if `requiresApproval` is set. |
| `require_approval` | Fires elicitation before the handler. |
| No user rule + `requiresApproval: true` | Fires elicitation using `approvalDescription` when present. |
| No user rule + no annotation | Runs immediately. |

Declined or cancelled approval returns `ElicitationDeclinedError`.

### MCP approval flow

When a gated call pauses, MCP hosts can surface browser approval. With `?elicitation_mode=browser`, the host returns an `approvalUrl` pointing at `/resume/<executionId>`. The model calls the `resume` tool to wait for the human decision. Modes `model` (default) and `native` keep approval inline with the agent.

## Data model

Policies persist in the `tool_policy` table (SQLite locally, Postgres in cloud). Rows are partitioned by tenant and owner policy, same as connections.

<ResponseField name="id" type="PolicyId">
Stable rule identifier (`pol_…`).
</ResponseField>

<ResponseField name="owner" type="org | user">
Rule owner partition.
</ResponseField>

<ResponseField name="pattern" type="string">
Dotted match pattern (see grammar above).
</ResponseField>

<ResponseField name="action" type="approve | require_approval | block">
Enforcement action.
</ResponseField>

<ResponseField name="position" type="string">
Fractional-index sort key. New rules without an explicit position are inserted above the current minimum (highest precedence).
</ResponseField>

## HTTP API

:::endpoint GET /policies
List all tool policies visible to the authenticated identity. Returns an array of policy objects with epoch-millisecond `createdAt` and `updatedAt`.
:::

:::endpoint POST /policies
Create an owner-scoped policy.

<RequestExample>
```json title="Create policy"
{
  "owner": "org",
  "pattern": "vercel.dns.*",
  "action": "require_approval"
}
```
</RequestExample>

<ParamField body="owner" type="org | user" required>
Owner partition for the new rule.
</ParamField>

<ParamField body="pattern" type="string" required>
Valid match pattern.
</ParamField>

<ParamField body="action" type="approve | require_approval | block" required>
Enforcement action.
</ParamField>

<ParamField body="position" type="string">
Optional fractional-index position. Omit to insert at the top of the owner's list.
</ParamField>
:::

:::endpoint PATCH /policies/:policyId
Update pattern, action, or position. Payload must include `owner` to identify the partition.

```json title="Update action"
{
  "owner": "org",
  "action": "block"
}
```
:::

:::endpoint DELETE /policies/:policyId
Remove a policy. Payload must include `owner`.

```json title="Remove policy"
{
  "owner": "org"
}
```

Returns `{ "removed": true }`.
:::

## SDK surface

The `executor.policies` namespace mirrors the HTTP API and adds resolution:

```typescript title="Policy CRUD and resolve"
const rules = await executor.policies.list();

const created = await executor.policies.create({
  owner: "org",
  pattern: "github.*.*.repos.delete",
  action: "block",
});

await executor.policies.update({
  id: String(created.id),
  owner: "org",
  action: "require_approval",
});

await executor.policies.remove({ id: String(created.id), owner: "org" });

const effective = await executor.policies.resolve(
  ToolAddress.make("tools.github.org.main.repos.delete"),
);
// { action, source, pattern?, policyId? }
```

Pure helpers exported from `@executor-js/sdk` (and `@executor-js/sdk/shared`) support UI and tests without a live executor:

- `matchPattern(pattern, toolId)`
- `isValidPattern(pattern)`
- `resolveToolPolicy(toolId, policies, ownerRank)`
- `resolveEffectivePolicy(toolId, policies, ownerRank, defaultRequiresApproval?)`
- `effectivePolicyFromSorted(toolId, sortedPolicies, defaultRequiresApproval?)`

## Resolution examples

<AccordionGroup>
<Accordion title="Org block overrides user approve">
Given org rule `vercel.*` → `block` and user rule `vercel.dns.create` → `approve`, calling `vercel.dns.create` is blocked. The org guardrail wins.
</Accordion>

<Accordion title="User require_approval strengthens org approve">
Given org `vercel.*` → `approve` and user `vercel.dns.create` → `require_approval`, that tool pauses for approval.
</Accordion>

<Accordion title="Position precedence within one owner">
Given `vercel.dns.create` → `approve` at position `a0` and `vercel.dns.*` → `require_approval` at `a1`, `vercel.dns.create` matches the more specific rule first and runs without approval.
</Accordion>

<Accordion title="Plugin default when no rule matches">
With no user rules, a tool annotated `requiresApproval: true` (for example an OpenAPI `DELETE`) pauses for approval. A `GET` tool with no annotation runs immediately.
</Accordion>

<Accordion title="Explicit approve overrides plugin default">
A user rule `vercel.*.*.delete` → `approve` suppresses the plugin's `requiresApproval` on the delete tool.
</Accordion>
</AccordionGroup>

## Errors

| Error | When |
|-------|------|
| `ToolBlockedError` | `execute` on a tool whose effective action is `block`. Message includes the matched pattern. |
| `ElicitationDeclinedError` | User declined or cancelled an approval prompt. |
| `StorageError` | Invalid pattern or action on create/update; `owner: "user"` without a bound subject; policy not found on update. |

MCP tool dispatch maps `ToolBlockedError` to code `tool_blocked` in the sandbox result.

## Related pages

<CardGroup>
<Card title="Manage policies" href="/manage-policies">
Create and update owner-scoped rules from the web UI, CLI, and HTTP API; handle approval pauses end to end.
</Card>
<Card title="Executions" href="/executions">
Paused states, `execute` and `resume` routes, and MCP elicitation handling when approval is required.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and how `includeBlocked` surfaces gated tools.
</Card>
<Card title="MCP proxy" href="/mcp-proxy">
Single MCP endpoint with shared auth and per-tool policies across integrations.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full `policies` route payloads and error shapes.
</Card>
<Card title="SDK reference" href="/sdk-reference">
`executor.policies` helpers and typed policy exports.
</Card>
</CardGroup>

---

## 09. Executions

> Code-mode execution via QuickJS, paused states for auth and approval, `execute` and `resume` HTTP routes, and MCP elicitation handling.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/09-executions.md
- Generated: 2026-06-23T19:24:18.920Z

### Source Files

- `packages/core/api/src/executions/api.ts`
- `packages/core/api/src/handlers/executions.ts`
- `packages/core/execution/src/engine.ts`
- `packages/kernel/runtime-quickjs/src/index.ts`
- `packages/hosts/mcp/src/tool-server.ts`
- `apps/cli/src/main.ts`

---
title: "Executions"
description: "Code-mode execution via QuickJS, paused states for auth and approval, `execute` and `resume` HTTP routes, and MCP elicitation handling."
---

Executor runs agent-authored TypeScript in a sandboxed QuickJS runtime, wires a `tools.*` proxy into that sandbox, and routes every tool call through the same executor stack used by direct invocation. When a tool needs user input (OAuth, credentials, policy approval), the execution engine pauses, returns an `executionId`, and resumes on `accept`, `decline`, or `cancel`. The same pause/resume model is exposed over HTTP (`POST /api/executions`), MCP (`execute` / `resume` tools), and the CLI (`executor resume`).

## Architecture

```mermaid
flowchart TB
  subgraph surfaces["Caller surfaces"]
    MCP["MCP host<br/>execute / resume tools"]
    HTTP["HTTP API<br/>POST /api/executions"]
    CLI["CLI<br/>call / resume"]
    UI["Web UI<br/>tool run panel"]
  end

  subgraph engine["@executor-js/execution"]
    EE["createExecutionEngine"]
    Pause["executeWithPause / resume"]
    Inline["execute + onElicitation"]
  end

  subgraph runtime["Code substrate"]
    QJS["runtime-quickjs<br/>makeQuickJsExecutor"]
    DW["runtime-dynamic-worker<br/>Cloudflare Workers"]
  end

  subgraph sandbox["QuickJS sandbox"]
    Proxy["tools.* proxy"]
    Emit["emit() / console"]
    Invoker["SandboxToolInvoker"]
  end

  subgraph executor["@executor-js/sdk Executor"]
    Tools["executor.execute(address, args)"]
    Policy["policy + elicitation"]
  end

  MCP --> EE
  HTTP --> EE
  CLI --> HTTP
  UI --> HTTP
  EE --> Pause
  EE --> Inline
  EE --> QJS
  EE --> DW
  QJS --> Proxy
  Proxy --> Invoker
  Invoker --> Tools
  Tools --> Policy
```

| Layer | Package / surface | Role |
| --- | --- | --- |
| Engine | `@executor-js/execution` | Pause/resume orchestration, result formatting, built-in sandbox tools |
| Runtime | `@executor-js/runtime-quickjs` (local, self-host) or `@executor-js/runtime-dynamic-worker` (cloud) | Sandboxed JS execution with timeout and memory limits |
| Invoker | `makeExecutorToolInvoker` | Maps `tools.<path>(args)` to `executor.execute` |
| Host wiring | `@executor-js/api` `makeExecutionStack` | Binds scoped executor + `CodeExecutorProvider` per request |

<Note>
MCP paused executions are **session-scoped**. The web approval page at `/resume/:executionId` resolves pauses through `/api/mcp-sessions/:mcpSessionId/executions/:executionId`, not the session-less `/api/executions/:id` route. HTTP `POST /api/executions` uses a separate engine instance per request.
</Note>

## Execution lifecycle

```mermaid
stateDiagram-v2
  [*] --> Running: execute / executeWithPause
  Running --> Completed: script finishes
  Running --> Paused: elicitation or policy gate
  Paused --> Running: resume accept (+ content)
  Paused --> Completed: resume decline / cancel
  Paused --> Completed: sandbox timeout / error
  Completed --> [*]
```

An execution returns one of two top-level statuses:

| Status | Meaning |
| --- | --- |
| `completed` | Sandbox finished. Includes `text`, `structured`, and `isError` when the script or a tool failed inside the sandbox. |
| `paused` | Waiting for user interaction. Includes `executionId`, interaction metadata, and resume instructions in `text` / `structured`. |

Paused execution IDs are globally unique (`exec_<uuid>`). The engine keeps a bounded FIFO cache (64 entries) of settled outcomes so duplicate `resume` calls replay the same result instead of returning "not found".

## QuickJS sandbox

Local and self-hosted deployments use `makeQuickJsExecutor()` from `@executor-js/runtime-quickjs`. Cloud uses `makeDynamicWorkerExecutor()` instead; both implement the same `CodeExecutor` interface from `@executor-js/codemode-core`.

### Runtime defaults

| Option | Default | Constraint |
| --- | --- | --- |
| `timeoutMs` | `300_000` (5 minutes) | Minimum `100` |
| `memoryLimitBytes` | `64 * 1024 * 1024` | Per-VM allocation cap |
| `maxStackSizeBytes` | `1 * 1024 * 1024` | Call-stack depth cap |

### Sandbox API

Scripts run inside an async IIFE with these globals:

| Global | Behavior |
| --- | --- |
| `tools` | Lazy proxy. `await tools.github.org.main.getRepo({ owner, repo })` dispatches to the matching executor tool address. |
| `tools.search({ query, namespace?, limit?, offset? })` | Built-in tool discovery (default limit `12`). |
| `tools.describe.tool({ path })` | Returns compact TypeScript input/output shapes for a tool path. |
| `tools.executor.sources.list({ query?, limit?, offset? })` | Lists configured integrations. |
| `emit(value)` | Sends user-visible output (MCP content blocks, `ToolFile`, or text). Emitted items are not returned to the model. |
| `console.log/warn/error/info/debug` | Captured into execution logs. |
| `fetch` | **Disabled** — all network access goes through `tools.*`. |

TypeScript type syntax (`: T`, `as T`, generics) is stripped before evaluation. Decorators and `enum` are not supported.

<CodeGroup>
```typescript title="Example: search then call"
const { items } = await tools.search({ query: "github issues", limit: 12 });
const path = items[0]?.path;
if (!path) return "No matching tools found.";

const details = await tools.describe.tool({ path });
const result = await tools[path]({ title: "Bug report" });
return result;
```

```typescript title="Example: emit file to user"
const attachment = await tools.gmail.org.main.getAttachment({ id: "..." });
if (attachment.ok) emit(attachment.data);
return undefined;
```
</CodeGroup>

## Pause reasons

Executions pause when the elicitation handler is invoked mid-run. Common triggers:

| Trigger | Request type | Typical cause |
| --- | --- | --- |
| Tool `elicit()` | `FormElicitation` or `UrlElicitation` | Missing credentials, OAuth browser flow, structured user input |
| Policy `require_approval` | `FormElicitation` (empty schema) | Owner policy matched the tool path |
| Tool annotation `requiresApproval` | `FormElicitation` (empty schema) | Integration spec marked the operation as sensitive |

### Elicitation request shapes

<ParamField body="FormElicitation" type="object">
Structured input or approval-only gate.

- `message` (string): Human-readable prompt.
- `requestedSchema` (JSON Schema object): Fields to collect. An empty schema means "approve or decline" with no form fields.
</ParamField>

<ParamField body="UrlElicitation" type="object">
Browser redirect flow (OAuth, external approval page).

- `message` (string): Human-readable prompt.
- `url` (string): URL the user must open.
- `elicitationId` (string): Correlation id for the callback.
</ParamField>

### Resume response

<ParamField body="action" type="accept | decline | cancel" required>
How the user responded to the paused interaction.
</ParamField>

<ParamField body="content" type="object">
Required for `accept` on form elicitations with a non-empty `requestedSchema`. Omitted or `{}` for approval-only gates.
</ParamField>

On `decline` or `cancel`, the engine raises `ElicitationDeclinedError` and the sandbox sees a tool failure message.

## HTTP API

All routes are under `/api/executions` and use the pause/resume engine (`executeWithPause`).

| Method | Path | Handler | Purpose |
| --- | --- | --- | --- |
| `POST` | `/api/executions` | `execute` | Run code; return `completed` or `paused` |
| `GET` | `/api/executions/:executionId` | `getPaused` | Inspect a paused execution without resuming |
| `POST` | `/api/executions/:executionId/resume` | `resume` | Submit `action` + optional `content` |

:::endpoint POST /api/executions
Run TypeScript in the QuickJS sandbox. Returns immediately on completion or on the first pause point.
:::

<ParamField body="code" type="string" required>
TypeScript/JavaScript source to execute.
</ParamField>

<ResponseField name="status" type="completed | paused">
Top-level outcome discriminator.
</ResponseField>

<ResponseField name="text" type="string">
Human-readable result or pause instructions (includes `executionId` and resume guidance when paused).
</ResponseField>

<ResponseField name="structured" type="object">
Machine-readable envelope. Completed runs use `{ status: "completed" | "error", result?, error?, logs, emitted? }`. Paused runs use `{ status: "waiting_for_interaction", executionId, interaction: { kind, message, instructions, url?, requestedSchema?, address, args } }`.
</ResponseField>

<ResponseField name="isError" type="boolean">
Present on `completed` responses when the sandbox or a tool returned an error.
</ResponseField>

<RequestExample>
```bash
curl -sS -X POST http://localhost:3001/api/executions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"code":"return await tools.search({ query: \"github\" });"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "status": "completed",
  "text": "{\n  \"items\": [...],\n  \"total\": 3,\n  \"hasMore\": false\n}",
  "structured": {
    "status": "completed",
    "result": { "items": [], "total": 3, "hasMore": false },
    "logs": []
  },
  "isError": false
}
```
</ResponseExample>

:::endpoint POST /api/executions/:executionId/resume
Resume a paused execution. May return another `paused` status if a second elicitation occurs in the same script.
:::

<ParamField body="executionId" type="string" required>
ID from the paused response (`structured.executionId` or `text`).
</ParamField>

<ParamField body="action" type="accept | decline | cancel" required>
User decision.
</ParamField>

<ParamField body="content" type="object">
JSON object matching `requestedSchema` when accepting a form elicitation.
</ParamField>

<ResponseField name="status" type="completed | paused">
Same union as `POST /api/executions`.
</ResponseField>

<RequestExample>
```bash
curl -sS -X POST "http://localhost:3001/api/executions/exec_abc123/resume" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action":"accept","content":{"note":"Approved"}}'
```
</RequestExample>

:::endpoint GET /api/executions/:executionId
Fetch formatted pause details for a known `executionId`. Returns `404 ExecutionNotFoundError` if the id is unknown or already resumed.
:::

## MCP surface

The MCP host registers two tools when elicitation is not fully native:

| Tool | When registered | Input |
| --- | --- | --- |
| `execute` | Always | `{ code: string }` |
| `resume` | `elicitationMode` is `model` or `browser` | See mode below |

### Elicitation modes

<Tabs>
<Tab title="model (default)">
The host calls `engine.executeWithPause(code)`. On pause, the model receives structured interaction metadata and must call `resume` with the user's decision.

`resume` input:
- `executionId` (string)
- `action`: `accept` | `decline` | `cancel`
- `content` (optional JSON string, default `"{}"`)

This is the default for `executor mcp --elicitation-mode model`.
</Tab>

<Tab title="browser">
Same pause flow, but `execute` returns `status: "user_approval_required"` with an `approvalUrl` instead of full interaction details. The model prompts the user to open the URL; `resume` blocks until the browser records a decision (up to 10 minutes).

Local CLI builds approval URLs as `{baseUrl}/resume/{executionId}?mcp_session_id=...`.

`executor mcp --elicitation-mode browser` selects this mode.
</Tab>

<Tab title="native">
The host calls `engine.execute` with an inline MCP `elicitInput` handler. Paused executions never surface `executionId`; the MCP client handles form/url elicitation directly. The `resume` tool is not registered.

Used when the connected MCP client advertises elicitation capabilities and the host opts into native mode.
</Tab>
</Tabs>

<Warning>
Paused MCP executions live in the MCP session's in-memory engine. They expire after a few minutes or when the session/runtime restarts. A missing execution returns `status: "execution_not_found"` with recovery guidance to re-run `execute`.
</Warning>

## CLI

The CLI routes codemode through the HTTP API.

<Steps>
<Step title="Run code or a tool path">
`executor call` and `executor tools search` compile to sandbox code and call `POST /api/executions`.
</Step>

<Step title="Handle a pause">
On `status: "paused"`, the CLI prints pause text, the browser approval URL (`{origin}/resume/{executionId}`), and ready-made `executor resume` commands.
</Step>

<Step title="Resume from the terminal">
```bash
executor resume --execution-id exec_abc123 --action accept
executor resume --execution-id exec_abc123 --action accept --content '{"note":"ok"}'
executor resume --execution-id exec_abc123 --action decline
```
</Step>
</Steps>

Use `--base-url` or `--server` to target a remote Executor instance. Paused state is not persisted across server restarts.

## Result formatting

`formatExecuteResult` and `formatPausedExecution` normalize engine output for HTTP and MCP:

| Field | Completed | Paused |
| --- | --- | --- |
| `text` | Truncated at 30,000 chars. Includes result value, logs, or emit acknowledgment. | Pause message, URL or schema, `executionId`, and step-by-step resume instructions. |
| `structured.status` | `"completed"` or `"error"` | `"waiting_for_interaction"` |
| `isError` | `true` when sandbox `error` is set | Not set (pause is not an error) |

Scripts that only `emit()` without `return` produce `(no return value; N items emitted to the user)` in `text` and an `emitted` count in `structured`.

## SDK embedding

```typescript
import { createExecutor } from "@executor-js/sdk";
import { createExecutionEngine } from "@executor-js/execution";
import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";

const executor = await createExecutor({ onElicitation: "accept-all" });

const engine = createExecutionEngine({
  executor,
  codeExecutor: makeQuickJsExecutor({ timeoutMs: 120_000 }),
});

// Inline elicitation (MCP-native style)
const result = await engine.execute(code, {
  onElicitation: async (ctx) => {
    // render ctx.request, collect user input
    return { action: "accept", content: { field: "value" } };
  },
});

// Pause/resume (HTTP / model-MCP style)
const started = await engine.executeWithPause(code);
if (started.status === "paused") {
  const resumed = await engine.resume(started.execution.id, {
    action: "accept",
    content: { note: "approved" },
  });
}
```

`engine.getDescription` returns the canonical workflow prose (search → describe → call) plus up to 50 connection prefixes for LLM tool selection.

## Failure modes

| Symptom | Likely cause | Recovery |
| --- | --- | --- |
| `ExecutionNotFoundError` (404) | Pause expired, already resumed, or wrong API scope (MCP session vs HTTP) | Re-run `execute`; for MCP pauses, include `mcp_session_id` on `/resume` |
| `No paused execution: exec_...` (MCP) | Session restarted or timeout | Re-run `execute` with the original code |
| `fetch is disabled in QuickJS executor` | Script called `fetch` directly | Route through `tools.*` |
| QuickJS timeout after N ms | Script or pending tool calls exceeded `timeoutMs` | Increase `timeoutMs` or shorten the workflow |
| `Tool requires approval but ... declined` | User chose `decline` or `cancel` on a policy or elicitation gate | Adjust policy or re-run with approval |
| Hang until client timeout | Rare race on fast sandbox failure (fixed via `Effect.raceFirst`) | Upgrade to current engine; verify runtime returns failures promptly |

Set `EXECUTOR_MCP_DEBUG=1` to log MCP elicitation capability snapshots and pause/resume transitions.

## Related pages

<CardGroup>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and how the sandbox `tools` proxy maps paths to executor tools.
</Card>

<Card title="Policies" href="/policies">
Per-tool `require_approval` rules that trigger approval pauses during execution.
</Card>

<Card title="Manage policies" href="/manage-policies">
Handle approval pauses from the web UI, MCP, and CLI resume flow.
</Card>

<Card title="MCP proxy" href="/mcp-proxy">
How Executor exposes `execute` and `resume` as MCP tools in front of integrations.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full route inventory including `executions`, payloads, and error shapes.
</Card>

<Card title="CLI reference" href="/cli-reference">
`executor call`, `executor resume`, and `executor mcp --elicitation-mode` flags.
</Card>

<Card title="Quickstart" href="/quickstart">
End-to-end path from install through a paused execution and resume.
</Card>
</CardGroup>

---

## 10. Add integrations

> Add OpenAPI, GraphQL, and MCP sources from the web UI or CLI, including spec URLs, namespaces, base URLs, and post-add verification with `tools sources`.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/10-add-integrations.md
- Generated: 2026-06-23T19:24:45.539Z

### Source Files

- `README.md`
- `apps/docs/local/cli.mdx`
- `packages/plugins/openapi/src/sdk/plugin.ts`
- `packages/plugins/graphql/src/sdk/plugin.ts`
- `packages/plugins/mcp/src/sdk/plugin.ts`
- `packages/core/api/src/integrations/api.ts`
- `apps/cli/src/integrations.ts`

---
title: "Add integrations"
description: "Add OpenAPI, GraphQL, and MCP sources from the web UI or CLI, including spec URLs, namespaces, base URLs, and post-add verification with `tools sources`."
---

Registering an integration adds a tenant-level catalog entry (slug, description, auth method descriptors). It does not attach credentials. OpenAPI operations are extracted at add time; GraphQL and MCP defer per-connection tool materialization until a connection exists. Add from the web UI **Connect** flow, plugin control tools (`executor.openapi.addSpec`, `executor.graphql.addIntegration`, `executor.mcp.addServer`), or the HTTP API groups under `/openapi`, `/graphql`, and `/mcp`.

```text
  Web UI / CLI / HTTP API
           │
           ▼
  Plugin add handler (addSpec / addIntegration / addServer)
           │
           ▼
  Tenant integration catalog  ──►  connections (separate step)
           │
           ▼
  Tool catalog (OpenAPI: at add; GraphQL/MCP: after connection)
```

<Note>
The catalog slug is the stable integration identity (`slug` in API payloads). The web UI labels this field **Namespace** in identity editors; it maps to the same value as `slug`.
</Note>

## Prerequisites

- A running Executor runtime: `executor install` (durable daemon) or `executor web --foreground` (throwaway foreground runtime).
- Protocol plugins loaded on the runtime (OpenAPI, GraphQL, MCP ship with the default local app).

## Web UI

<Steps>
<Step title="Open the Integrations page">

Run `executor web` and open **Integrations**, or navigate to the integrations route in your deployment.

</Step>

<Step title="Start Connect">

Click **Connect** (or **Connect an integration** when the list is empty). The dialog accepts a preset search, a URL for auto-detection, or a manual plugin type.

</Step>

<Step title="Detect or pick a type">

Paste a URL and click **Detect**. Executor calls `POST /integrations/detect` with `{ "url": "<endpoint>" }` and each loaded plugin returns zero or one `IntegrationDetectionResult` candidates (`kind`, `confidence`, `endpoint`, `name`, `slug`). The UI picks the highest-confidence match and routes to the matching add form with `url` and `namespace` (slug) prefilled.

If detection fails, pick **OpenAPI**, **GraphQL**, or **MCP** under **Or add manually**, or choose a preset from **Popular integrations**.

</Step>

<Step title="Complete the plugin add form">

| Plugin | Form behavior | Registers via |
| --- | --- | --- |
| OpenAPI | Auto-previews the spec (`previewSpec`), lets you set base URL, display name, namespace, description, and auth methods | `POST /openapi/specs` (`addSpec`) |
| GraphQL | Endpoint, identity fields, optional auth methods (apiKey placements) | `POST /graphql/integrations` (`addIntegration`) |
| MCP (remote) | Auto-probes endpoint (`probeEndpoint`), seeds auth from probe, optional stdio tab when enabled | `POST /mcp/servers` (`addServer`) |

Submitting registers the integration and routes to its detail hub. Connection creation is a follow-up step on that page.

</Step>

<Step title="Verify in the UI">

Confirm the new slug appears in the Integrations grid with the expected `kind` and description.

</Step>
</Steps>

### OpenAPI-specific fields

- **Spec**: URL or raw JSON/YAML blob. Wire shape: `{ "kind": "url", "url": "..." }` or `{ "kind": "blob", "value": "..." }`.
- **Base URL**: Required when the spec declares no `servers` or only relative server URLs (for example `"/api/v3"`). When omitted and servers exist, the first resolved server is used.
- **Auth methods**: Derived from the spec on preview; send an explicit `authenticationTemplate` (including `[]`) to override detected methods.

## CLI

`executor call`, `executor tools`, and `executor resume` auto-start the local daemon when needed.

<Tabs>
<Tab title="OpenAPI">

```bash
executor call executor openapi previewSpec '{"spec":"https://petstore3.swagger.io/api/v3/openapi.json"}'
```

```bash
executor call executor openapi addSpec '{
  "spec": { "kind": "url", "url": "https://petstore3.swagger.io/api/v3/openapi.json" },
  "slug": "petstore",
  "baseUrl": "https://petstore3.swagger.io/api/v3"
}'
```

<ParamField body="spec" type="object" required>
OpenAPI document source. `{ "kind": "url", "url": string }` or `{ "kind": "blob", "value": string }`.
</ParamField>

<ParamField body="slug" type="string" required>
Catalog slug for the new integration.
</ParamField>

<ParamField body="baseUrl" type="string">
Override base URL when the spec uses relative `servers` entries or you need a non-default host.
</ParamField>

<ParamField body="description" type="string">
Agent-visible catalog description.
</ParamField>

<ParamField body="authenticationTemplate" type="array">
Auth method inputs. Omit to derive from spec security schemes; pass `[]` for no auth methods.
</ParamField>

<ResponseField name="slug" type="string">
Registered integration slug.
</ResponseField>

<ResponseField name="toolCount" type="number">
Operations extracted from the spec at registration time.
</ResponseField>

</Tab>

<Tab title="GraphQL">

```bash
executor call executor graphql addIntegration '{
  "endpoint": "https://api.example.com/graphql",
  "slug": "example_graphql",
  "name": "Example GraphQL"
}'
```

<ParamField body="endpoint" type="string" required>
GraphQL HTTP endpoint URL.
</ParamField>

<ParamField body="slug" type="string">
Catalog slug. When omitted, derived from the endpoint hostname.
</ParamField>

<ParamField body="introspectionJson" type="string">
Static introspection JSON when live introspection is disabled on the endpoint.
</ParamField>

<ParamField body="authenticationTemplate" type="array">
Declared apiKey header/query placements for connections.
</ParamField>

<ResponseField name="slug" type="string">
Registered integration slug.
</ResponseField>

<ResponseField name="name" type="string">
Display name stored on the integration.
</ResponseField>

</Tab>

<Tab title="MCP">

Probe before add when the server requires OAuth or custom headers:

```bash
executor call executor mcp probeEndpoint '{"endpoint":"https://mcp.example.com/mcp"}'
```

```bash
executor call executor mcp addServer '{
  "transport": "remote",
  "name": "Example MCP",
  "endpoint": "https://mcp.example.com/mcp",
  "slug": "example_mcp"
}'
```

Remote stdio registration (when stdio is enabled on the runtime):

```bash
executor call executor mcp addServer '{
  "transport": "stdio",
  "name": "Local MCP",
  "command": "npx",
  "args": ["-y", "some-mcp-server"]
}'
```

<ParamField body="endpoint" type="string">
Remote MCP URL (remote transport).
</ParamField>

<ParamField body="remoteTransport" type="string">
`streamable-http`, `sse`, or `auto` (default).
</ParamField>

<ParamField body="command" type="string">
Executable for stdio transport.
</ParamField>

<ParamField body="authenticationTemplate" type="array">
Auth methods connections can apply (OAuth, apiKey, none).
</ParamField>

<ResponseField name="slug" type="string">
Registered integration slug.
</ResponseField>

</Tab>
</Tabs>

<Warning>
`addSpec`, `addIntegration`, and `addServer` are annotated `requiresApproval: true`. A CLI or MCP invocation may pause until approved. Resume with `executor resume --execution-id <id>`.
</Warning>

## HTTP API

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/integrations/detect` | URL type detection across loaded plugins |
| `POST` | `/openapi/preview` | Preview spec servers, auth, operation count |
| `POST` | `/openapi/specs` | Register OpenAPI integration |
| `POST` | `/graphql/integrations` | Register GraphQL integration |
| `POST` | `/mcp/probe` | Probe remote MCP endpoint shape and auth |
| `POST` | `/mcp/servers` | Register MCP integration (remote or stdio) |
| `GET` | `/integrations` | List tenant integrations |

:::endpoint POST /integrations/detect
Detect integration type from a URL. Payload: `{ "url": string }` (max 2048 chars). Returns an array of detection results sorted by confidence tier (`high`, `medium`, `low`).
:::

:::endpoint POST /openapi/specs
Register an OpenAPI integration. Returns `{ slug, toolCount }`. Re-adding an existing slug returns `IntegrationAlreadyExistsError` (409).
:::

## Verify with `tools sources`

After adding, list configured integrations and tool counts:

```bash
executor tools sources
```

Optional filters:

```bash
executor tools sources --query petstore --limit 20
```

The command executes `tools.executor.sources.list({ query?, limit?, offset? })` inside the runtime sandbox. Each item includes:

| Field | Meaning |
| --- | --- |
| `id` / `name` | Integration slug |
| `kind` | Owning plugin (`openapi`, `graphql`, `mcp`, …) |
| `toolCount` | Tools currently in the catalog for that integration |
| `canRemove` / `canRefresh` | Whether the integration supports removal or spec refresh |
| `description` | Catalog description when it adds information beyond the slug |

<Info>
OpenAPI integrations typically show `toolCount > 0` immediately after `addSpec`. GraphQL and MCP integrations often show `toolCount: 0` until you create a connection; tools are materialized per connection at create/refresh time.
</Info>

<RequestExample>

```bash
executor tools sources
```

</RequestExample>

<ResponseExample>

```json
{
  "items": [
    {
      "id": "petstore",
      "name": "petstore",
      "kind": "openapi",
      "toolCount": 19,
      "canRemove": true,
      "canRefresh": true
    }
  ],
  "hasMore": false
}
```

</ResponseExample>

Follow-up discovery:

```bash
executor tools search "list pets" --namespace petstore
executor call petstore --help
```

## Errors and constraints

| Error | Cause | Recovery |
| --- | --- | --- |
| `integration_already_exists` | Slug collision | Pick a new slug or update the existing integration |
| `openapi_parse_failed` / `openapi_extraction_failed` | Invalid or unsupported OpenAPI document | Fix the spec URL or paste valid JSON/YAML |
| `graphql_introspection_failed` | Endpoint unreachable or introspection disabled | Supply `introspectionJson` or fix endpoint/auth |
| `mcp_connection_failed` | MCP probe/connect failed | Check endpoint URL, transport, and headers |
| Paused execution | Approval gate on add tools | `executor resume --execution-id <id>` |

Duplicate slugs are blocked at the API layer (`IntegrationAlreadyExistsError`, HTTP 409) to avoid clobbering existing connections and policies.

## Related pages

<CardGroup>
<Card title="Integrations" href="/integrations">
Tenant-level catalog identities, detection, and auth method descriptors.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Create connections and attach credentials after registering an integration.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and invocation after integrations are registered.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `executor` subcommands including `call`, `tools`, and `resume`.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Integrations, connections, and plugin route groups.
</Card>
</CardGroup>

---

## 11. Connect MCP clients

> Wire Cursor, Claude Code, OpenCode, and other MCP clients via stdio (`executor mcp`) or streamable HTTP, including `add-mcp` setup and client restart requirements.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/11-connect-mcp-clients.md
- Generated: 2026-06-23T19:24:38.361Z

### Source Files

- `apps/docs/local/cli.mdx`
- `apps/docs/mcp-proxy.mdx`
- `apps/cli/src/main.ts`
- `packages/hosts/mcp/src/tool-server.ts`
- `packages/core/api/src/server/mcp-build.ts`
- `apps/local/package.json`

---
title: "Connect MCP clients"
description: "Wire Cursor, Claude Code, OpenCode, and other MCP clients via stdio (`executor mcp`) or streamable HTTP, including `add-mcp` setup and client restart requirements."
---

Executor exposes one MCP endpoint that fronts your full tool catalog. Clients connect over **streamable HTTP** at `/mcp` on a running Executor server, or over **stdio** by launching `executor mcp` as a subprocess. The web UI **Connect an agent** card and `npx add-mcp` generate copy-paste setup for Cursor, Claude Code, OpenCode, and other MCP clients.

## Prerequisites

<Steps>
  <Step title="Install and start Executor">

Run the durable local service (or use a hosted deployment):

```bash
executor install
executor daemon status
```

For a throwaway foreground runtime instead, run `executor web --foreground`.

  </Step>
  <Step title="Confirm the MCP surface is reachable">

Local and self-hosted runtimes serve streamable HTTP at `<origin>/mcp`. Cloud runtimes use `<origin>/<org-slug>/mcp`. The **Connect an agent** card on the Integrations page prints the exact URL for your deployment.

An unauthenticated `POST` to the MCP URL should return **401** (auth challenge), not **404**. A 404 usually means the path or org segment is wrong.

  </Step>
</Steps>

## Choose a transport

| | Remote HTTP | Standard I/O (stdio) |
| --- | --- | --- |
| Client config | URL to `<origin>/mcp` | Command: `executor mcp` |
| Requires running daemon | Yes (`executor install` or `executor daemon run`) | No; CLI starts a foreground local server |
| Auth | Bearer token (local) or OAuth (cloud/self-host) | Built into the spawned process |
| Approval flow | `browser`, `model`, or `native` via `?elicitation_mode=` | `model` (default) or `browser` via `--elicitation-mode` |
| Desktop app | Supported (HTTP only) | Not shown; desktop routes through the sidecar over HTTP |

<Info>
After you add or change an MCP server entry, most clients only load servers at startup. Restart the client or open a new chat session before expecting `execute` and `resume` tools to appear.
</Info>

## Connect with `add-mcp`

`npx add-mcp` detects the installed MCP client and writes its config. Use the command from the **Connect an agent** card when possible; it already includes your origin, org slug (cloud), auth header (when needed), and elicitation mode.

<Tabs>
  <Tab title="Remote HTTP">

```bash
npx add-mcp http://127.0.0.1:4789/mcp --transport http --name executor
```

When the web UI has an active bearer session, the generated command may append an auth header:

```bash
npx add-mcp http://127.0.0.1:4789/mcp --transport http --name executor --header 'Authorization: Bearer <token>'
```

On Executor Cloud, the URL is org-scoped:

```bash
npx add-mcp https://executor.example/acme-corp/mcp --transport http --name executor
```

  </Tab>
  <Tab title="Standard I/O">

Requires the published `executor` CLI on your `PATH`:

```bash
npx add-mcp "executor mcp" --name executor
```

Pin a workspace scope (directory containing `executor.jsonc`):

```bash
npx add-mcp "executor mcp --scope /path/to/workspace" --name executor
```

Browser approval over stdio:

```bash
npx add-mcp "executor mcp --elicitation-mode browser" --name executor
```

  </Tab>
</Tabs>

<Warning>
Restart your MCP client after running `add-mcp`. Until the client reloads its server list, Executor tools will not be available in the session.
</Warning>

## Use the Connect card

The **Connect an agent** card on the Integrations page is the canonical source for install commands:

- **Remote HTTP** is the default tab. It targets streamable HTTP at your server's `/mcp` path (or `/<org-slug>/mcp` on cloud).
- **Standard I/O** appears on local runtimes where the `executor` CLI is on `PATH`. The desktop app shows HTTP only because it does not ship a global `executor` binary.
- **Advanced → Resume approvals** sets how paused tool calls are handled (see [Elicitation modes](#elicitation-modes) below).

Executor Cloud also surfaces MCP setup on `/setup-mcp` during onboarding.

## Manual client configuration

If you prefer to edit config files directly, use the same transport and URL the Connect card would generate.

### Claude Code / Cursor (stdio)

```json
{
  "mcpServers": {
    "executor": {
      "command": "executor",
      "args": ["mcp"]
    }
  }
}
```

With scope and browser approval:

```json
{
  "mcpServers": {
    "executor": {
      "command": "executor",
      "args": ["mcp", "--scope", "/path/to/workspace", "--elicitation-mode", "browser"]
    }
  }
}
```

### OpenCode (remote HTTP)

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "executor": {
      "type": "remote",
      "url": "http://127.0.0.1:4789/mcp",
      "enabled": true
    }
  }
}
```

Replace the URL with your deployment's MCP endpoint. On cloud, use `https://<host>/<org-slug>/mcp`.

## MCP endpoint reference

:::endpoint POST /mcp
Streamable-HTTP MCP transport. Creates a session on the first `POST` without an `mcp-session-id` header; subsequent requests forward to the active session. Also accepts `GET`, `DELETE`, and `OPTIONS` (CORS preflight).
:::

Local runtimes gate `/mcp` with a bearer token (401 when missing). Cloud and self-hosted deployments authenticate through their OAuth layer (401 challenge with protected-resource metadata).

<ParamField body="mcp-session-id" type="string">
Session identifier returned by the transport. Required on follow-up requests after session creation.
</ParamField>

<ParamField body="elicitation_mode" type="string">
Query parameter on the MCP URL. One of `browser`, `model`, or `native`. Defaults to `model`. Legacy alias: `allow_model_resume=true` maps to `model`.
</ParamField>

### Default ports by runtime

| Runtime | Typical port | MCP path |
| --- | --- | --- |
| `executor install` (supervised daemon) | `4789` | `/mcp` |
| `executor daemon run` | `4788` | `/mcp` |
| Docker self-host | `17888` | `/mcp` |
| Executor Cloud | HTTPS default | `/<org-slug>/mcp` |

The CLI may pick another free port when the default is busy. Use `executor daemon status` or the Connect card for the live origin; do not hard-code a port unless you pin it yourself.

## `executor mcp` (stdio)

```bash
executor mcp [--scope <dir>] [--elicitation-mode browser|model]
```

<ParamField body="--scope" type="string">
Sets `EXECUTOR_SCOPE_DIR` to a workspace directory containing `executor.jsonc`.
</ParamField>

<ParamField body="--elicitation-mode" type="browser | model" default="model">
`model` exposes a `resume` tool the agent calls with accept/decline/cancel. `browser` returns an approval URL and waits for a browser decision before resuming.
</ParamField>

Stdio behavior:

- The CLI reroutes `stdout` logging to `stderr` so MCP JSON-RPC on stdout stays clean.
- It starts a foreground local HTTP server, publishes a local server manifest, connects an MCP server over `StdioServerTransport`, and tears down when the client closes stdin or sends `SIGINT`/`SIGTERM`.
- Stdio does not support `native` elicitation; use Remote HTTP with `?elicitation_mode=native` for that mode.

## Elicitation modes

Paused executions (auth or policy approval) behave differently per mode:

| Mode | Transport | Behavior |
| --- | --- | --- |
| `model` (default) | HTTP query or stdio default | Agent calls the `resume` MCP tool with `executionId`, `action`, and optional `content` |
| `browser` | HTTP query or stdio `--elicitation-mode` | `execute` returns an `approvalUrl`; user approves in the web UI; agent calls `resume` to wait for the decision |
| `native` | HTTP query only | Executor uses the client's native MCP elicitation capability during `execute` |

HTTP examples:

```
http://127.0.0.1:4789/mcp?elicitation_mode=browser
https://executor.example/acme-corp/mcp?elicitation_mode=native
```

## MCP tools exposed to clients

Every Executor MCP session registers:

| Tool | Always present | Purpose |
| --- | --- | --- |
| `execute` | Yes | Run TypeScript against the tool catalog (discover, call, handle pauses) |
| `resume` | When elicitation mode is not `native` | Continue a paused execution |

New integrations added in the web UI or CLI appear in the catalog automatically; you do not reconfigure the MCP client per integration.

## Verify the connection

<Steps>
  <Step title="Restart or reload the client">

Quit and reopen the MCP client, or start a new chat/agent session, so it reloads server definitions.

  </Step>
  <Step title="Confirm tools are listed">

In the client, check that `execute` (and `resume` when applicable) appear in the MCP tool list.

  </Step>
  <Step title="Probe from the CLI">

```bash
executor tools sources
executor tools search "list issues"
```

These commands auto-start the local daemon when needed.

  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Tools do not appear after add-mcp">

Most MCP clients load servers only at startup. Restart the client or open a new chat. If you edited config manually, confirm JSON syntax and that the transport (`http` URL vs `command`/`args`) matches your chosen mode.

  </Accordion>
  <Accordion title="HTTP 401 on /mcp">

Local HTTP requires a bearer token on every `/mcp` request. Copy the `--header 'Authorization: Bearer …'` flags from the Connect card, or read the token from `auth.json` under your `EXECUTOR_DATA_DIR`. Cloud and self-hosted endpoints expect OAuth; complete the client authorization flow when prompted.

  </Accordion>
  <Accordion title="HTTP 404 on the Connect card URL">

On self-host, the server serves bare `/mcp` but the card may print `/<org-id>/mcp` for parity with cloud routing. The self-host front-end strips the org segment; if you still see 404, confirm you are hitting the correct origin and that the daemon is running (`executor daemon status`).

  </Accordion>
  <Accordion title="stdio: executor not found">

Install the global CLI (`npm install -g executor`) or switch to Remote HTTP. On monorepo dev checkouts, the Connect card uses `bun run dev:cli mcp` instead of `executor mcp`.

  </Accordion>
  <Accordion title="Paused execution not found on resume">

Paused executions live in the session runtime and expire after a few minutes or when the host restarts. Re-run `execute` with the original code to obtain a fresh `executionId`.

  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
  <Card title="MCP proxy" href="/mcp-proxy">
    How Executor fronts OpenAPI, GraphQL, and upstream MCP integrations behind one endpoint.
  </Card>
  <Card title="Add integrations" href="/add-integrations">
    Register sources so they appear in the MCP catalog after you connect a client.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full `executor mcp`, `daemon`, and `install` flags and behavior.
  </Card>
  <Card title="Hosted cloud" href="/hosted-cloud">
    Org-scoped MCP URLs and OAuth on Executor Cloud.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Daemon port conflicts, unreachable servers, and recovery commands.
  </Card>
</CardGroup>

---

## 12. Configure credentials

> Credential providers (file secrets, keychain, 1Password, encrypted stores), connection creation payloads, OAuth flows, and placement-based auth templates.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/12-configure-credentials.md
- Generated: 2026-06-23T19:24:35.489Z

### Source Files

- `packages/core/api/src/oauth/api.ts`
- `packages/core/api/src/handlers/oauth.ts`
- `packages/core/sdk/src/oauth-service.ts`
- `packages/plugins/file-secrets/src/sdk/plugin.ts`
- `packages/plugins/keychain/src/sdk/plugin.ts`
- `packages/plugins/onepassword/src/sdk/plugin.ts`
- `packages/core/sdk/src/http-auth.ts`

---
title: "Configure credentials"
description: "Credential providers (file secrets, keychain, 1Password, encrypted stores), connection creation payloads, OAuth flows, and placement-based auth templates."
---

Executor stores credentials as **connections**: owner-scoped bindings to one integration, one auth template, and one credential provider. Static secrets are created with `connections.create` (HTTP `POST /connections`, SDK `executor.connections.create`); OAuth connections are minted through `oauth.start` / `oauth.complete`. Protocol plugins declare **placement-based auth templates** via `@executor-js/sdk/http-auth`; the runtime resolves credential values lazily per tool call.

## Credential providers

A `CredentialProvider` is where secret material lives. The connection row stores routing (`provider` key plus opaque `item_ids` per template variable), not the secret itself. Providers register at executor startup from `createExecutor({ providers: [...] })` and from each plugin's `credentialProviders` hook. Config-level providers register first and the **first writable provider** becomes the default store for pasted `value` / `values` inputs.

| Provider key | Package | Writable | Storage | `list` support |
|---|---|---|---|---|
| `file` | `@executor-js/plugin-file-secrets` | yes | Flat JSON map at `{XDG_DATA_HOME}/executor/auth.json` (mode `0600`) | yes |
| `keychain` | `@executor-js/plugin-keychain` | yes | OS keychain (`@napi-rs/keyring`, service name default `executor`) | no |
| `onepassword` | `@executor-js/plugin-onepassword` | no (read-only) | `op://` URIs scoped to a configured vault | yes |
| `workos-vault` | `@executor-js/plugin-workos-vault` | yes | WorkOS Vault (KEK-partitioned encrypted objects) | yes |

<Note>
The keychain plugin probes write/delete at startup. If the platform keychain is unreachable (headless CI, WSL without secret-service), the provider is skipped rather than registered.
</Note>

### File secrets (`file`)

Secrets are a flat map of opaque id to value. The v2 model drops per-scope partitioning; the connection row owns the `(tenant, owner, subject)` partition.

```json
{
  "github-token": "ghp_xxx",
  "connection:org:inventory:default:token": "inventory-api-key"
}
```

Override the directory with `fileSecretsPlugin({ directory: "/path/to/dir" })`. The extension exposes `executor.fileSecrets.filePath`.

### Keychain (`keychain`)

Each `ProviderItemId` is the keychain account name. Override the service name with `keychainPlugin({ serviceName: "my-app" })`.

### 1Password (`onepassword`)

Configure once per owner partition via the plugin tools (`onepassword.configure`) or extension API. Auth modes:

- `desktop-app` with `accountName` (local biometric)
- `service-account` with `token`

Item ids are either fully qualified `op://vault/item/field` URIs or bare item ids scoped to the configured vault. The provider resolves secrets on read; it never writes.

### WorkOS Vault (`workos-vault`)

Cloud-hosted encrypted storage for self-host and Executor Cloud. Requires `workosVaultPlugin({ credentials: { apiKey, clientId } })` or a pre-built client. Object names encode owner partitions (`connection:<owner>:...`, `oauth:<owner>:...`).

### Register providers in application code

<CodeGroup>
```typescript title="SDK (promise mode)"
import { createExecutor, ProviderKey, ProviderItemId, Effect } from "@executor-js/sdk/promise";
import { fileSecretsPlugin, keychainPlugin, onepasswordPlugin } from "...";

const memory = new Map<string, string>();
const memoryProvider = {
  key: ProviderKey.make("memory"),
  writable: true,
  get: (id) => Effect.sync(() => memory.get(String(id)) ?? null),
  set: (id, value) => Effect.sync(() => { memory.set(String(id), value); }),
};

const executor = await createExecutor({
  plugins: [fileSecretsPlugin(), keychainPlugin(), onepasswordPlugin()],
  providers: [memoryProvider], // registers first; wins as default writable
});
```

```typescript title="all-plugins reference"
// examples/all-plugins wires every published provider + protocol plugin.
// Uncomment workosVaultPlugin when WORKOS_API_KEY is available.
```
</CodeGroup>

Discover registered backends:

| Endpoint | Returns |
|---|---|
| `GET /providers` | `ProviderKey[]` |
| `GET /providers/:key/items` | `{ id, name }[]` for browse-and-pick UIs |

## Connection creation payloads

A connection is identified by `(owner, integration, name)` where `owner` is `org` (workspace-shared) or `user` (personal). The `template` field selects one auth method slug from the integration catalog.

### Credential origin (exactly one)

The HTTP API enforces one origin per create request. The SDK accepts the same shapes plus a per-variable `inputs` map.

<ParamField body="value" type="string">
Single pasted secret. Sugar for the `token` variable. Written to the default writable provider at `connection:{owner}:{integration}:{name}:token`.
</ParamField>

<ParamField body="values" type="Record<string, string>">
One pasted value per placement variable. Required for multi-input methods (for example Datadog `api_token` + `team_id`).
</ParamField>

<ParamField body="from" type="{ provider: ProviderKey; id: ProviderItemId }">
Reference an external provider entry. The value is resolved on demand; Executor stores only the opaque id.
</ParamField>

<ParamField body="inputs" type="Record<string, ConnectionInputOrigin>" required={false}>
SDK-only canonical form. Mixes `{ value }` and `{ from: { provider, id } }` per variable. HTTP create does not expose this field yet.
</ParamField>

### Common fields

<ParamField body="owner" type="Owner" required>
`org` or `user`. Personal connections require a signed-in subject.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Unique per `(owner, integration)`.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Catalog slug for the OpenAPI, GraphQL, or MCP source.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
Auth method slug (`apiKey`, `bearer`, `oauth2`, `none`, ...).
</ParamField>

<ParamField body="identityLabel" type="string | null">
Optional human label (which account).
</ParamField>

<ParamField body="description" type="string | null">
Agent-visible context surfaced in `connections.list` and execute-tool inventory.
</ParamField>

### Constraints

- A credentialed template requires at least one input. Exception: `template: "none"` for public integrations (empty `values: {}` is valid).
- Cannot mix pasted and external inputs in one connection.
- All external inputs must use the same provider.
- OAuth connections are minted via `oauth.start`, not `connections.create`.

<RequestExample>
```json title="POST /connections — single API key"
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key"
}
```
</RequestExample>

<RequestExample>
```json title="POST /connections — multi-input method"
{
  "owner": "org",
  "name": "mixed",
  "integration": "datadog",
  "template": "token_and_team",
  "values": {
    "api_token": "tok_mixed",
    "team_id": "team_42"
  }
}
```
</RequestExample>

<RequestExample>
```json title="POST /connections — external provider reference"
{
  "owner": "org",
  "name": "prod",
  "integration": "github",
  "template": "apiKey",
  "from": {
    "provider": "onepassword",
    "id": "op://VaultName/GitHub/item/password"
  }
}
```
</RequestExample>

<ResponseExample>
```json title="Connection response"
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "provider": "file",
  "address": "tools.inventory.org.default",
  "identityLabel": null,
  "description": null,
  "expiresAt": null,
  "oauthClient": null,
  "oauthClientOwner": null,
  "oauthScope": null
}
```
</ResponseExample>

Creating a connection writes the credential, persists the connection row, and **produces per-connection tools** for that integration.

## Placement-based auth templates

Protocol plugins (OpenAPI, GraphQL, MCP) share the `@executor-js/sdk/http-auth` vocabulary. An integration declares a **list** of auth methods; a connection binds one by `template` slug.

### ApiKey methods

Each `apikey` method carries `placements`: spots on the outbound HTTP request.

| Placement field | Role |
|---|---|
| `carrier` | `header` or `query` |
| `name` | Header or query param name |
| `prefix` | Literal prepended to the credential (for example `Bearer `) |
| `variable` | Credential input name; defaults to `token` |
| `literal` | Static value with no credential input |

Core resolves `values: Record<variable, string|null>` and plugins render via `renderAuthPlacements`. Use `requiredPlacementVariables(placements)` to know which inputs a connection must supply.

### Authoring dialect

Plugin registration inputs accept a request-shaped template normalized at the boundary:

```typescript
import { variable } from "@executor-js/sdk/http-auth";

authenticationTemplate: [
  {
    slug: "token_and_team",
    type: "apiKey",
    headers: { Authorization: ["Bearer ", variable("api_token")] },
    queryParams: { team_id: [variable("team_id")] },
  },
  { slug: "bearer", type: "apiKey", headers: { Authorization: ["Bearer ", variable("token")] } },
  { kind: "oauth2" }, // OAuth is per-plugin, not in placements model
]
```

Rules for template values:

- A parts-array renders at most **one** variable, and it must be the **final** part.
- Two placements sharing a `variable` share one credential input.
- Distinct variables get distinct inputs (and a `values` map at connect time).

### OAuth methods

OAuth is **not** modeled as placements. Each protocol plugin carries its own OAuth variant (`kind: "oauth2"` for OpenAPI/MCP, with plugin-specific endpoint/scope fields). OAuth-refreshed connections resolve only the `token` input; do not mix OAuth token values into apikey placement methods.

### None auth

`{ slug: "none", kind: "none" }` integrations need no credential. Create with `template: "none"` and no value origin.

## OAuth flows

OAuth is a credential mechanism separate from integration type. The v2 model:

1. **Register an OAuth app** (`oauth_client` row) with endpoints and client credentials.
2. **Start a flow** through that app to mint a connection for one integration.
3. **Complete** the authorization code exchange (or connect inline for `client_credentials`).

```mermaid
sequenceDiagram
  participant UI as Web UI / client
  participant API as Executor HTTP API
  participant OAuth as oauth service
  participant AS as Authorization server
  participant CP as Credential provider

  UI->>API: POST /oauth/clients (createClient)
  API->>OAuth: createClient
  OAuth->>CP: set oauth-client:{owner}:{slug}:secret
  UI->>API: POST /oauth/start
  API->>OAuth: start
  alt client_credentials
    OAuth->>AS: token exchange
    OAuth->>CP: set oauth:{owner}:{integration}:{name}
    OAuth-->>UI: { status: "connected", connection }
  else authorization_code
    OAuth->>OAuth: persist oauth_session + PKCE
    OAuth-->>UI: { status: "redirect", authorizationUrl, state }
    UI->>AS: user authorizes
    AS->>API: GET /oauth/callback?state&code
    API->>OAuth: complete
    OAuth->>AS: code exchange
    OAuth->>CP: set access + refresh tokens
    OAuth-->>UI: connection (via popup postMessage)
  end
```

### OAuth HTTP endpoints

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/oauth/clients` | Register owner-scoped OAuth app |
| `POST` | `/oauth/clients/register-dynamic` | RFC 7591 Dynamic Client Registration |
| `GET` | `/oauth/clients` | List visible apps (no secrets) |
| `DELETE` | `/oauth/clients/:slug` | Remove app (connections fail at next refresh) |
| `POST` | `/oauth/start` | Begin flow; returns `connected` or `redirect` |
| `POST` | `/oauth/complete` | Exchange code, mint connection |
| `POST` | `/oauth/cancel` | Drop in-flight session |
| `POST` | `/oauth/probe` | Discover AS metadata for onboarding |
| `GET` | `/oauth/callback` | Browser callback; renders popup HTML |

### Start payload

<ParamField body="client" type="OAuthClientSlug" required>
OAuth app slug.
</ParamField>

<ParamField body="clientOwner" type="Owner" required>
Owner of the OAuth app. A personal connection may use a shared workspace app (`clientOwner: "org"`, `owner: "user"`). Workspace connections cannot use a personal app.
</ParamField>

<ParamField body="owner" type="Owner" required>
Connection owner to mint.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Connection name.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Target integration.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
OAuth template slug (for example `oauth2`).
</ParamField>

<ParamField body="redirectUri" type="string | null">
Per-flow override. Defaults to executor `redirectUri` (`${webBaseUrl}${mountPrefix}/oauth/callback`). Required for `authorization_code` flows; hosts without a callback must fail loudly.
</ParamField>

Requested scopes come from the integration's **declared** OAuth scopes for `(integration, template)`, not from the OAuth app row.

### Token storage layout

| Item id pattern | Content |
|---|---|
| `oauth:{owner}:{integration}:{name}` | Access token |
| `oauth:{owner}:{integration}:{name}:refresh` | Refresh token |
| `oauth-client:{owner}:{slug}:secret` | OAuth app client secret |

The connection row records `oauthClient`, `oauthClientOwner`, `oauthScope`, `expiresAt`, and optional regional `oauthTokenUrl` override (Datadog multi-site).

### Dynamic Client Registration

`POST /oauth/clients/register-dynamic` mints a public PKCE client via the server's `registration_endpoint`. The user supplies no client id/secret. Auth method negotiation: `none` when advertised or empty; otherwise `client_secret_post`.

### Probe

`POST /oauth/probe` with `{ url }` tries RFC 9728 protected-resource metadata first, then RFC 8414/OIDC authorization-server metadata. Returns `authorizationUrl`, `tokenUrl`, `scopesSupported`, `registrationEndpoint`, and related fields for UI pre-fill.

<Warning>
`redirectUri` has no localhost default. An executor constructed without it rejects redirect-requiring flows instead of registering a wrong callback URL with providers.
</Warning>

## Runtime resolution

On each tool invocation, the executor:

1. Loads the connection row and resolves the registered `CredentialProvider`.
2. For OAuth connections with expired (or near-expired) tokens, refreshes first (shared in-flight gate prevents parallel refresh races).
3. Fetches each `item_ids[variable]` from the provider.
4. Passes `values` to the protocol plugin, which renders placements onto the outbound request.

Errors surface as `CredentialProviderNotRegisteredError` (HTTP 409), `InvalidConnectionInputError` (HTTP 400), or `CredentialResolutionError` with `reauthRequired: true` when refresh fails.

## Configure from the web UI

The connect modal mirrors the HTTP payloads:

1. Pick owner (`org` / `user`), connection name, and auth method from the integration catalog.
2. For apikey methods, fill one field per `requiredPlacementVariables` entry (or browse external provider items via `GET /providers/:key/items`).
3. For OAuth, select an OAuth app, call `POST /oauth/start`, complete the browser redirect, and receive the minted connection.
4. Verify with `connections.list` or `tools.list` filtered by integration.

Provider choice is automatic: pasted values go to the default writable provider; `from` references route to the named external provider.

## Failure modes

| Symptom | Likely cause | Recovery |
|---|---|---|
| `Credential provider "default" is not registered` | No writable provider registered | Add `fileSecretsPlugin`, `keychainPlugin`, inline provider, or `workos-vault` |
| `Expected exactly one credential origin` | Multiple of `value`, `values`, `from` in one payload | Send exactly one origin field |
| `A connection cannot mix pasted and external-provider inputs` | Mixed `value` + `from` (or `inputs` mix) | Use all pasted or all external per connection |
| `OAuth redirect flow requires a configured redirectUri` | Host missing callback URL | Set `redirectUri` on `createExecutor`; local default is `{webBaseUrl}/api/oauth/callback` |
| `OAuth session expired or not found` | TTL exceeded or cancelled session | Restart with `POST /oauth/start` |
| Keychain provider missing | Probe failed at startup | Use `file` provider or fix secret-service on Linux |
| 1Password returns null | Not configured or URI outside vault | Run `onepassword.configure`; check vault scope |

## Related pages

<CardGroup>
<Card title="Connections" href="/connections">
Owner-scoped credential identity, provider resolution, and the `(owner, integration, name)` model.
</Card>

<Card title="Integrations" href="/integrations">
How auth method descriptors are declared and projected into the catalog.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full route listings for `connections`, `providers`, and `oauth` groups.
</Card>

<Card title="Embed with the SDK" href="/embed-sdk">
Compose `createExecutor` with plugins and credential providers in application code.
</Card>

<Card title="Plugin catalog" href="/plugin-catalog">
Published provider plugins and how `examples/all-plugins` wires them.
</Card>

<Card title="Configuration reference" href="/configuration-reference">
`EXECUTOR_DATA_DIR`, `EXECUTOR_WEB_BASE_URL`, and OAuth callback derivation.
</Card>
</CardGroup>

---

## 13. Manage policies

> Create and update owner-scoped tool policies, interpret effective policy precedence, and handle approval pauses from the web UI, MCP, and CLI resume flow.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/13-manage-policies.md
- Generated: 2026-06-23T19:25:56.731Z

### Source Files

- `apps/docs/concepts/policies.mdx`
- `packages/core/api/src/policies/api.ts`
- `packages/core/api/src/handlers/policies.ts`
- `packages/core/sdk/src/policies.ts`
- `packages/hosts/mcp/src/browser-approval.ts`
- `packages/hosts/mcp/src/browser-approval-store.ts`

---
title: "Manage policies"
description: "Create and update owner-scoped tool policies, interpret effective policy precedence, and handle approval pauses from the web UI, MCP, and CLI resume flow."
---

Executor persists owner-scoped tool policies in the `tool_policy` table and resolves them on every `tools.list` and `execute` call through `resolveEffectivePolicy`. Policies are created and updated through the web UI at `/policies`, the HTTP `policies` API group, SDK `executor.policies` methods, and built-in core tools (`executor.coreTools.policies.*`). When a matched action is `require_approval`, invocation pauses until a human approves via the console `/resume/:executionId` page, the MCP `resume` tool, or `executor resume`.

## Policy actions

| Action | At list time | At invoke time |
|--------|--------------|----------------|
| `approve` | Tool appears in search and catalog | Runs without an approval prompt |
| `require_approval` | Tool appears | Pauses for human approval before running |
| `block` | Hidden from search (unless `includeBlocked`) | Fails with `ToolBlockedError` |

<Note>
The HTTP API and SDK schema use `approve`, not `allow`. User-facing labels in the web UI map `approve` to **Allow**.
</Note>

## Owner scopes

Each policy row is scoped to an `owner`:

| Owner | Web UI label | Role |
|-------|--------------|------|
| `org` | Workspace (or **Local** on single-player hosts) | Outer guardrail shared across the workspace |
| `user` | Personal | Inner preference for the signed-in member |

Org and user each contribute at most one matched rule per tool. The final action is the **most restrictive** match across owners: `block` beats `require_approval`, which beats `approve`. A user `approve` cannot weaken an org `block`; a user `require_approval` can strengthen an org `approve`.

Within a single owner, rules are ordered by `position` (fractional-indexing key). Lower lexicographic `position` means higher precedence. The first matching pattern for that owner wins.

## Pattern grammar

Policies match against the full tool address:

```text
<integration>.<owner>.<connection>.<tool>
```

Static core tools (for example `executor.coreTools.policies.list`) match on their fqid address directly.

| Pattern form | Example | Matches |
|--------------|---------|---------|
| Universal | `*` | Every tool |
| Integration subtree | `vercel.*` | Any tool under `vercel` |
| Trailing wildcard | `vercel.dns.*` | Literal prefix plus anything deeper |
| Exact | `vercel.dns.create` | One tool path |
| Mid-segment `*` | `vercel.*.*.dns.create` | Wildcards exactly one segment per `*` |

<Warning>
Invalid patterns are rejected at create time: empty strings, leading `*`, partial wildcards like `me*`, and consecutive dots are not allowed. Use `isValidPattern` (SDK) or the web UI validator before writing rules.
</Warning>

### Connection-agnostic UI patterns

The tools tree displays `integration.<tool>` without owner or connection segments. When you author a rule from a tool or category row, the UI stores a connection-wildcarded pattern via `toPolicyPattern`:

```text
integration.*.*.<tool-or-subtree>
```

A leaf rule on `records.create` becomes `myapi.*.*.records.create`; a category rule on `records` becomes `myapi.*.*.records.*`. Rules authored on one connection apply to every connection for that integration.

## Effective policy resolution

```mermaid
flowchart TD
  subgraph inputs [Inputs per tool call]
    T[Tool address]
    P[Owner-scoped policy rows]
    D[Plugin default annotation]
  end
  subgraph perOwner [Per owner]
    O1[Org: first match by position]
    O2[User: first match by position]
  end
  subgraph merge [Cross-owner merge]
    M[Most restrictive action wins]
  end
  subgraph fallback [No user match]
    F[Plugin default: requiresApproval or approve]
  end
  T --> O1
  T --> O2
  P --> O1
  P --> O2
  O1 --> M
  O2 --> M
  M -->|no match| F
  D --> F
```

When no user-authored rule matches, the executor falls back to the plugin annotation `requiresApproval`. OpenAPI maps `POST`, `PUT`, `PATCH`, and `DELETE` to `requiresApproval: true`; read-only `GET` operations default to auto-approve. MCP upstream tools inherit `requiresApproval` from `destructiveHint`.

## Manage policies in the web UI

Open **Policies** (`/policies`) to list, create, reorder, and remove rules.

<Steps>
<Step title="Add a policy">
Enter a pattern, choose an action (`Allow`, `Require approval`, or `Block`), and pick **Workspace** or **Personal** when the host exposes both owners. Click **Add policy**. New rules without an explicit position are inserted at the top of that owner's list (highest precedence).
</Step>
<Step title="Author from the tools tree">
On an integration's **Tools** tab, use **Set policy for …** on a tool row (exact rule) or category row (subtree rule). The menu previews the stored pattern before write. The tool detail header badge is the same surface: set, recognize, or clear a rule.
</Step>
<Step title="Reorder for precedence">
Use **Move up** / **Move down** on `/policies` to change `position`. More-specific patterns should sit above broader ones so a leaf rule is not shadowed by a later group rule.
</Step>
<Step title="Verify">
Confirm the rule appears under **Active policies** with the expected owner, pattern, and action. Invoke a gated tool or run `tools.list` to confirm blocked tools disappear from search.
</Step>
</Steps>

<Tip>
`usePolicyActions` auto-places new tree-authored rules below more-specific existing rules so a freshly added group rule does not silently override a leaf rule.
</Tip>

## HTTP API

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/policies` | List all policies for the authenticated identity |
| `POST` | `/policies` | Create a policy |
| `PATCH` | `/policies/:policyId` | Update pattern, action, or position |
| `DELETE` | `/policies/:policyId` | Remove a policy |

:::endpoint POST /policies
Create an owner-scoped tool policy.
:::

<ParamField body="owner" type="string" required>
`org` or `user`.
</ParamField>

<ParamField body="pattern" type="string" required>
Tool address pattern. Must pass `isValidPattern`.
</ParamField>

<ParamField body="action" type="string" required>
`approve`, `require_approval`, or `block`.
</ParamField>

<ParamField body="position" type="string">
Optional fractional-indexing key. Omit to place at the top of the owner's list.
</ParamField>

<ResponseField name="id" type="PolicyId">
Stable policy identifier.
</ResponseField>

<ResponseField name="owner" type="Owner">
`org` or `user`.
</ResponseField>

<ResponseField name="pattern" type="string">
Stored match pattern.
</ResponseField>

<ResponseField name="action" type="ToolPolicyAction">
`approve`, `require_approval`, or `block`.
</ResponseField>

<ResponseField name="position" type="string">
Fractional-indexing order key.
</ResponseField>

<ResponseField name="createdAt" type="number">
Unix milliseconds.
</ResponseField>

<ResponseField name="updatedAt" type="number">
Unix milliseconds.
</ResponseField>

<RequestExample>

```bash title="Create a require-approval gate"
curl -X POST "$EXECUTOR_BASE_URL/policies" \
  -H "content-type: application/json" \
  -H "authorization: Bearer $TOKEN" \
  -d '{
    "owner": "org",
    "pattern": "executor.coreTools.policies.list",
    "action": "require_approval"
  }'
```

</RequestExample>

:::

:::endpoint PATCH /policies/:policyId
Update an existing policy. The payload must include `owner` so the handler can verify partition ownership.
:::

<ParamField path="policyId" type="PolicyId" required>
Policy to update.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owning partition (`org` or `user`).
</ParamField>

<ParamField body="pattern" type="string">
New pattern.
</ParamField>

<ParamField body="action" type="ToolPolicyAction">
New action.
</ParamField>

<ParamField body="position" type="string">
New order key for precedence within the owner.
</ParamField>

:::

:::endpoint DELETE /policies/:policyId
Remove a policy. Payload requires `owner`.
:::

<ParamField path="policyId" type="PolicyId" required>
Policy to remove.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owning partition (`org` or `user`).
</ParamField>

:::

## SDK and core tools

`createExecutor` exposes `executor.policies.list`, `.create`, `.update`, `.remove`, and `.resolve(address)`.

<CodeGroup>

```typescript title="SDK (promise mode)"
const policy = await executor.policies.create({
  owner: "org",
  pattern: "myapi.*.*.records.create",
  action: "require_approval",
});

const effective = await executor.policies.resolve(
  "myapi.org.prod.records.create" as ToolAddress,
);
```

```typescript title="Core tools via execute"
const result = await tools.executor.coreTools.policies.create({
  owner: "org",
  pattern: "vercel.dns.*",
  action: "block",
});
```

</CodeGroup>

<Warning>
`policies.create`, `policies.update`, and `policies.remove` core tools carry `annotations: { requiresApproval: true }`. Mutating policies always pauses for approval so prompt-injected code cannot silently disable guardrails.
</Warning>

## Handle approval pauses

When effective policy or tool annotations require approval, `execute` pauses and returns a `PausedExecution`. Resume the pause from the console, MCP, or CLI.

### Web console

Browser approval URLs follow:

```text
<origin>/resume/<executionId>?mcp_session_id=<sessionId>
```

The `mcp_session_id` query routes MCP-session-scoped pauses to `/api/mcp-sessions/:id/executions/:executionId/resume`. Pauses without that query use the standard `/api/executions/:executionId/resume` path.

<Steps>
<Step title="Open the approval page">
Follow the `approvalUrl` from the paused MCP result or the link printed by `executor call` / `executor resume`.
</Step>
<Step title="Review the gated action">
The page shows the elicitation message, tool id, and argument preview.
</Step>
<Step title="Decide">
Click **Approve**, **Decline**, or **Cancel**. The host records `{ action, content? }` and wakes any in-flight `resume` waiter.
</Step>
</Steps>

### MCP elicitation modes

| Mode | Query | Resume surface |
|------|-------|----------------|
| `browser` (typical for human-in-the-loop) | `?elicitation_mode=browser` | `approvalUrl` plus long-poll on MCP `resume` |
| `model` (default) | absent or `?elicitation_mode=model` | MCP `resume` tool exposed to the agent |
| `native` | `?elicitation_mode=native` | Client-native elicitation only; no `resume` tool |

In `browser` mode, `BrowserApprovalStore.waitForResponse(executionId)` blocks until the console posts a decision. `recordResponse` wakes the waiter.

### CLI resume

```bash
executor resume --execution-id <id> --action accept
executor resume --execution-id <id> --action decline
executor resume --execution-id <id> --action cancel
```

Optional `--content '{"key":"value"}'` supplies form content when `action=accept`. If the resumed execution pauses again, the CLI prints the next approval URL.

`executor call`, `executor resume`, and `executor tools` auto-start the local daemon when needed.

<ResponseExample>

```text title="Paused call output"
Approval required:
http://127.0.0.1:5173/resume/exec_abc123?mcp_session_id=sess_xyz
```

</ResponseExample>

### Declined or canceled approvals

If the human declines or cancels, the engine returns `ElicitationDeclinedError` and the gated tool does not run. For browser flows, the MCP `resume` call completes with acknowledgement text (`I've denied it`, `I've canceled it`) rather than the tool result.

## Troubleshooting

<AccordionGroup>
<Accordion title="Rule exists but the wrong action applies">
Check owner precedence first (org block beats user approve), then position within the owner (lower `position` wins). A broad rule listed above a specific rule shadows the leaf.
</Accordion>
<Accordion title="UI pattern does not match invoke-time address">
Tree-authored rules store `integration.*.*.<tail>`. Compare against the full five-segment address or static fqid, not the shortened display id.
</Accordion>
<Accordion title="MCP resume hangs in browser mode">
Confirm the console approval page loaded with the same `mcp_session_id` from the paused result and that you clicked Approve or Decline. The in-process store times out after 10 minutes without a decision.
</Accordion>
<Accordion title="Policy mutation did not take effect immediately">
Policy writes are optimistic in the web UI. A failed API call rolls back the row; check network errors on create/update/remove.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Policies concept" href="/policies">
Per-tool actions, pattern matching grammar, and plugin-derived defaults.
</Card>
<Card title="Executions" href="/executions">
Paused states, `execute` and `resume` routes, and elicitation handling.
</Card>
<Card title="MCP proxy" href="/mcp-proxy">
Single MCP endpoint, elicitation modes, and shared policy enforcement.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full `policies` route payloads and error shapes.
</Card>
<Card title="CLI reference" href="/cli-reference">
`executor resume`, `executor call`, and auto-start behavior.
</Card>
</CardGroup>

---

## 14. Embed with the SDK

> Compose `createExecutor` with plugins and credential providers, register integrations, create connections, list and invoke tools, and shut down cleanly in application code.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/14-embed-with-the-sdk.md
- Generated: 2026-06-23T19:26:01.498Z

### Source Files

- `packages/core/sdk/src/executor.ts`
- `packages/core/sdk/src/promise.ts`
- `packages/core/sdk/package.json`
- `examples/docs-sdk-quickstart/src/main.ts`
- `examples/all-plugins/src/main.ts`
- `packages/plugins/openapi/README.md`

---
title: "Embed with the SDK"
description: "Compose `createExecutor` with plugins and credential providers, register integrations, create connections, list and invoke tools, and shut down cleanly in application code."
---

`@executor-js/sdk` exposes `createExecutor`, a composable runtime that wires protocol plugins, credential providers, and a FumaDB-backed catalog into one typed `Executor` object. Application code registers integrations through plugin extensions (`executor.openapi.addSpec`, `executor.graphql.addIntegration`, `executor.mcp.addServer`), saves credentials as connections, lists persisted tools per connection, and invokes them with `executor.execute`. The default published import is Promise-based; the Effect entry at `@executor-js/sdk/core` is for plugin authors and hosts that already run on Effect.

## Install

```sh
bun add @executor-js/sdk @executor-js/plugin-openapi
```

Add additional `@executor-js/plugin-*` packages for GraphQL, MCP, keychain, file-secrets, 1Password, Google bundles, or WorkOS Vault as needed. See the [plugin catalog](/plugin-catalog) for the full set.

## Import paths

<Tabs>
<Tab title="Promise (recommended)">

Use the root or `/promise` subpath. Every Effect-returning method is promisified; no Effect dependency required in application code.

```ts
import { createExecutor, ProviderKey, ProviderItemId } from "@executor-js/sdk";
import { openApiPlugin, variable } from "@executor-js/plugin-openapi";
```

`examples/docs-sdk-quickstart` is the canonical snippet source for this surface.

</Tab>
<Tab title="Effect">

Use `@executor-js/sdk/core` (or `@executor-js/sdk` in the monorepo dev tree) when the host already runs on Effect. Plugin factories and credential provider `get`/`set` methods return `Effect`s.

```ts
import { Effect } from "effect";
import { createExecutor, Tenant } from "@executor-js/sdk/core";
import { openApiPlugin } from "@executor-js/plugin-openapi/core";

const program = Effect.gen(function* () {
  const executor = yield* createExecutor({ tenant: Tenant.make("my-tenant"), /* ... */ });
  yield* executor.close();
});
```

`examples/all-plugins` exercises this shape end to end.

</Tab>
</Tabs>

<Note>
Plugin READMEs that reference `executor.tools.invoke`, `executor.scopes`, or `executor.secrets` describe the pre-v2 surface. The v2 SDK invokes tools with `executor.execute(address, args)` and stores credentials as connections routed through credential providers.
</Note>

## Architecture

```mermaid
flowchart TB
  subgraph app [Application]
    CE["createExecutor(config)"]
    EX["Executor"]
  end
  subgraph plugins [Protocol plugins]
    OA["executor.openapi"]
    GQL["executor.graphql"]
    MCP["executor.mcp"]
  end
  subgraph providers [Credential providers]
    MEM["inline memory provider"]
    KC["keychain / file-secrets / 1Password"]
  end
  subgraph store [Persistence]
    DB["FumaDB (default: in-memory)"]
  end
  CE --> EX
  EX --> plugins
  EX --> providers
  EX --> DB
  OA -->|"addSpec"| INT["integrations catalog"]
  INT -->|"connections.create"| CON["connections + tool rows"]
  CON -->|"tools.list"| TOOLS["addressable tools"]
  TOOLS -->|"execute"| INV["plugin.invokeTool"]
  providers --> INV
```

Integrations are tenant-level catalog entries. Connections bind a credential to one integration and produce that integration's tools. Tool addresses follow `tools.<integration>.<owner>.<connection>.<tool>`; the tool segment may contain dots (for example `tools.inventory.org.default.items__list`).

## Compose `createExecutor`

<Steps>
<Step title="Pick plugins and providers">

Pass protocol plugins in `plugins` and optional inline credential providers in `providers`. Plugins may also contribute providers through `plugin.credentialProviders`; config-level providers register first and win as the default writable store.

</Step>
<Step title="Set elicitation policy">

`onElicitation` is required. Pass `"accept-all"` for scripts, tests, and non-interactive hosts. Pass a handler function for interactive hosts that must answer approval prompts and mid-call form or URL elicitations.

</Step>
<Step title="Await the executor">

`createExecutor` returns a Promise (Promise surface) or `Effect<Executor, StorageFailure>` (Effect surface). Plugin extension methods are merged onto the returned object as `executor[pluginId]` (for example `executor.openapi`).

</Step>
</Steps>

### `ExecutorConfig` fields

| Field | Default | Role |
| --- | --- | --- |
| `tenant` | `"default-tenant"` | Org/workspace partition for `owner: "org"` rows |
| `subject` | omitted | Acting member; required to target `owner: "user"` |
| `plugins` | `[]` | Protocol and provider plugins to load |
| `providers` | `[]` | Inline credential providers (registered before plugin providers) |
| `db` | in-memory FumaDB | Persistent store factory or handle; omit for ephemeral scripts |
| `onElicitation` | required | `"accept-all"` or `(ctx) => ElicitationResponse` |
| `redirectUri` | none | OAuth callback URL; required for interactive OAuth (no localhost default) |
| `coreTools` | disabled | Built-in agent-facing static tools over integrations/connections/policies |
| `blobs` | FumaDB blob table | Object-store backend for large plugin blobs |
| `httpClientLayer` / `fetch` | platform default | Outbound HTTP for plugins |

<ParamField body="tenant" type="string">
Org or workspace id. Branded as `Tenant` on the Effect surface.
</ParamField>

<ParamField body="subject" type="string">
Optional member id. Omit for a pure-org executor with no `owner: "user"` rows.
</ParamField>

<ParamField body="plugins" type="readonly AnyPlugin[]" required>
Plugin factories such as `openApiPlugin()`, `graphqlPlugin()`, `mcpPlugin()`, `keychainPlugin()`, `fileSecretsPlugin()`.
</ParamField>

<ParamField body="providers" type="readonly CredentialProvider[]">
Writable stores for inline credential values. A writable provider is required before `connections.create({ value })` can persist a pasted secret.
</ParamField>

<ParamField body="onElicitation" type='"accept-all" | ElicitationHandler' required>
How the executor answers approval prompts and tool elicitations. Overridable per call through `execute` options.
</ParamField>

<ParamField body="db" type="FumaDb | ExecutorDb | factory">
Supply a SQLite/LibSQL handle for durable hosts (as `apps/local` does). Omit to use the SDK's ephemeral in-memory adapter.
</ParamField>

<ParamField body="redirectUri" type="string">
OAuth redirect such as `${webBaseUrl}/oauth/callback`. Omit only when the executor never runs interactive OAuth.
</ParamField>

<ParamField body="coreTools" type="object">
Enable `core-tools` static tools. Set `webBaseUrl`, optional `orgSlug`, and `includeProviders` for agent-facing configuration helpers.
</ParamField>

### Minimal composition

<CodeGroup>
```ts title="Promise — in-memory script"
import { createExecutor, ProviderKey, ProviderItemId, type CredentialProvider } from "@executor-js/sdk";
import { Effect } from "effect";
import { openApiPlugin } from "@executor-js/plugin-openapi";

const memory = new Map<string, string>();
const memoryProvider: CredentialProvider = {
  key: ProviderKey.make("memory"),
  writable: true,
  get: (id) => Effect.sync(() => memory.get(String(id)) ?? null),
  set: (id, value) => Effect.sync(() => { memory.set(String(id), value); }),
};

const executor = await createExecutor({
  plugins: [openApiPlugin()],
  providers: [memoryProvider],
  onElicitation: "accept-all",
});
```

```ts title="Effect — with tenant"
import { Effect } from "effect";
import { createExecutor, Tenant } from "@executor-js/sdk/core";
import { openApiPlugin } from "@executor-js/plugin-openapi/core";

const executor = yield* createExecutor({
  tenant: Tenant.make("example-tenant"),
  plugins: [openApiPlugin()],
  onElicitation: "accept-all",
});
```
</CodeGroup>

## Credential providers

A connection's value lives in a registered `CredentialProvider`, not a separate secret store. Providers expose `key`, `writable`, `get`, and optional `set`, `delete`, and `list`. Plugin-provided providers (keychain, file-secrets, 1Password, WorkOS Vault) register at startup; inline providers in `config.providers` register first and become the default writable store.

```ts
const keys = await executor.providers.list();
const entries = await executor.providers.items(ProviderKey.make("memory"));
```

For production hosts, swap the in-memory map for a durable plugin provider. See [Configure credentials](/configure-credentials).

## Register integrations

Each protocol plugin exposes an extension object on the executor. Registration methods write to the integrations catalog and declare auth templates; they do not create callable tools until a connection exists.

| Plugin | Extension | Registration method |
| --- | --- | --- |
| OpenAPI | `executor.openapi` | `addSpec({ slug, spec, baseUrl, authenticationTemplate, ... })` |
| GraphQL | `executor.graphql` | `addIntegration({ slug, endpoint, introspectionJson, ... })` |
| MCP | `executor.mcp` | `addServer({ slug, transport, endpoint, ... })` |
| Google | `executor.google` | `addBundle({ slug, urls, ... })` |

OpenAPI auth templates declare where credentials render on each request. Use `variable("token")` as the slot the resolved credential fills:

```ts
import { variable } from "@executor-js/plugin-openapi";

await executor.openapi.addSpec({
  slug: "inventory",
  description: "Inventory API",
  baseUrl: "https://inventory.example.com",
  spec: { kind: "blob", value: JSON.stringify(openApiDocument) },
  authenticationTemplate: [
    {
      slug: "apiKey",
      type: "apiKey",
      headers: { "X-API-Key": [variable("token")] },
    },
  ],
});
```

`executor.integrations.list()`, `executor.integrations.get(slug)`, `executor.integrations.detect(url)`, and `executor.integrations.remove(slug)` operate on the shared catalog regardless of which plugin registered an entry.

## Create connections

A connection is the saved credential for one integration, scoped by `(owner, integration, name)`. Creating a connection with an inline `value` writes to the default writable provider and produces the integration's per-connection tool rows.

<ParamField body="owner" type='"org" | "user"' required>
`"org"` for workspace-scoped credentials; `"user"` requires `subject` on the executor.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Connection name within the owner and integration (commonly `"default"`).
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Slug of the registered integration.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
Which auth method from the integration's `authenticationTemplate` to apply.
</ParamField>

<ParamField body="value" type="string">
Inline credential for single-input templates. Written to the default writable provider.
</ParamField>

```ts
const connection = await executor.connections.create({
  owner: "org",
  name: "default",
  integration: "inventory",
  template: "apiKey",
  value: "inventory-api-key",
});
```

<ResponseField name="address" type="ConnectionAddress">
Callable prefix `tools.<integration>.<owner>.<connection>`. Append `.<tool>` for a specific tool address.
</ResponseField>

For OAuth-backed integrations, use `executor.oauth.start` and `executor.oauth.complete` instead of inline `value`. Hosts that serve OAuth must pass `redirectUri` to `createExecutor`.

External provider references use `from: { provider, id }` or multi-input `values` / `inputs` maps. See [Connections](/connections) for the full identity model.

## List and inspect tools

Tools are persisted per connection at create/refresh time. `tools.list` reads stored rows; it does not dial upstream APIs on every call.

```ts
const tools = await executor.tools.list({ integration: "inventory" });

for (const tool of tools) {
  console.log(`${tool.address}: ${tool.description}`);
}
```

### `ToolListFilter`

| Field | Purpose |
| --- | --- |
| `integration` | Narrow to one integration slug |
| `owner` | Filter by `org` or `user` |
| `connection` | Filter by connection name |
| `query` | Case-insensitive substring on name or description |
| `includeAnnotations` | Resolve plugin-derived annotations (default `true`) |
| `includeBlocked` | Include tools with effective `block` policy (default `false`) |

Inspect input schemas and generated TypeScript previews:

```ts
const schema = await executor.tools.schema(tools[0].address);
console.log(schema?.inputTypeScript ?? "No input required");
```

`ToolSchemaView` includes `inputSchema`, `outputSchema`, `schemaDefinitions` for `$ref` resolution, and optional `inputTypeScript` / `outputTypeScript` strings.

## Invoke tools

Call `executor.execute` with a `ToolAddress` and arguments matching the tool's input schema. This is the v2 invoke path: the executor resolves the connection credential, enforces effective policy and approval elicitation, then dispatches to the owning plugin's `invokeTool`.

```ts
const result = await executor.execute(
  "tools.inventory.org.default.items__list",
  {},
);
```

<ParamField body="address" type="ToolAddress" required>
Full five-segment address from `tools.list`, or a static tool fqid for core-tools entries.
</ParamField>

<ParamField body="args" type="unknown" required>
JSON-shaped arguments matching the tool input schema.
</ParamField>

<ParamField body="options.onElicitation" type="OnElicitation">
Per-call override of the executor-level elicitation handler.
</ParamField>

### Invoke-time behavior

| Stage | Behavior |
| --- | --- |
| Policy resolution | `block` raises `ToolBlockedError`; `require_approval` or `annotations.requiresApproval` triggers an approval elicitation |
| Credential resolution | Connection value loaded from the registered provider and rendered through the auth template |
| Plugin dispatch | Owning plugin's `invokeTool` builds and sends the upstream request |
| Elicitation | Mid-call form or URL requests route through `onElicitation`; `"accept-all"` auto-accepts |

Typed failures on the `execute` channel include `ToolNotFoundError`, `ToolInvocationError`, `ToolBlockedError`, `ConnectionNotFoundError`, `CredentialResolutionError`, and `ElicitationDeclinedError`. Promise callers receive these as rejected Promise values.

<Warning>
POST and DELETE OpenAPI operations commonly carry `requiresApproval: true` in default annotations. With `onElicitation: "accept-all"`, approval prompts are auto-accepted. Interactive hosts should supply a handler that surfaces approval UI.
</Warning>

## Policies

Owner-scoped tool policies are available directly on the embedded executor:

```ts
await executor.policies.create({ owner: "org", pattern: "tools.inventory.*", action: "require_approval" });
const effective = await executor.policies.resolve(toolAddress);
```

See [Policies](/policies) and [Manage policies](/manage-policies) for precedence and approval flows.

## Persist across restarts

Omitting `db` uses an in-memory FumaDB adapter suitable for scripts and tests. Durable hosts pass a database factory or handle with the executor-owned table set from `collectTables()`. The local daemon (`apps/local`) wires SQLite, runs data migrations, sets `redirectUri` from `EXECUTOR_WEB_BASE_URL`, and enables `coreTools`.

```ts
const executor = await createExecutor({
  tenant: "my-workspace",
  subject: "user-123",
  db: sqliteHandle,
  plugins,
  onElicitation: interactiveHandler,
  redirectUri: new URL("/api/oauth/callback", webBaseUrl).toString(),
  coreTools: { webBaseUrl },
});
```

## Shut down

Call `executor.close()` when the process or request scope ends. `close` runs every plugin's `close` hook (for example MCP connection pool teardown) and invokes the database `close` callback when supplied.

```ts
await executor.close();
```

Hosts that wrap the executor in a managed runtime should close on dispose, as `apps/local` does in `createExecutorHandle`.

## End-to-end example

The `examples/docs-sdk-quickstart` package walks the full embed flow in under 150 lines: in-memory provider, `openApiPlugin`, `addSpec`, `connections.create`, `tools.list`, `tools.schema`, and `close`. Run it with:

```sh
cd examples/docs-sdk-quickstart && bun run start
```

For every published plugin wired together, see `examples/all-plugins` and the [SDK quickstart example](/sdk-quickstart-example) page.

## Related pages

<CardGroup>
<Card title="SDK reference" href="/sdk-reference">
`createExecutor` exports, typed IDs, plugin extension typing, promise-mode entry points, and error tags.
</Card>
<Card title="SDK quickstart example" href="/sdk-quickstart-example">
Line-by-line walkthrough of `examples/docs-sdk-quickstart`.
</Card>
<Card title="Plugin catalog" href="/plugin-catalog">
Published protocol and provider plugins and how `examples/all-plugins` composes them.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credential identity, provider routing, and OAuth minting.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, schema inspection, and invocation across surfaces.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Durable credential providers, auth templates, and connection payloads.
</Card>
</CardGroup>

---

## 15. Deploy self-hosted

> Run Executor in Docker or on Cloudflare Workers: image defaults, volume mounts, bootstrap admin env vars, TLS/public-origin requirements, and sandbox network constraints.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/15-deploy-self-hosted.md
- Generated: 2026-06-23T19:25:45.544Z

### Source Files

- `apps/docs/hosted/docker.mdx`
- `apps/docs/hosted/cloudflare.mdx`
- `apps/host-selfhost/Dockerfile`
- `apps/host-selfhost/docker-compose.yml`
- `apps/host-selfhost/executor.config.ts`
- `apps/host-cloudflare/wrangler.jsonc`
- `apps/host-cloudflare/executor.config.ts`

---
title: "Deploy self-hosted"
description: "Run Executor in Docker or on Cloudflare Workers: image defaults, volume mounts, bootstrap admin env vars, TLS/public-origin requirements, and sandbox network constraints."
---

Executor ships two operator-run runtimes that expose the same surfaces (web UI, HTTP API, streamable-HTTP MCP, QuickJS code execution) without Executor Cloud: a single-container Docker image (`apps/host-selfhost`) and a Cloudflare Worker (`apps/host-cloudflare`). Both are single-tenant, disable stdio MCP spawning (`dangerouslyAllowStdioMCP: false`), and persist encrypted secrets with the encrypted-secrets plugin.

<Tabs>
<Tab title="Docker">

The Docker image bundles libSQL (SQLite), Better Auth, QuickJS, and the web SPA in one process. No external database or proxy is required.

</Tab>
<Tab title="Cloudflare Workers">

The Worker bundles D1 storage, R2 blob storage, QuickJS-WASM, and Workers Static Assets for the SPA. Authentication is Cloudflare Access only; there is no in-app login or bootstrap admin flow.

</Tab>
</Tabs>

| Surface | Docker (`host-selfhost`) | Cloudflare (`host-cloudflare`) |
| --- | --- | --- |
| Auth | Better Auth (email/password, API keys, MCP OAuth) | Cloudflare Access JWT on every API/MCP request |
| Storage | libSQL file under `/data` | D1 + R2 (`executor-blobs`) |
| Code execution | QuickJS in-process | QuickJS-WASM in the Worker |
| Default port / URL | `4788` (`http://localhost:4788`) | `executor-cloudflare.<subdomain>.workers.dev` |
| Admin bootstrap | Browser first-run or `EXECUTOR_BOOTSTRAP_ADMIN_*` env vars | `ADMIN_EMAILS` + Access policies |
| Public origin var | `EXECUTOR_WEB_BASE_URL` | `VITE_PUBLIC_SITE_URL` (optional) |
| Sandbox network opt-in | `EXECUTOR_ALLOW_LOCAL_NETWORK=true` | `ALLOW_LOCAL_NETWORK=true` |

```mermaid
flowchart TB
  subgraph docker["Docker container (host-selfhost)"]
    SPA_D["Web SPA"]
    API_D["HTTP API /api/*"]
    MCP_D["MCP /mcp"]
    QJS_D["QuickJS executor"]
    DB_D["libSQL at /data/data.db"]
    SPA_D --> API_D
    API_D --> QJS_D
    MCP_D --> QJS_D
    API_D --> DB_D
  end

  subgraph cf["Cloudflare Worker (host-cloudflare)"]
    ACCESS["Cloudflare Access"]
    SPA_C["Static Assets (dist/)"]
    API_C["Worker /api/*"]
    MCP_C["Worker /mcp + McpSessionDO"]
    QJS_C["QuickJS-WASM"]
    D1["D1 (executor)"]
    R2["R2 (executor-blobs)"]
    ACCESS --> SPA_C
    ACCESS --> API_C
    ACCESS --> MCP_C
    API_C --> QJS_C
    MCP_C --> QJS_C
    API_C --> D1
    API_C --> R2
  end
```

## Docker

<Steps>
<Step title="Run the published image">

<CodeGroup>
```bash title="Published GHCR image"
docker run -d \
  --name executor-selfhost \
  -p 4788:4788 \
  -v executor-data:/data \
  ghcr.io/rhyssullivan/executor-selfhost:latest
```

```bash title="Build from a clone (apps/host-selfhost)"
docker compose up -d --build
```
</CodeGroup>

Open `http://localhost:4788`. A bare run needs no env vars: the container generates and persists session and encryption keys under `/data` on first boot.

</Step>

<Step title="Verify health">

The image healthcheck probes `GET /api/health` on port `4788`.

```bash
curl -sf http://localhost:4788/api/health
```

</Step>

<Step title="Create the admin">

Without bootstrap env vars, the first account created in the browser becomes the owner. After that, new users join only through single-use invite links from the **Admin** page.

For headless deploys, set both bootstrap variables before start (see [Bootstrap admin](#bootstrap-admin-docker)).

</Step>
</Steps>

### Image defaults

The multi-stage `apps/host-selfhost/Dockerfile` produces a distroless runtime image:

| Setting | Default |
| --- | --- |
| Base runtime | `gcr.io/distroless/cc-debian12` with Bun copied from `oven/bun:1` |
| `NODE_ENV` | `production` |
| `EXECUTOR_HOST` | `0.0.0.0` (bind all interfaces inside the container) |
| `PORT` | `4788` |
| `EXECUTOR_DATA_DIR` | `/data` |
| Entrypoint | `bun run dist-server/serve.js` |
| Published image | `ghcr.io/rhyssullivan/executor-selfhost` (`latest` for stable, `beta` for prereleases) |

<Note>
The local CLI daemon (`executor install`) listens on `17888` by default. The self-hosted Docker image uses `4788`; map the host port accordingly.
</Note>

### Volume mounts

Mount `/data` so the SQLite database and generated keys survive restarts and upgrades:

```bash
-v executor-data:/data          # named volume (docker-compose default)
-v /srv/executor:/data          # host path
```

| Path in container | Contents |
| --- | --- |
| `/data/data.db` | libSQL database (`EXECUTOR_DB_PATH` overrides the file path) |
| `/data/auth-secret.key` | Generated `BETTER_AUTH_SECRET` when unset |
| `/data/secret.key` | Generated `EXECUTOR_SECRET_KEY` when unset |

Back up by snapshotting the volume or copying `/data`.

## Cloudflare Workers

<Steps>
<Step title="Install dependencies and log in">

From the repo root:

```bash
bun install
cd apps/host-cloudflare
bunx wrangler login
```

</Step>

<Step title="Run deploy setup">

```bash
bun run deploy:setup
```

`deploy:setup` (`scripts/deploy.sh`) is idempotent. It:

1. Verifies `wrangler` login
2. Creates or reuses the `executor` D1 database and writes its `database_id` into `wrangler.jsonc`
3. Generates and uploads `EXECUTOR_SECRET_KEY` via `wrangler secret put` if not already set
4. Builds the SPA (`vite build` → `dist/`)
5. Deploys the Worker

The script also provisions R2 (`executor-blobs`) and a Durable Object (`McpSessionDO`) per `wrangler.jsonc`.

</Step>

<Step title="Enable Cloudflare Access">

Until the Worker sits behind an Access application, API and MCP routes return `401`. In the Zero Trust dashboard:

1. **Access → Applications → Add an application → Self-hosted**
2. Application domain: `executor-cloudflare.<your-subdomain>.workers.dev`
3. Add an Access policy (for example, emails ending in `@yourcompany.com`)
4. Copy the application **Audience (AUD)** tag, then redeploy:

```bash
bunx wrangler deploy \
  --var ACCESS_AUD:<aud> \
  --var ACCESS_TEAM_DOMAIN:<your-team>.cloudflareaccess.com
```

You can also set `ACCESS_AUD` and `ACCESS_TEAM_DOMAIN` in `wrangler.jsonc` and redeploy.

</Step>

<Step title="Grant admin role">

Set `ADMIN_EMAILS` to a comma-separated list of emails that receive the admin role after Access authentication:

```bash
bunx wrangler deploy --var ADMIN_EMAILS:admin@example.com,ops@example.com
```

</Step>
</Steps>

<Warning>
`ENABLE_DEV_AUTH=true` bypasses Access and treats every request as a fixed dev admin. Use only in local `wrangler dev` with `.dev.vars`. Never set it on a deployed Worker that is not already behind Access.
</Warning>

### Worker routing

`wrangler.jsonc` serves the built SPA from `./dist` with `single-page-application` fallback. `run_worker_first` routes `/api/*`, `/mcp`, and `/mcp/*` to the Worker; client routes like `/policies` fall through to the SPA.

## Bootstrap admin (Docker)

Docker supports two admin paths. Cloudflare has no equivalent: identity and membership come from Access.

**Browser first-run (default).** On a fresh instance with no bootstrap env vars, boot creates a single organization with zero members. The first signup through the setup screen claims ownership.

**Headless bootstrap.** Set both email and password to pre-create the admin at boot:

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_EMAIL" type="string">
Email for the bootstrap admin. Must be set together with `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD`.
</ParamField>

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD" type="string">
Password for the bootstrap admin. Must be set together with `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL`.
</ParamField>

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_NAME" type="string" default="Admin">
Display name for the bootstrap admin.
</ParamField>

<RequestExample>
```bash
docker run -d \
  -p 4788:4788 \
  -v executor-data:/data \
  -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \
  -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD='change-me-to-something-strong' \
  -e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
  ghcr.io/rhyssullivan/executor-selfhost:latest
```
</RequestExample>

<Info>
`BETTER_AUTH_SECRET` and `EXECUTOR_SECRET_KEY` are optional on Docker: when unset, random keys are generated and persisted under `/data`. Set them explicitly to manage rotation and to keep secrets readable if you move the data directory.
</Info>

## Public origin and TLS

Executor builds absolute links for OAuth redirects, MCP OAuth metadata, and connect-card URLs from a configured public origin. It does **not** derive this from the request `Host` header (that would allow host-header injection).

### Docker

<ParamField body="EXECUTOR_WEB_BASE_URL" type="string">
Public URL browsers use (scheme + host + port). Must exactly match the address loaded in the browser.
</ParamField>

<Warning>
Behind a reverse proxy or TLS terminator, set `EXECUTOR_WEB_BASE_URL` to the exact public URL (for example `https://executor.example.com`). A mismatch causes Better Auth to reject logins with `403 Invalid origin`.
</Warning>

When `EXECUTOR_WEB_BASE_URL` is unset, the resolver checks platform-injected vars (`RAILWAY_PUBLIC_DOMAIN`, `RENDER_EXTERNAL_URL`, `FLY_APP_NAME`, and others) before falling back to `http://localhost:<PORT>`.

### Cloudflare

<ParamField body="VITE_PUBLIC_SITE_URL" type="string">
Optional canonical web base URL. When unset, the Worker derives the origin from each request's URL (Cloudflare-set, not spoofable via `Host`).
</ParamField>

Set `VITE_PUBLIC_SITE_URL` only when you need a pinned canonical URL, for example behind a proxy that rewrites `Host`.

## Sandbox network constraints

Sandboxed code (QuickJS executions and tool calls that use the hosted HTTP client) outbound requests pass through a network guard. By default, local and private addresses are blocked.

| Runtime | Opt-in variable | Default |
| --- | --- | --- |
| Docker | `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` (unset or any value other than `"true"`) |
| Cloudflare | `ALLOW_LOCAL_NETWORK` | `false` |

When the guard is active (`allowLocalNetwork: false`), outbound HTTP/HTTPS requests are blocked if they target:

- `localhost` or `*.localhost`
- Private, loopback, link-local, or multicast IPv4/IPv6 ranges
- Cloud metadata hostnames (`metadata.google.internal`, `169.254.169.254`, and similar)
- Hostnames that DNS-resolve to any of the above (rebinding protection on Docker)

Only `http:` and `https:` protocols are allowed.

<Warning>
Set `EXECUTOR_ALLOW_LOCAL_NETWORK=true` or `ALLOW_LOCAL_NETWORK=true` only when you trust the code running in the sandbox and need it to reach internal services. Keep the default off for adversarial or LLM-generated code.
</Warning>

## Environment reference

### Docker

| Variable | Default | Purpose |
| --- | --- | --- |
| `PORT` | `4788` | HTTP listen port |
| `EXECUTOR_HOST` | `0.0.0.0` in image; `127.0.0.1` in bare `loadConfig()` | Bind address |
| `EXECUTOR_DATA_DIR` | `/data` in image | Data directory for DB and generated keys |
| `EXECUTOR_DB_PATH` | `<data dir>/data.db` | SQLite file path |
| `EXECUTOR_WEB_BASE_URL` | platform-detected or `http://localhost:<PORT>` | Public browser URL |
| `BETTER_AUTH_SECRET` | generated in `/data` | Session secret (32+ chars if set explicitly) |
| `EXECUTOR_SECRET_KEY` | generated in `/data` | At-rest secret encryption key |
| `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` | unset | Headless admin email |
| `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` | unset | Headless admin password |
| `EXECUTOR_BOOTSTRAP_ADMIN_NAME` | `Admin` | Headless admin display name |
| `EXECUTOR_ORG_NAME` | `Default` | Single org display name |
| `EXECUTOR_ORG_SLUG` | `default` | URL slug for org-prefixed routes |
| `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandbox code to reach private networks |

Copy `apps/host-selfhost/.env.example` to `.env` beside `docker-compose.yml` for optional overrides.

### Cloudflare (`wrangler.jsonc` vars and secrets)

| Name | Kind | Purpose |
| --- | --- | --- |
| `EXECUTOR_SECRET_KEY` | **secret** (`wrangler secret put`) | Required. Encrypts stored secrets at rest in D1 |
| `ACCESS_TEAM_DOMAIN` | var | Zero Trust team domain |
| `ACCESS_AUD` | var | Access application audience tag |
| `ACCESS_NAME_CLAIM` | var | JWT claim for display name (default `name`) |
| `ACCESS_GROUPS_CLAIM` | var | JWT claim for groups (default `groups`) |
| `ADMIN_EMAILS` | var | Comma-separated admin emails |
| `SELF_HOSTED_ORG_ID` | var | Single org id (default `default`) |
| `SELF_HOSTED_ORG_NAME` | var | Single org name (default `Default`) |
| `SELF_HOSTED_ORG_SLUG` | var | Org URL slug (default `default`) |
| `ALLOW_LOCAL_NETWORK` | var | Sandbox private-network opt-in |
| `VITE_PUBLIC_SITE_URL` | var | Optional pinned public origin |
| `ENABLE_DEV_AUTH` | var | Local dev only; bypasses Access |

## Connect agents and clients

Both runtimes expose streamable-HTTP MCP at `/mcp`:

- Docker: `http://localhost:4788/mcp` (or your public `EXECUTOR_WEB_BASE_URL` + `/mcp`)
- Cloudflare: `https://executor-cloudflare.<subdomain>.workers.dev/mcp` (authenticated through Access)

See [MCP proxy](/mcp-proxy) for how the endpoint fronts your integrations, and [Connect MCP clients](/connect-mcp-clients) for client wiring.

## Related pages

<CardGroup>
<Card title="Configuration reference" href="/configuration-reference">
Environment variables, runtime paths, ports, and client overrides across all runtimes.
</Card>
<Card title="Connect MCP clients" href="/connect-mcp-clients">
Wire Cursor, Claude Code, and other MCP clients to your self-hosted `/mcp` endpoint.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Set up credential providers and connections after the instance is running.
</Card>
<Card title="Hosted cloud" href="/hosted-cloud">
How Executor Cloud differs from Docker and Cloudflare self-host deployments.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Recovery for port conflicts, invalid-origin errors, OAuth behind proxies, and daemon issues.
</Card>
</CardGroup>

---

## 16. Executor Cloud

> Hosted Executor Cloud entry points, sign-in flow, shared MCP endpoint usage, and how cloud deployments differ from local daemon and self-host runtimes.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/16-executor-cloud.md
- Generated: 2026-06-23T19:26:02.576Z

### Source Files

- `apps/docs/hosted/cloud.mdx`
- `apps/cloud/package.json`
- `apps/cloud/executor.config.ts`
- `packages/core/api/src/api.ts`
- `e2e/setup/cloud.globalsetup.ts`
- `apps/docs/mcp-proxy.mdx`

---
title: "Executor Cloud"
description: "Hosted Executor Cloud entry points, sign-in flow, shared MCP endpoint usage, and how cloud deployments differ from local daemon and self-host runtimes."
---

Executor Cloud is the hosted `apps/cloud` deployment at [https://executor.sh](https://executor.sh): a Cloudflare Worker that serves the web console, the protected `/api/*` HTTP API, and the org-scoped MCP transport on a shared Postgres backend (Hyperdrive), R2 blob storage, and Durable Object MCP sessions.

## Entry points

| Surface | URL pattern | Purpose |
| --- | --- | --- |
| Web console | `https://executor.sh` | Manage integrations, connections, policies, billing, and API keys |
| Protected HTTP API | `https://executor.sh/api/*` | Same core API groups as other runtimes: `tools`, `integrations`, `connections`, `providers`, `executions`, `oauth`, `policies` |
| MCP transport | `https://executor.sh/mcp` or `https://executor.sh/<org-slug>/mcp` | Streamable HTTP MCP endpoint agents connect to |
| OAuth discovery | `/.well-known/oauth-protected-resource/<org>/mcp`, `/.well-known/oauth-authorization-server` | MCP client OAuth metadata (RFC 9728) |
| OpenAPI docs | `https://executor.sh/api/docs` | Swagger UI for the HTTP API |
| Billing proxy | `https://executor.sh/api/billing/*` | Autumn subscription and usage endpoints |

Org-scoped console routes use `/<org-slug>/…` (for example `/<org-slug>/policies`, `/<org-slug>/api-keys`). Public onboarding routes (`/login`, `/create-org`, `/setup-mcp`) sit outside the org prefix.

```mermaid
flowchart TB
  subgraph clients [Clients]
    Browser[Web browser]
    MCP[MCP agents]
    CLI[executor CLI]
  end

  subgraph worker [executor-cloud Worker]
    Start[start.ts dispatch]
    App[ExecutorApp.make handler]
    DO[McpSessionDO]
    Loader[Dynamic worker loader]
  end

  subgraph external [External services]
    WorkOS[WorkOS AuthKit]
    PG[(Postgres via Hyperdrive)]
    R2[(R2 executor-cloud-blobs)]
    Autumn[Autumn billing]
  end

  Browser --> Start
  MCP --> Start
  CLI --> Start
  Start -->|/api/* and /mcp| App
  App --> DO
  App --> Loader
  App --> PG
  App --> R2
  App --> WorkOS
  App --> Autumn
```

## Sign in

Executor Cloud authenticates users through WorkOS AuthKit. A successful web login sets the `wos-session` sealed-session cookie. The protected `/api/*` plane accepts credentials in this order on the `Authorization` header: a WorkOS device-login JWT (from `executor login`), then a WorkOS API key, then (with no header) the session cookie.

### Web sign-in

<Steps>
<Step title="Open the login page">

Go to [https://executor.sh/login](https://executor.sh/login) and click **Sign in**. The page redirects to `/api/auth/login`, which starts the WorkOS hosted AuthKit flow.

</Step>
<Step title="Complete AuthKit and land in the console">

WorkOS redirects back to `/api/auth/callback` with an authorization code. On success the worker sets `wos-session` and sends you to your `returnTo` path (default: the app shell).

</Step>
<Step title="Create or join an organization">

If you have no active organization, create one at `/create-org` via `POST /api/auth/create-organization`, or accept a pending invitation from `GET /api/auth/pending-invitations`. Free accounts can belong to up to three organizations unless they hold a paid subscription on another org.

</Step>
</Steps>

Unsigned visitors hitting protected console routes are redirected to `/login` before the SPA loads. OAuth integration callbacks that arrive without a session redirect to login with the callback URL preserved.

### CLI device login

The CLI signs into cloud with the OAuth 2.0 Device Authorization Grant (RFC 8628). Discovery comes from `GET /api/auth/cli-login`, which returns WorkOS device and token endpoints plus the public client id.

<CodeGroup>
```bash title="Sign in to cloud"
executor login --base-url https://executor.sh
```

```bash title="Verify the stored profile"
executor whoami --server <profile-name>
```

```bash title="Call the API with stored credentials"
executor tools sources --server <profile-name>
```
</CodeGroup>

<ParamField body="--base-url" type="string">
  Origin of the hosted server (for example `https://executor.sh`). Mutually exclusive with `--server`.
</ParamField>

<ParamField body="--server" type="string">
  Named CLI server profile to update. Mutually exclusive with `--base-url`.
</ParamField>

<ParamField body="--name" type="string">
  Profile name to save under. Defaults to the authenticated account email.
</ParamField>

<ParamField body="--no-browser" type="boolean" default="false">
  Print the verification URL instead of opening a browser.
</ParamField>

The CLI polls the WorkOS token endpoint until you approve the device in the browser, then stores the access (and refresh) token as an `oauth` credential on the profile. Subsequent `executor` commands send `Authorization: Bearer <access_token>` to `/api/*`.

<Note>
Device-login JWTs are WorkOS *user_management* tokens verified against the SSO JWKS. MCP OAuth tokens use a separate AuthKit `/oauth2/jwks` keyset. Do not interchange them.
</Note>

## Connect MCP clients

Cloud exposes one shared MCP endpoint per organization. The install card and `/setup-mcp` page build org-pinned URLs:

```
https://executor.sh/<org-slug>/mcp
```

The server also accepts the legacy `/<org_id>/mcp` form (`org_…` WorkOS id). Bare `/mcp` resolves the organization from the authenticated token's `org_id` claim.

<Steps>
<Step title="Copy the endpoint from the console">

After sign-in, open **Connect** in the org shell or finish onboarding at `/setup-mcp`. The page prints the MCP server URL and an `npx add-mcp` install command for your org slug.

</Step>
<Step title="Add the server to your agent">

Run the generated command (HTTP transport is the default for cloud):

```bash
npx add-mcp 'https://executor.sh/<org-slug>/mcp' --transport http --name executor
```

Restart the MCP client so it picks up the new server entry.

</Step>
<Step title="Complete MCP OAuth when prompted">

MCP clients authenticate through OAuth against AuthKit (`MCP_AUTHKIT_DOMAIN`, default `https://signin.executor.sh`). Protected-resource metadata is served at `https://executor.sh/.well-known/oauth-protected-resource/<org>/mcp`.

</Step>
</Steps>

Optional query parameter `elicitation_mode` controls approval handling: `browser` (web UI), `model` (default resume tool), or `native` (client-native elicitation).

<Tip>
Cloud does not support stdio MCP (`executor mcp`). Agents must use streamable HTTP against the hosted `/mcp` path. See [Connect MCP clients](/connect-mcp-clients) for client-specific wiring.
</Tip>

## How cloud differs from other runtimes

| Dimension | Executor Cloud | Local daemon (`apps/local`) | Self-host (`apps/host-selfhost`) |
| --- | --- | --- | --- |
| Hosting | Managed Worker at `executor.sh` | Background service on your machine | Your infrastructure (Docker, etc.) |
| Identity | WorkOS AuthKit (multi-tenant orgs) | Local scope, no hosted login | Better Auth (single configured org) |
| Database | Postgres via Hyperdrive | Local SQLite / scope storage | libSQL file in `EXECUTOR_DATA_DIR` |
| MCP sessions | Durable Object (`McpSessionDO`) | In-process | In-process |
| Code execution | Cloudflare dynamic worker loader | QuickJS in-process | QuickJS in-process |
| MCP URL | `https://executor.sh/<org-slug>/mcp` | `http://localhost:<port>/mcp` or `executor mcp` stdio | `https://<your-host>/mcp` |
| Stdio MCP | Disabled (`dangerouslyAllowStdioMCP: false`) | Enabled | Disabled |
| Secret providers | WorkOS Vault plugin | Keychain, file secrets, 1Password | Encrypted DB secrets plugin |
| Billing | Autumn metering and seat gates | None | None |
| CLI login provider | WorkOS (`provider: "workos"` from `/api/auth/cli-login`) | N/A (local profile) | Better Auth |

The core HTTP API shape is identical across runtimes (`CoreExecutorApi` groups). Cloud adds auth, org, billing, and MCP-session extensions on top of the shared `ExecutorApp.make` assembly.

### Plugin set

Cloud ships only plugins safe in a multi-tenant Worker: OpenAPI, Google, Microsoft, GraphQL, HTTP MCP, and WorkOS Vault. It excludes local-only credential backends (keychain, file secrets, 1Password) and blocks stdio MCP process spawning.

```ts
// apps/cloud/executor.config.ts (abbreviated)
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
workosVaultPlugin({ credentials: workosCredentials ?? { apiKey: "", clientId: "" } }),
```

Local adds desktop settings and OS credential plugins; self-host swaps WorkOS Vault for `encryptedSecretsPlugin`.

## API authentication reference

:::endpoint GET /api/auth/cli-login
CLI device-login discovery. Returns WorkOS device authorization and token endpoints plus `clientId`. No authentication required.
:::

:::endpoint GET /api/auth/login
Starts the WorkOS hosted login redirect. Optional `returnTo` query param (same-origin relative path).
:::

:::endpoint GET /api/auth/callback
OAuth callback from WorkOS. Sets `wos-session` on success.
:::

:::endpoint GET /api/auth/me
Returns the signed-in user and active organization. Requires session or bearer credentials.
:::

For programmatic access without the CLI device flow, mint a WorkOS API key from the org **API keys** page (`/<org-slug>/api-keys`) and send `Authorization: Bearer <api_key>` to `/api/*`. API keys take precedence over the session cookie when both are present.

MCP accepts the same bearer forms plus MCP OAuth access tokens verified against AuthKit JWKS.

## Plans and limits

Free organizations include up to three members and 10,000 executions per month, with pay-as-you-go overage. Free accounts without a paid org subscription can create up to three organizations. Team and Enterprise plans lift member and execution limits.

Billing routes live under `/api/billing/*` and the console **Billing** pages (`/<org-slug>/billing`).

## Operations notes

Production deploys with `bun run deploy` from `apps/cloud` (build + `wrangler deploy`). The Worker binds:

- `HYPERDRIVE` to Postgres
- `BLOBS` to the `executor-cloud-blobs` R2 bucket
- `MCP_SESSION` to the `McpSessionDO` Durable Object class
- `LOADER` for sandboxed code execution
- `MARKETING` service for apex marketing pages

`VITE_PUBLIC_SITE_URL` is `https://executor.sh`. `ALLOW_LOCAL_NETWORK` defaults unset (`false`), blocking sandboxed code from reaching private networks.

<Warning>
Do not point MCP clients at bare `/mcp` when you need a specific organization unless the token already carries the correct `org_id`. Prefer the org-slug URL the console prints.
</Warning>

## Related pages

<CardGroup>
<Card title="MCP proxy" href="/mcp-proxy">
How Executor fronts OpenAPI, GraphQL, and upstream MCP with one endpoint, shared auth, and per-tool policies.
</Card>
<Card title="Connect MCP clients" href="/connect-mcp-clients">
Wire Cursor, Claude Code, and other agents via HTTP or stdio, including `add-mcp` setup.
</Card>
<Card title="CLI reference" href="/cli-reference">
`login`, `logout`, `whoami`, `mcp`, `call`, and server profile flags.
</Card>
<Card title="Deploy self-hosted" href="/deploy-selfhost">
Run Executor on Docker or Cloudflare Workers when you need your own infrastructure.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Connection payloads, OAuth flows, and credential provider options across runtimes.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Route groups, payloads, and error shapes for the protected API.
</Card>
</CardGroup>

---

## 17. CLI reference

> All `executor` subcommands and flags: `install`, `web`, `daemon`, `service`, `mcp`, `call`, `resume`, `tools`, `server`, `login`, and auto-start behavior.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/17-cli-reference.md
- Generated: 2026-06-23T19:26:51.492Z

### Source Files

- `apps/cli/src/main.ts`
- `apps/cli/bin/executor.ts`
- `apps/cli/src/daemon.ts`
- `apps/cli/src/daemon-state.ts`
- `apps/cli/src/service.ts`
- `apps/cli/src/server-profile.ts`
- `README.md`

---
title: "CLI reference"
description: "All `executor` subcommands and flags: `install`, `web`, `daemon`, `service`, `mcp`, `call`, `resume`, `tools`, `server`, `login`, and auto-start behavior."
---

The `executor` binary (`apps/cli/bin/executor.ts`) is the local entry point for installing a background service, opening the web UI, managing the daemon, invoking tools, resuming paused executions, exposing an MCP stdio server, and targeting hosted or local HTTP APIs. Commands are defined in `apps/cli/src/main.ts` using Effect CLI. Default local daemon port is **4788**; the OS-supervised service defaults to **4789**.

## Command overview

| Command | Purpose |
| --- | --- |
| `executor install` | Install and start the OS-supervised background service |
| `executor web` | Open the running web UI (or start a temporary foreground server) |
| `executor open` | Open the running web app in the browser with `?_token=` pre-filled |
| `executor daemon` | Run, stop, restart, or inspect the local daemon |
| `executor service` | Install, uninstall, restart, or inspect the OS-supervised service |
| `executor mcp` | Start an MCP server over stdio |
| `executor call` | Invoke a tool by dot-separated path segments |
| `executor resume` | Resume a paused execution (auth or approval) |
| `executor tools` | Search, list sources, or describe tools |
| `executor server` | Manage named server connection profiles |
| `executor login` | Sign in to a hosted server via OAuth device flow |
| `executor logout` | Clear stored credentials for a server profile |
| `executor whoami` | Show the authenticated account for a server |

## Global options

<ParamField body="-v" type="flag">
Print the CLI version and exit. Defined in `apps/cli/package.json` (currently `1.5.18`).
</ParamField>

<ParamField body="--log-level" type="string">
When set to `debug`, `trace`, or `all`, error output includes full stack traces. Otherwise errors are normalized to a short message with a hint to re-run with `--log-level debug`.
</ParamField>

## Shared server targeting flags

Several commands accept these flags to choose which Executor HTTP server to talk to:

<ParamField body="--base-url" type="string">
Executor server origin (for example `http://localhost:4788` or `https://cloud.example.com`). For local `http://` origins without stored credentials, the CLI may auto-start a daemon. Mutually exclusive with `--server`.
</ParamField>

<ParamField body="--server" type="string">
Named server profile from `~/.executor/server-connections.json`. Mutually exclusive with `--base-url`.
</ParamField>

<ParamField body="--scope" type="string">
Path to a workspace directory containing `executor.jsonc`. Sets `EXECUTOR_SCOPE_DIR` for the command.
</ParamField>

**Server resolution order** (when neither flag is passed):

1. Default server profile (if configured)
2. Active local server manifest (`~/.executor/server-control/server.json`)
3. Implicit default `http://localhost:4788`

Environment fallbacks for bearer auth when a profile has no stored credential:

- `EXECUTOR_API_KEY` (hosted API key)
- `EXECUTOR_AUTH_TOKEN` (local/desktop bearer token)

## `executor install`

Installs Executor as an OS-supervised background service. Alias of `executor service install`.

```bash
executor install [--port <port>]
```

<ParamField body="--port" type="integer" default="4789">
Loopback port the supervised daemon binds. Clients discover the live port from `server.json`.
</ParamField>

**Behavior:**

- Registers a service with the OS manager: **launchd** (macOS), **systemd --user** (Linux), or **Windows Task Scheduler** (Windows).
- Runs `<executor> daemon run --foreground --port <port> --hostname 127.0.0.1`.
- Waits up to 45 seconds for a reachable `cli-daemon` manifest at `http://127.0.0.1:<port>`.
- Idempotent: if the same version, port, and executable are already running, prints a noop message.
- **Requires the compiled binary** in production. In a dev checkout (`bun run src/main.ts`), fails with a message to use `executor daemon run --foreground` instead.

<RequestExample>

```bash
executor install
executor install --port 4789
```

</RequestExample>

<Check>
After install, open the UI with `executor web` or `executor open`.
</Check>

## `executor service`

Manages the same OS-supervised daemon as `install`, with explicit lifecycle commands.

| Subcommand | Description |
| --- | --- |
| `service install [--port <port>]` | Same as `executor install` |
| `service uninstall` | Stop and remove the OS service registration |
| `service status` | Show platform, registration, running state, serving origin, and version drift |
| `service restart` | Restart via the OS manager (`launchctl kickstart`, `systemctl --user restart`, or Task Scheduler) |

Service label: `sh.executor.daemon`. Logs: `~/.executor/logs/daemon.log` and `daemon.error.log`.

<Warning>
On Windows, `service install` may require an **Administrator** PowerShell because boot-triggered Scheduled Tasks need elevation.
</Warning>

## `executor web`

Opens the Executor web UI.

```bash
executor web [--foreground] [--port <port>] [--hostname <host>] [--allowed-host <origin>...] [--auth-token <token>] [--scope <dir>]
```

**Default (no `--foreground`):** reads the active local server manifest and opens the browser to `{origin}/?_token={bearer}`. If nothing is running, prints install instructions.

**With `--foreground`:** starts a temporary in-process server, publishes a `foreground` manifest, and blocks until Ctrl+C.

<ParamField body="--foreground" type="boolean" default="false">
Run a temporary web server in the current terminal instead of opening the installed background service.
</ParamField>

<ParamField body="--port" type="integer" default="4788">
Port for the `--foreground` server.
</ParamField>

<ParamField body="--hostname" type="string" default="127.0.0.1">
Bind address for `--foreground`. Use `0.0.0.0` to listen on all interfaces (only on trusted networks).
</ParamField>

<ParamField body="--allowed-host" type="string" repeatable>
Grant an extra CORS origin for `--foreground`. Localhost is always allowed; bearer token is the access gate.
</ParamField>

<ParamField body="--auth-token" type="string">
Override the bearer token for `--foreground`. Defaults to the stable token in `auth.json`.
</ParamField>

<RequestExample>

```bash
executor web
executor web --foreground
executor web --foreground --port 4788 --hostname 0.0.0.0
```

</RequestExample>

## `executor open`

Same browser-open behavior as `executor web` without `--foreground`: reads `server.json`, prints the URL, and opens `{origin}/?_token={token}`.

## `executor daemon`

Manages the local Executor HTTP daemon (distinct from the OS-supervised service, though both run `daemon run` under the hood).

| Subcommand | Description |
| --- | --- |
| `daemon run` | Start the daemon (background by default) |
| `daemon status [--base-url <url>]` | Show running state, PID, and scope |
| `daemon stop [--base-url <url>]` | SIGTERM the recorded PID and clean up state files |
| `daemon restart [--base-url <url>] [--scope <dir>]` | Stop then ensure the daemon is reachable again |

### `daemon run` flags

<ParamField body="--port" type="integer" default="4788">
Preferred listen port. If busy, an ephemeral port is chosen and reported on stderr.
</ParamField>

<ParamField body="--hostname" type="string" default="127.0.0.1">
Bind address. Keep local unless you trust the network.
</ParamField>

<ParamField body="--foreground" type="boolean" default="false">
Run in the current process (used by OS service managers). Without it, spawns a detached child.
</ParamField>

<ParamField body="--allowed-host" type="string" repeatable>
Extra CORS origins (repeatable).
</ParamField>

<ParamField body="--auth-token" type="string">
Override bearer token; defaults to `auth.json` under `EXECUTOR_DATA_DIR`.
</ParamField>

**State files** (under `EXECUTOR_DATA_DIR`, default `~/.executor`):

- `daemon-{host}-{port}.json` — PID record for a specific port
- `daemon-active-{host}-{scope-hash}.json` — scope-scoped pointer with token
- `server-control/server.json` — active server manifest with bearer token (mode `0600`)

<RequestExample>

```bash
executor daemon run
executor daemon run --foreground --port 4788
executor daemon status
executor daemon stop
executor daemon restart
```

</RequestExample>

## `executor mcp`

Starts an MCP server over stdio for Cursor, Claude Code, OpenCode, and other MCP clients.

```bash
executor mcp [--scope <dir>] [--elicitation-mode browser|model]
```

<ParamField body="--elicitation-mode" type="choice" default="model">
`browser`: approval pauses open `{baseUrl}/resume/{executionId}` in the browser. `model`: exposes a CLI resume tool to the model.
</ParamField>

**Behavior:**

- Acquires a local server startup lock, picks an available port (preferred 4788), and starts an in-process server.
- Reroutes stdout to stderr so JSON-RPC on stdout stays clean.
- Publishes a `foreground` manifest for the MCP session.
- Stops the server when the MCP session ends.

<Tip>
Example MCP client config: `"command": "executor", "args": ["mcp"]`. See [Connect MCP clients](/connect-mcp-clients).
</Tip>

## `executor call`

Invokes a tool by dot-separated path segments. The CLI compiles the path and trailing JSON args into TypeScript, executes it via the `/executions/execute` API, and prints the result.

```bash
executor call <segment> [<segment> ...] ['{"key":"value"}']
```

### Browse mode (`--help`)

```bash
executor call <path...> --help [--match "<text>"] [--limit <n>] [--base-url <url>] [--server <profile>] [--scope <dir>]
```

`--match` filters large namespaces; `--limit` caps listed subcommands.

<RequestExample>

```bash
executor call github issues create '{"owner":"octocat","repo":"Hello-World","title":"Hi"}'
executor call github --help
executor call cloudflare --help --match dns --limit 20
```

</RequestExample>

**Paused executions:** when a call pauses for auth or approval, the CLI prints the execution ID, a browser approval URL, and suggested `executor resume` commands.

## `executor resume`

Resumes a paused execution.

```bash
executor resume --execution-id <id> [--action accept|decline|cancel] [--content '<json>'] [--base-url <url>] [--server <profile>] [--scope <dir>]
```

<ParamField body="--execution-id" type="string" required>
Execution ID from a paused `call` or MCP elicitation.
</ParamField>

<ParamField body="--action" type="choice" default="accept">
`accept`, `decline`, or `cancel`.
</ParamField>

<ParamField body="--content" type="string">
JSON object payload when `--action accept` and the pause requested form input.
</ParamField>

<RequestExample>

```bash
executor resume --execution-id exec_abc123
executor resume --execution-id exec_abc123 --action accept --content '{"approved":true}'
executor resume --execution-id exec_abc123 --action decline
```

</RequestExample>

## `executor tools`

| Subcommand | Description |
| --- | --- |
| `tools search <query> [--namespace <ns>] [--limit <n>]` | Search tools by natural-language query (default limit 12) |
| `tools sources [--query <text>] [--limit <n>]` | List configured sources and tool counts (default limit 50) |
| `tools describe <path>` | Show a tool's TypeScript and JSON schema |

All `tools` subcommands accept `--base-url`, `--server`, and `--scope`, and auto-start a local daemon when needed.

<RequestExample>

```bash
executor tools search "send email"
executor tools sources
executor tools describe github.issues.list
```

</RequestExample>

## `executor server`

Manages named connection profiles in `~/.executor/server-connections.json`.

| Subcommand | Description |
| --- | --- |
| `server add <name> <origin> [--display-name <label>] [--kind http\|desktop-sidecar] [--default]` | Add or update a profile |
| `server list` | List profiles (`*` marks default) |
| `server use <name>` | Set the default profile |
| `server remove <name>` | Remove a profile |
| `server rotate-token` | Rotate the local server bearer token in `auth.json` |

Profile names: letters, numbers, dots, underscores, dashes (`^[A-Za-z0-9_.-]+$`).

Stored auth kinds: `basic`, `bearer`, `oauth` (from device login).

<RequestExample>

```bash
executor server add local http://localhost:4788 --default
executor server list
executor server use cloud
executor server rotate-token
```

</RequestExample>

## `executor login`

Signs in to a hosted Executor server using OAuth device flow (browser verification).

```bash
executor login [--base-url <origin>] [--server <profile>] [--name <profile-name>] [--no-browser]
```

<ParamField body="--base-url" type="string">
Hosted server origin. Mutually exclusive with `--server`.
</ParamField>

<ParamField body="--server" type="string">
Existing profile to re-authenticate.
</ParamField>

<ParamField body="--name" type="string">
Profile name to save under. Defaults to account email or user ID.
</ParamField>

<ParamField body="--no-browser" type="boolean" default="false">
Print the verification URL instead of opening a browser.
</ParamField>

On success, stores OAuth tokens in the profile and sets it as default. Tokens refresh automatically ~60 seconds before expiry.

### `executor logout` and `executor whoami`

```bash
executor logout [--base-url <origin>] [--server <profile>]
executor whoami [--base-url <origin>] [--server <profile>]
```

`logout` clears stored credentials for the resolved profile. `whoami` prints server origin, account email (from profile display name), user ID, organization, and token expiry.

## Auto-start behavior

Commands that talk to a local HTTP server without stored credentials may spawn a daemon automatically: **`call`**, **`resume`**, **`tools`**, and any command using default local targeting.

```mermaid
sequenceDiagram
  participant CLI as executor CLI
  participant Lock as daemon-active lock
  participant Daemon as daemon process
  participant Health as GET /api/health

  CLI->>Health: Probe target origin
  alt Already reachable
    Health-->>CLI: ok
    CLI->>CLI: Use active manifest bearer
  else Local host, no auth, not reachable
    CLI->>Lock: acquireDaemonStartLock
    CLI->>Daemon: spawnDetached daemon run --foreground
    loop up to 15s
      CLI->>Health: Probe
    end
    Lock-->>CLI: release lock
    CLI->>CLI: Read server.json bearer token
  end
```

### When auto-start runs

| Condition | Auto-start |
| --- | --- |
| `http://` origin on `localhost`, `127.0.0.1`, `::1`, or `[::1]` | Yes |
| Connection has stored or env auth (`oauth`, `bearer`, `basic`) | No — uses remote server directly |
| `https://` or non-local hostname | No — fails with manual start instructions |
| Another local server already owns the data directory | No — fails with conflict error |

### Port selection

1. Try the requested port (default **4788** from URL or implicit default).
2. If busy, pick an ephemeral port and print: `Port 4788 is in use. Starting daemon on available port <N> instead.`
3. Track the chosen port in daemon pointer and manifest files.

### Concurrency

- `daemon-active-*.json.lock` serializes daemon spawns across concurrent CLI invocations.
- `server-control/startup.lock` serializes local server starts (foreground web, MCP, supervised daemon).

### Commands that do **not** use daemon auto-start

- `executor web` (without `--foreground`) — requires an already-running service
- `executor install` / `executor service install` — registers OS service instead
- `executor mcp` — starts its own ephemeral foreground server, not the scope daemon
- `executor daemon run --foreground` — you start it explicitly

<Note>
`executor mcp` always starts a dedicated foreground server on an available port. It does not attach to an existing daemon.
</Note>

## Runtime paths and environment

| Variable / path | Role |
| --- | --- |
| `EXECUTOR_DATA_DIR` | Data root (default `~/.executor`) |
| `EXECUTOR_SCOPE_DIR` | Workspace scope for daemon pointer and DB isolation |
| `EXECUTOR_WEB_BASE_URL` | Web UI base URL override (set automatically during local sessions) |
| `EXECUTOR_API_KEY` / `EXECUTOR_AUTH_TOKEN` | Bearer fallback for server connections |
| `EXECUTOR_SUPERVISED` | Set by OS service managers; daemon reclaims stale manifests on boot |
| `~/.executor/server-connections.json` | Named server profiles |
| `~/.executor/server-control/server.json` | Active local server manifest |
| `~/.executor/auth.json` | Stable local bearer token (mode `0600`) |

See [Configuration reference](/configuration-reference) for the full environment variable list.

## Error handling

- Most failures exit with code `1`.
- Normalized error text strips ANSI sequences and repeated `Error:` prefixes.
- Use `--log-level debug` for full `Cause.pretty` output.
- Paused executions exit `0` from `resume` when still paused after an interaction.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install the published CLI and verify the background service starts.
</Card>
<Card title="Quickstart" href="/quickstart">
Install, open the web UI, add an integration, and call your first tool.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and invocation across CLI, HTTP, and MCP.
</Card>
<Card title="Connect MCP clients" href="/connect-mcp-clients">
Wire Cursor, Claude Code, and other clients to `executor mcp`.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Environment variables, ports, and data directory layout.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Daemon port conflicts, unreachable servers, and recovery commands.
</Card>
</CardGroup>

---

## 18. HTTP API reference

> Core Executor HTTP API groups: `tools`, `integrations`, `connections`, `providers`, `executions`, `oauth`, and `policies` routes, payloads, and error shapes.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/18-http-api-reference.md
- Generated: 2026-06-23T19:27:48.714Z

### Source Files

- `packages/core/api/src/api.ts`
- `packages/core/api/src/tools/api.ts`
- `packages/core/api/src/integrations/api.ts`
- `packages/core/api/src/connections/api.ts`
- `packages/core/api/src/executions/api.ts`
- `packages/core/api/src/oauth/api.ts`
- `packages/core/api/src/policies/api.ts`

---
title: "HTTP API reference"
description: "Core Executor HTTP API groups: `tools`, `integrations`, `connections`, `providers`, `executions`, `oauth`, and `policies` routes, payloads, and error shapes."
---

Executor exposes a typed Effect `HttpApi` (`CoreExecutorApi`) composed with plugin groups via `composePluginApi(plugins)`. Every host serves the combined API under the `/api` path prefix (for example `http://localhost:4788/api` on the local daemon). Routes are grouped by resource name; the typed client mirrors group names (`api.tools.list`, `api.connections.create`, and so on).

<Note>
Tool invocation is not a standalone HTTP route in the core API. Call tools through `POST /executions` (sandbox code) or the MCP `execute` tool. The `tools` group is catalog read-only: list metadata and fetch schemas.
</Note>

## Base URL and authentication

| Runtime | Typical base URL | Auth mechanism |
| --- | --- | --- |
| Local daemon | `http://localhost:4788/api` | Single-user token (see `apps/local/src/auth.ts`) |
| Self-hosted | `https://<host>/api` | Better Auth session cookie or API key |
| Executor Cloud | `https://<org>.executor.dev/api` | WorkOS sealed session or Bearer API key |

Protected routes run behind `IdentityProvider` middleware. Auth failures use tagged errors:

| Error | Status | When |
| --- | --- | --- |
| `Unauthorized` | 401 | Missing or invalid credential |
| `NoOrganization` | 403 | Valid credential, no organization binding |
| `Unavailable` | 503 | Transient auth backend outage (cloud API-key validation) |

Hosts may attach optional `code` and `message` fields on auth errors for UI display. Credential precedence (cookie vs Bearer vs API key) is host-specific and stays inside each `IdentityProvider` implementation.

Unauthenticated probes exist outside the seven core groups: `GET /api/health` (liveness) and, on self-host, `GET /api/setup-status`. OpenAPI Swagger is available at `/api/docs` when the host wires `HttpApiSwagger`.

## API composition

```text
CoreExecutorApi ("executor")
├── tools
├── integrations
├── connections
├── providers
├── executions
├── oauth
└── policies

+ plugin groups (per loaded plugin)
  ├── openapi   → /openapi/*
  ├── graphql   → /graphql/*
  ├── mcp       → /mcp/*
  └── …
```

Plugin routes extend the same `/api` namespace. Registration endpoints (`POST /openapi/specs`, GraphQL `addIntegration`, MCP `addServer`, and similar) live in plugin groups, not in the core `integrations` group. The core `integrations` group is the tenant catalog (list, get, update, remove, detect).

## Error shapes

Executor uses Effect Schema tagged errors on the wire. Domain failures include a `_tag` discriminator and typed fields. HTTP status comes from `httpApiStatus` annotations on each error schema.

<ResponseField name="_tag" type="string">
Tagged error name, for example `ConnectionNotFoundError`, `ToolNotFoundError`, `OAuthStartError`.
</ResponseField>

<ResponseField name="traceId" type="string">
Present only on `InternalError` (HTTP 500). Opaque correlation id for operator lookup. No internal message or stack crosses the wire.
</ResponseField>

| Error | Status | Typical fields |
| --- | --- | --- |
| `InternalError` | 500 | `traceId` |
| `ToolNotFoundError` | 404 | `address`, optional `suggestions` |
| `IntegrationNotFoundError` | 404 | `slug` |
| `IntegrationRemovalNotAllowedError` | 409 | `slug` |
| `ConnectionNotFoundError` | 404 | `owner`, `integration`, `name` |
| `InvalidConnectionInputError` | 400 | `message` |
| `CredentialProviderNotRegisteredError` | 409 | `provider` |
| `ExecutionNotFoundError` | 404 | `executionId` |
| `OAuthStartError` | 400 | `message` |
| `OAuthCompleteError` | 400 | `message`, optional `restartRequired` |
| `OAuthProbeError` | 400 | `message` |
| `OAuthRegisterDynamicError` | 400 | `message` |
| `OAuthSessionNotFoundError` | 404 | `state` |

Storage failures inside handlers are captured and re-thrown as `InternalError({ traceId })` at the HTTP edge.

## Shared identifiers

| Identifier | Shape | Example |
| --- | --- | --- |
| `Owner` | `"org"` or `"user"` | Workspace-shared vs personal credentials |
| `IntegrationSlug` | string | `vercel`, `executor` |
| `ConnectionName` | string | `prod` |
| `ConnectionAddress` | string | `tools.vercel.org.prod` |
| `ToolAddress` | dotted string | `tools.vercel.org.prod.listProjects` |
| `PolicyId` | string | Opaque rule id |
| `OAuthClientSlug` | string | Registered OAuth app slug |
| `OAuthState` | string | Flow correlation token from `start` |
| `ProviderKey` | string | `default`, `keychain`, `1password` |
| `ProviderItemId` | string | Opaque handle in the provider backend |

`ToolAddress` is always carried as a query parameter, never as a path segment, because dots are valid in addresses.

## Tools

Catalog read surface. Tools are per-connection and address-keyed.

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/tools` | List tool metadata |
| `GET` | `/tools/schema` | Full schema view for one address |

:::endpoint GET /tools List tools

<ParamField query="integration" type="IntegrationSlug">
Filter by integration slug.
</ParamField>

<ParamField query="owner" type="Owner">
Filter by owner (`org` or `user`).
</ParamField>

<ParamField query="connection" type="ConnectionName">
Filter by connection name.
</ParamField>

<ParamField query="query" type="string">
Free-text search across tool names and descriptions.
</ParamField>

<ParamField query="includeAnnotations" type="string">
Pass `"true"` to include `mayElicit`, `requiresApproval`, and `approvalDescription`.
</ParamField>

<ParamField query="includeBlocked" type="string">
Pass `"false"` to omit tools blocked by policy. Default includes blocked tools.
</ParamField>

<ResponseField name="address" type="ToolAddress" />
<ResponseField name="owner" type="Owner" />
<ResponseField name="integration" type="IntegrationSlug" />
<ResponseField name="connection" type="ConnectionName" />
<ResponseField name="name" type="string" />
<ResponseField name="pluginId" type="string" />
<ResponseField name="description" type="string" />
<ResponseField name="requiresApproval" type="boolean">
Plugin-derived default approval annotation when no user policy matches.
</ResponseField>

:::

:::endpoint GET /tools/schema Get tool schema

<ParamField query="address" type="ToolAddress" required>
Full tool address.
</ParamField>

Returns `ToolSchemaView`: `inputSchema`, `outputSchema`, `schemaDefinitions`, and optional TypeScript preview strings.

Errors: `ToolNotFoundError` (404), `InternalError` (500).

:::

## Integrations

Tenant-level catalog identities. No scope segment in paths; the executor binds catalog access from request auth.

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/integrations` | List integrations |
| `GET` | `/integrations/:slug` | Get one integration |
| `PATCH` | `/integrations/:slug` | Update display metadata |
| `DELETE` | `/integrations/:slug` | Remove integration |
| `POST` | `/integrations/detect` | Autodetect integration from URL |

:::endpoint GET /integrations/:slug Get integration

<ResponseField name="slug" type="IntegrationSlug" />
<ResponseField name="name" type="string">
Display name.
</ResponseField>
<ResponseField name="description" type="string" />
<ResponseField name="kind" type="string">
Owning plugin id (for example `openapi`, `mcp`, `built-in`).
</ResponseField>
<ResponseField name="canRemove" type="boolean" />
<ResponseField name="canRefresh" type="boolean" />
<ResponseField name="authMethods" type="AuthMethodDescriptor[]">
Declared auth methods with `kind` (`oauth`, `apikey`, `header`, `none`), `template`, optional `placements`, and optional `oauth` discovery fields.
</ResponseField>
<ResponseField name="displayUrl" type="string">
Non-secret URL for favicons.
</ResponseField>

Errors: `IntegrationNotFoundError` (404).

:::

:::endpoint POST /integrations/detect Detect integration from URL

<ParamField body="url" type="string" required>
Endpoint to probe. Max length 2048 characters.
</ParamField>

Returns an array of `IntegrationDetectionResult` objects (one per matching plugin): `kind`, `confidence` (`high` | `medium` | `low`), `endpoint`, `name`, `slug`.

:::

:::endpoint DELETE /integrations/:slug Remove integration

Returns `{ removed: true }`.

Errors: `IntegrationRemovalNotAllowedError` (409) when `canRemove` is false (static plugin integrations).

:::

## Connections

Owner-scoped credentials bound 1:1 to an integration. Identity is `(owner, integration, name)`.

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/connections` | List connections |
| `POST` | `/connections` | Create connection |
| `GET` | `/connections/:owner/:integration/:name` | Get connection |
| `PATCH` | `/connections/:owner/:integration/:name` | Update metadata |
| `DELETE` | `/connections/:owner/:integration/:name` | Remove connection |
| `POST` | `/connections/:owner/:integration/:name/refresh` | Refresh tools from upstream |

:::endpoint POST /connections Create connection

Exactly one credential origin is required: `value`, `values`, or `from`.

<ParamField body="owner" type="Owner" required />
<ParamField body="name" type="ConnectionName" required />
<ParamField body="integration" type="IntegrationSlug" required />
<ParamField body="template" type="AuthTemplateSlug" required />
<ParamField body="value" type="string">
Single pasted credential (sugar for a `token` input).
</ParamField>
<ParamField body="values" type="Record<string, string>">
Named inputs for multi-field auth methods.
</ParamField>
<ParamField body="from" type="object">
External reference: `{ provider: ProviderKey, id: ProviderItemId }`.
</ParamField>
<ParamField body="identityLabel" type="string | null">
Optional display label.
</ParamField>
<ParamField body="description" type="string | null">
Optional description.
</ParamField>

<ResponseField name="address" type="ConnectionAddress" />
<ResponseField name="provider" type="ProviderKey" />
<ResponseField name="oauthClient" type="OAuthClientSlug | null">
OAuth app slug when minted via OAuth; never a secret.
</ResponseField>
<ResponseField name="expiresAt" type="number | null">
Token expiry epoch ms when applicable.
</ResponseField>

Errors: `IntegrationNotFoundError` (404), `CredentialProviderNotRegisteredError` (409), `InvalidConnectionInputError` (400).

:::

:::endpoint POST /connections/:owner/:integration/:name/refresh Refresh connection tools

Re-discovers tools from the upstream integration and returns the updated tool list for that connection.

:::

## Providers

Credential-backend discovery for external secret stores.

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/providers` | List registered provider keys |
| `GET` | `/providers/:key/items` | Browse provider entries |

<ResponseField name="id" type="ProviderItemId">
Opaque item handle for `from: { provider, id }` on connection create.
</ResponseField>
<ResponseField name="name" type="string">
Human-readable entry label.
</ResponseField>

## Executions

Code-mode execution via QuickJS. Paused states cover auth elicitation, approval gates, and form input.

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/executions` | Execute sandbox code |
| `GET` | `/executions/:executionId` | Read paused execution info |
| `POST` | `/executions/:executionId/resume` | Resume a paused execution |

:::endpoint POST /executions Execute code

<ParamField body="code" type="string" required>
JavaScript executed in the QuickJS sandbox with access to `tools.*` bindings.
</ParamField>

<ResponseExample>

```json
{
  "status": "completed",
  "text": "…",
  "structured": {},
  "isError": false
}
```

```json
{
  "status": "paused",
  "text": "Execution paused: …\nexecutionId: …",
  "structured": { "executionId": "…", "elicitation": { … } }
}
```

</ResponseExample>

:::

:::endpoint POST /executions/:executionId/resume Resume paused execution

<ParamField body="action" type="string" required>
One of `accept`, `decline`, or `cancel`.
</ParamField>
<ParamField body="content" type="unknown">
Form elicitation payload when the pause expects user input.
</ParamField>

Returns the same `completed` | `paused` union as `execute`.

Errors: `ExecutionNotFoundError` (404) when the id is unknown or no longer paused.

:::

<RequestExample>

```bash
curl -X POST "$BASE/api/executions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code":"const r = await tools.executor.coreTools.tools.list({}); return JSON.stringify(r);"}'
```

</RequestExample>

## OAuth

OAuth is a credential mechanism, not an integration type. Register apps, run flows, and mint connections.

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/oauth/clients` | Register OAuth app (manual client id/secret) |
| `POST` | `/oauth/clients/register-dynamic` | RFC 7591 dynamic client registration |
| `GET` | `/oauth/clients` | List client summaries (no secrets) |
| `DELETE` | `/oauth/clients/:slug` | Remove OAuth app |
| `POST` | `/oauth/start` | Start flow to mint a connection |
| `POST` | `/oauth/complete` | Exchange authorization code |
| `POST` | `/oauth/cancel` | Cancel in-flight session |
| `POST` | `/oauth/probe` | Discover authorization-server metadata |
| `GET` | `/oauth/callback` | Browser popup callback (HTML) |

:::endpoint POST /oauth/start Start OAuth flow

<ParamField body="client" type="OAuthClientSlug" required />
<ParamField body="clientOwner" type="Owner" required>
Owner of the OAuth app (a personal connection may use a workspace app).
</ParamField>
<ParamField body="owner" type="Owner" required>
Owner of the connection being minted.
</ParamField>
<ParamField body="name" type="ConnectionName" required />
<ParamField body="integration" type="IntegrationSlug" required />
<ParamField body="template" type="AuthTemplateSlug" required />
<ParamField body="redirectUri" type="string | null">
Required for `authorization_code` grants.
</ParamField>

Returns either:

- `{ status: "connected", connection: ConnectionResponse }` for `client_credentials` or inline completion
- `{ status: "redirect", authorizationUrl, state }` when the user must visit the authorization URL

Errors: `OAuthStartError` (400).

:::

:::endpoint POST /oauth/complete Complete authorization code exchange

<ParamField body="state" type="OAuthState" required />
<ParamField body="code" type="string" required />
<ParamField body="callbackDomain" type="string | null">
Regional host echoed by the authorization server (Datadog `domain`/`site`).
</ParamField>

Returns a `ConnectionResponse`. Errors: `OAuthCompleteError` (400), `OAuthSessionNotFoundError` (404).

:::

:::endpoint GET /oauth/callback Browser callback

Query params: `state`, optional `code`, optional `error`, optional `error_description`, optional `domain`, optional `site`.

Renders HTML for the OAuth popup. The popup posts completion results to the opener via `postMessage` / `BroadcastChannel`. Allow-list redirect URI: `${origin}/api/oauth/callback`.

:::

<Warning>
`DELETE /oauth/clients/:slug` is idempotent but does not cascade to connections. Connections that referenced the removed slug keep their stored value and fail at the next token refresh.
</Warning>

## Policies

Owner-scoped tool policies gate invocation by pattern and action. Org rules are the outer guardrail; the most restrictive matched action across owners wins.

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/policies` | List policies |
| `POST` | `/policies` | Create policy |
| `PATCH` | `/policies/:policyId` | Update policy |
| `DELETE` | `/policies/:policyId` | Remove policy |

:::endpoint POST /policies Create policy

<ParamField body="owner" type="Owner" required />
<ParamField body="pattern" type="string" required>
Tool address glob (for example `tools.github.org.*.createIssue`).
</ParamField>
<ParamField body="action" type="string" required>
One of `approve`, `require_approval`, or `block`.
</ParamField>
<ParamField body="position" type="string">
Ordering hint for rule precedence within the owner.
</ParamField>

<ResponseField name="id" type="PolicyId" />
<ResponseField name="createdAt" type="number">
Epoch milliseconds.
</ResponseField>

:::

Delete and update payloads include `owner` to scope the mutation. Delete returns `{ removed: true }`.

| Action | Effect |
| --- | --- |
| `approve` | Allow without extra gate |
| `require_approval` | Pause execution until user approves via resume |
| `block` | Reject invocation with `ToolBlockedError` |

## Typed client

Use `HttpApiClient.make(composePluginApi(plugins), { baseUrl, transformClient })` with auth headers on every request. The e2e harness resolves `baseUrl` to `new URL("/api", target.baseUrl)`.

```typescript
import { HttpApiClient } from "effect/unstable/httpapi";
import { composePluginApi } from "@executor-js/api/server";

const api = composePluginApi([/* plugins */] as const);
const client = await Effect.runPromise(
  HttpApiClient.make(api, { baseUrl: "http://localhost:4788/api" }),
);
const tools = await Effect.runPromise(client.tools.list({ query: {} }));
```

The `@executor-js/api` package exports each group's route schemas (`ToolsApi`, `ConnectionsApi`, and so on) for OpenAPI generation and client derivation.

## Related pages

<CardGroup>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and how invocation flows through executions and MCP.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credential model and OAuth minting.
</Card>
<Card title="Executions" href="/executions">
Paused states, resume actions, and elicitation handling.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Credential providers and connection create payloads.
</Card>
<Card title="SDK reference" href="/sdk-reference">
`createExecutor` and promise-mode entry points that mirror these routes.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`EXECUTOR_DATA_DIR`, ports, and web base URL env vars.
</Card>
</CardGroup>

---

## 19. SDK reference

> `@executor-js/sdk` exports: `createExecutor`, plugin wiring, tool listing and invocation, connection APIs, policy helpers, typed IDs, and promise-mode entry points.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/19-sdk-reference.md
- Generated: 2026-06-23T19:27:23.422Z

### Source Files

- `packages/core/sdk/package.json`
- `packages/core/sdk/src/executor.ts`
- `packages/core/sdk/src/promise.ts`
- `packages/core/sdk/src/ids.ts`
- `packages/core/sdk/src/policies.ts`
- `packages/core/sdk/src/errors.ts`
- `packages/core/sdk/src/elicitation.ts`

---
title: "SDK reference"
description: "`@executor-js/sdk` exports: `createExecutor`, plugin wiring, tool listing and invocation, connection APIs, policy helpers, typed IDs, and promise-mode entry points."
---

`@executor-js/sdk` is the programmatic surface for composing an Executor runtime in application code: register integrations through protocol plugins, create owner-scoped connections, list and invoke tools by address, manage tool policies, and shut down cleanly. The published package defaults to a Promise façade; the Effect-native core entry remains available for plugin authors and hosts that already run on Effect.

## Package entry points

| Import path | Audience | Returns |
| --- | --- | --- |
| `@executor-js/sdk` / `@executor-js/sdk/promise` | Application embedders | Promise-based `createExecutor`, domain types, tagged errors |
| `@executor-js/sdk/core` | Hosts and Effect-native callers | Effect-based `createExecutor`, full plugin SDK, FumaDB helpers |
| `@executor-js/sdk/shared` | Browser UI, plugin client code | Branded IDs, domain types, policy helpers (no Node/FumaDB) |
| `@executor-js/sdk/client` | Plugin React UI | `defineClientPlugin`, Atom/HttpApi reactivity primitives |
| `@executor-js/sdk/testing` | Tests | `makeTestExecutor`, OAuth test server, SQLite test DB |
| `@executor-js/sdk/http-auth` | Protocol plugins | Placement-based auth template vocabulary (`variable`, `ApiKeyAuthTemplate`, …) |

<Note>
Published npm types for the root export resolve to the Promise surface (`dist/promise.d.ts`). Plugin factories remain Effect-native; the Promise proxy unwraps plugin extension methods (for example `executor.openapi.addSpec`) into `async` calls.
</Note>

## `createExecutor`

### Promise mode (recommended for embedders)

```typescript
import { createExecutor } from "@executor-js/sdk/promise";
import { openApiPlugin } from "@executor-js/plugin-openapi/promise";

const executor = await createExecutor({
  plugins: [openApiPlugin()],
  providers: [memoryProvider],
  onElicitation: "accept-all",
});

await executor.close();
```

<ParamField body="tenant" type="string">
Org/workspace partition. Defaults to `"default-tenant"`. `owner: "org"` rows file here.
</ParamField>

<ParamField body="subject" type="string">
Acting member identity. Omit for a pure-org executor that cannot write `owner: "user"` connections.
</ParamField>

<ParamField body="plugins" type="readonly AnyPlugin[]">
Protocol and provider plugins to load. Each plugin contributes an extension namespace keyed by `plugin.id` (for example `openapi`, `mcp`).
</ParamField>

<ParamField body="providers" type="readonly CredentialProvider[]" required={false}>
Config-level credential backends, merged with every `plugin.credentialProviders`. Config providers register first, so the default writable store is chosen from them when present. A writable provider is required before `connections.create({ value })` can persist an inline credential.
</ParamField>

<ParamField body="db" type="FumaDb | factory" required={false}>
FumaDB handle or factory receiving `collectTables()`. When omitted, `createExecutor` provisions an in-memory database.
</ParamField>

<ParamField body="onElicitation" type="PromiseOnElicitation" required>
How to answer mid-invocation user prompts. Pass `"accept-all"` for tests and non-interactive hosts, or `(ctx) => ElicitationResponse | Promise<ElicitationResponse>` for interactive flows. Required at construction so callers do not thread per-invoke options.
</ParamField>

### Effect mode (hosts and plugin authors)

```typescript
import { Effect } from "effect";
import { createExecutor } from "@executor-js/sdk/core";

const executor = await Effect.runPromise(
  createExecutor({
    tenant: Tenant.make("acme"),
    subject: Subject.make("user-1"),
    plugins: [openApiPlugin()],
    onElicitation: "accept-all",
  }),
);

await Effect.runPromise(executor.close());
```

The Effect `ExecutorConfig` adds host-only options not exposed on the Promise config:

| Option | Purpose |
| --- | --- |
| `blobs` | Override the plugin blob store (default: FumaDB `blob` table) |
| `httpClientLayer` | Custom `HttpClient` layer for plugin HTTP |
| `fetch` | Fetch implementation when a layer is impractical |
| `redirectUri` | OAuth callback URL (`${webBaseUrl}/oauth/callback`). Omit only when the executor never runs interactive OAuth |
| `oauthEndpointUrlPolicy` | Policy for OAuth endpoint URL validation |
| `coreTools` | Enable built-in agent-facing static tools over integrations, connections, and policies |

## Executor surface

Every namespace method on the returned executor is available in both modes; Promise mode unwraps `Effect` return values into `Promise`s and rejects with tagged errors.

```text
Executor
├── integrations   list, get, update, remove, detect
├── connections    create, list, get, update, remove, refresh
├── oauth          createClient, registerDynamicClient, listClients, removeClient,
│                  start, complete, cancel, probe
├── tools          list, schema
├── providers      list, items
├── policies       list, create, update, remove, resolve
├── execute        invoke one tool by address
├── close          release DB and runtime resources
└── <pluginId>     per-plugin extensions (e.g. openapi.addSpec)
```

### Integrations

| Method | Returns | Errors |
| --- | --- | --- |
| `list()` | `readonly Integration[]` | `StorageFailure` |
| `get(slug)` | `Integration \| null` | `StorageFailure` |
| `update(slug, patch)` | `void` | `IntegrationNotFoundError`, `StorageFailure` |
| `remove(slug)` | `void` | `IntegrationRemovalNotAllowedError`, `StorageFailure` |
| `detect(url)` | `readonly IntegrationDetectionResult[]` | `StorageFailure` |

Integrations are tenant-level catalog identities. Type-specific configuration (OpenAPI spec, MCP URL, auth templates) is owned by the registering plugin and stored as opaque `config`. Use plugin extensions to add integrations (for example `executor.openapi.addSpec`); the core `integrations` namespace manages the catalog after registration.

### Connections

| Method | Purpose |
| --- | --- |
| `create(input)` | Save a credential for one integration. Identity is `(owner, integration, name)`. |
| `list(filter?)` | List connections, optionally filtered by `integration` or `owner`. |
| `get(ref)` | Fetch one connection by `ConnectionRef`. |
| `update(ref, input)` | Edit `description` or `identityLabel` only. |
| `remove(ref)` | Delete the connection and its tool rows. |
| `refresh(ref)` | Re-resolve tools from the upstream spec or server. |

<ParamField body="owner" type='"org" | "user"' required>
`"org"` for tenant-shared credentials; `"user"` for the acting subject's private credentials. Requires `subject` on the executor when `"user"`.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Connection name segment in tool addresses.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Target integration slug.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
Which declared auth method the credential applies through. Use `"none"` (`NO_AUTH_TEMPLATE`) for integrations that require no credential.
</ParamField>

<ParamField body="value | from | values | inputs" type="ConnectionValueInput" required={false}>
Credential origin. `value` pastes a single `token` input to the default writable provider; `from` references an external provider item; `values` and `inputs` carry multi-input maps. For OAuth, use `oauth.start` instead of inline values.
</ParamField>

Creating a connection triggers the owning plugin's `resolveTools` hook and persists the produced tools per connection.

### Tools

| Method | Purpose |
| --- | --- |
| `list(filter?)` | Return persisted, addressable tools. |
| `schema(address)` | Return `ToolSchemaView` with JSON Schema roots, shared definitions, and optional TypeScript preview strings. |

`ToolListFilter` fields:

| Field | Default | Behavior |
| --- | --- | --- |
| `integration` | — | Restrict to one integration slug |
| `owner` | — | Restrict to `org` or `user` |
| `connection` | — | Restrict to one connection name |
| `query` | — | Case-insensitive substring match on `name` or `description` |
| `includeAnnotations` | `true` | Attach plugin-derived annotations |
| `includeBlocked` | `false` | Include tools whose effective policy is `block` |

### `execute`

```typescript
const result = await executor.execute(
  "tools.inventory.org.default.listItems",
  { limit: 10 },
);
```

<ParamField body="address" type="ToolAddress" required>
Full callable address. See [Tool addresses](#tool-addresses).
</ParamField>

<ParamField body="args" type="unknown" required>
Arguments matching the tool's `inputSchema`. Omit or pass `{}` when no input is required.
</ParamField>

<ParamField body="options.onElicitation" type="OnElicitation" required={false}>
Override the executor-level elicitation handler for this call only.
</ParamField>

Invoke path (simplified):

```mermaid
sequenceDiagram
  participant App
  participant Executor
  participant Policy
  participant Plugin

  App->>Executor: execute(address, args)
  Executor->>Executor: parse address / lookup static tool
  Executor->>Policy: resolveEffectivePolicy
  alt action is block
    Executor-->>App: ToolBlockedError
  else action is require_approval or annotation
    Executor->>App: onElicitation (FormElicitation)
    App-->>Executor: accept / decline / cancel
  end
  Executor->>Executor: resolve connection credentials
  Executor->>Plugin: invokeTool / static handler
  Plugin-->>Executor: result
  Executor-->>App: unknown
```

`ExecuteError` union: `ToolNotFoundError`, `ToolInvocationError`, `ToolBlockedError`, `PluginNotLoadedError`, `NoHandlerError`, `ConnectionNotFoundError`, `CredentialProviderNotRegisteredError`, `CredentialResolutionError`, `ElicitationDeclinedError`, `StorageFailure`.

### Policies

| Method | Purpose |
| --- | --- |
| `list()` | All owner-scoped `tool_policy` rows |
| `create(input)` | Add a rule (`owner`, `pattern`, `action`, optional `position`) |
| `update(input)` | Patch an existing rule by `id` and `owner` |
| `remove(input)` | Delete a rule |
| `resolve(address)` | Compute `EffectivePolicy` for one tool address |

Policy actions: `approve`, `require_approval`, `block`. Each owner contributes its first matching rule by local position; the most restrictive matched action across owners wins, so a user preference cannot weaken an org guardrail.

Pure policy helpers (also exported for plugins and UI):

| Export | Purpose |
| --- | --- |
| `matchPattern(pattern, toolId)` | Test whether a pattern matches a tool id |
| `isValidPattern(pattern)` | Validate pattern grammar |
| `resolveToolPolicy` / `resolveEffectivePolicy` | Layered resolution with plugin default `requiresApproval` |
| `effectivePolicyFromSorted` | Resolve against a pre-sorted policy list |
| `rowToToolPolicy` | Map a DB row to `ToolPolicy` |
| `ToolPolicyActionSchema` | Effect Schema literal union for actions |

Pattern grammar (matched against `<integration>.<owner>.<connection>.<tool>` or the full static address):

| Form | Example | Matches |
| --- | --- | --- |
| Universal | `*` | Everything |
| Exact | `inventory.org.default.listItems` | One tool |
| Subtree | `inventory.org.*` | Prefix and anything deeper |
| Mid-segment wildcard | `inventory.*.*.listItems` | One segment per `*` |

### OAuth (`executor.oauth`)

Shared OAuth service used by hosts, plugins (`ctx.oauth`), and SDK consumers.

| Method | Purpose |
| --- | --- |
| `createClient(input)` | Register a static OAuth app |
| `registerDynamicClient(input)` | RFC 7591 dynamic client registration |
| `listClients()` | Metadata-only client summaries (no secrets) |
| `removeClient(owner, slug)` | Delete a registered app |
| `start(input)` | Begin a flow; returns `connected` or `redirect` with `authorizationUrl` |
| `complete(input)` | Exchange authorization code for a `Connection` |
| `cancel(state)` | Abort an in-flight session |
| `probe(input)` | Discover authorization-server metadata from a URL |

OAuth errors: `OAuthStartError`, `OAuthCompleteError`, `OAuthProbeError`, `OAuthRegisterDynamicError`, `OAuthSessionNotFoundError`.

### Providers (`executor.providers`)

| Method | Purpose |
| --- | --- |
| `list()` | Registered `ProviderKey` values |
| `items(key)` | Browse discoverable entries from a provider (for example 1Password items) |

`CredentialProvider` contract:

<ResponseField name="key" type="ProviderKey">
Provider identifier (for example `"default"`, `"1password"`, `"keychain"`).
</ResponseField>

<ResponseField name="writable" type="boolean">
When `false`, `set`/`delete` are skipped and `remove` only drops routing.
</ResponseField>

<ResponseField name="get" type="(id) => Effect<string | null>">
Resolve a value by opaque `ProviderItemId`.
</ResponseField>

<ResponseField name="set" type="(id, value) => Effect<void>" optional>
Write a value (writable providers only).
</ResponseField>

<ResponseField name="list" type="() => Effect<ProviderEntry[]>" optional>
Enumerate items for discovery UIs.
</ResponseField>

## Tool addresses

Callable tools use the five-segment form:

```text
tools.<integration>.<owner>.<connection>.<tool>
```

The `<tool>` segment is everything after the fourth dot, so dotted tool names (for example OpenAPI `aliases.deleteAlias`) address naturally as `tools.inventory.org.default.aliases.deleteAlias`.

Helpers:

| Function | Produces |
| --- | --- |
| `parseToolAddress(address)` | `ParsedToolAddress` or `null` |
| `connectionAddress(owner, integration, connection)` | `tools.<integration>.<owner>.<connection>` |
| `toolAddress(owner, integration, connection, tool)` | Full `ToolAddress` |

Static plugin tools (for example core-tools) use their fully qualified id as the address and bypass the five-segment parser.

## Typed IDs

Branded Effect Schema types. Construct with `X.make("…")`; at runtime they are plain strings.

| Export | Role |
| --- | --- |
| `Tenant` | Org/workspace partition |
| `Subject` | Acting member (required for `owner: "user"`) |
| `Owner` | `"org"` or `"user"` |
| `IntegrationSlug` | Integration catalog slug |
| `ConnectionName` | Connection name within `(integration, owner)` |
| `AuthTemplateSlug` | Auth method template slug |
| `NO_AUTH_TEMPLATE` | Sentinel `"none"` for credential-free integrations |
| `ProviderKey` / `ProviderItemId` | Credential backend routing |
| `ConnectionAddress` | `tools.<integration>.<owner>.<connection>` |
| `ToolAddress` / `ToolName` | Callable tool identity |
| `PolicyId` | Tool policy row id |
| `OAuthClientSlug` / `OAuthState` | OAuth app and flow correlation |
| `ElicitationId` | URL elicitation callback correlation |

## Plugin wiring

Plugins are registered at construction and expose two surfaces:

1. **Extension namespace** on the executor: `executor.<pluginId>.*` (typed via `PluginExtensions<TPlugins>`).
2. **Runtime hooks** the executor calls internally: `resolveTools`, `invokeTool`, `credentialProviders`, `staticTools`, `detect`, and optional HTTP API groups.

Author plugins with `definePlugin` from `@executor-js/sdk/core`. The plugin model is Effect-only (storage factories, `PluginCtx`, schema). Application embedders import prebuilt plugins (for example `@executor-js/plugin-openapi/promise`) rather than authoring plugins in Promise style.

`defineExecutorConfig` declares the plugin list for CLI and host runtimes:

```typescript
import { defineExecutorConfig } from "@executor-js/sdk/promise";
import { openApiPlugin } from "@executor-js/plugin-openapi/promise";

export default defineExecutorConfig({
  plugins: () => [openApiPlugin()],
});
```

`plugins` is always a factory so hosts can inject runtime deps (for example a config file sink keyed to the active scope cwd).

## Elicitation

When a tool needs user input mid-call, the invoke fiber suspends and the configured `onElicitation` handler decides the response.

| Request type | Fields | Use case |
| --- | --- | --- |
| `FormElicitation` | `message`, `requestedSchema` | Structured input or approval prompts |
| `UrlElicitation` | `message`, `url`, `elicitationId` | OAuth or browser approval pages |

<ResponseField name="action" type='"accept" | "decline" | "cancel"'>
Response action. Non-`accept` actions raise `ElicitationDeclinedError`.
</ResponseField>

<ResponseField name="content" type="Record<string, unknown>" optional>
Payload when `action` is `"accept"`.
</ResponseField>

`execute` enforces approval by sending a `FormElicitation` when the effective policy is `require_approval` or the tool annotation sets `requiresApproval`.

## Errors

Tagged `Schema.TaggedErrorClass` instances. Promise callers receive them as rejected values; Effect callers see them in the typed error channel.

| Error | When raised |
| --- | --- |
| `ToolNotFoundError` | Address not found; may include `suggestions` |
| `ToolInvocationError` | Handler or upstream failure (wraps `cause`) |
| `ToolBlockedError` | Matched `block` policy (`pattern` included) |
| `PluginNotLoadedError` | Tool's plugin not in this executor config |
| `NoHandlerError` | Plugin has no `invokeTool` handler |
| `IntegrationNotFoundError` | Unknown integration slug |
| `IntegrationAlreadyExistsError` | Add operation targets an existing slug (HTTP 409) |
| `IntegrationRemovalNotAllowedError` | Static plugin-declared integration |
| `ConnectionNotFoundError` | Missing `(owner, integration, name)` |
| `InvalidConnectionInputError` | Structurally invalid create input |
| `CredentialProviderNotRegisteredError` | Unknown `ProviderKey` |
| `CredentialResolutionError` | Provider returned nothing or OAuth refresh failed; may set `reauthRequired` |
| `ElicitationDeclinedError` | User declined or cancelled during elicitation |

Convenience unions: `ExecuteError` (invoke path), `ExecutorError` (broader SDK surface).

## Address and schema utilities

| Export | Purpose |
| --- | --- |
| `collectTables()` | Return executor-owned Fuma table definitions (host DB bring-up) |
| `buildToolTypeScriptPreview` | Generate TS preview strings for tool schemas |
| `ToolSchemaView` | Schema-side tool view from `tools.schema` |
| `IntegrationDetectionResult` | URL autodetect result from `integrations.detect` |
| `ToolResult` / `ToolFileSchema` | Typed discriminated union for tool outcomes |
| `defineExecutorConfig` | Typed CLI config declaration |

## Related pages

<CardGroup>
<Card title="Embed with the SDK" href="/embed-sdk">
End-to-end workflow: compose plugins, register integrations, create connections, list and invoke tools, and shut down.
</Card>
<Card title="SDK quickstart example" href="/sdk-quickstart-example">
Walkthrough of `examples/docs-sdk-quickstart` with in-memory credentials and OpenAPI `addSpec`.
</Card>
<Card title="Plugin catalog" href="/plugin-catalog">
Published protocol and provider plugins and how `examples/all-plugins` wires them.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, schema inspection, and invocation across surfaces.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credential model and OAuth minting.
</Card>
<Card title="Policies" href="/policies">
Per-tool allow, require-approval, and block actions with pattern matching.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
HTTP routes that mirror the SDK namespaces for hosted and daemon runtimes.
</Card>
</CardGroup>

---

## 20. Configuration reference

> Environment variables and runtime paths: `EXECUTOR_DATA_DIR`, `EXECUTOR_SCOPE_DIR`, `EXECUTOR_WEB_BASE_URL`, secret keys, bootstrap admin, ports, and client-specific overrides.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/20-configuration-reference.md
- Generated: 2026-06-23T19:27:20.992Z

### Source Files

- `apps/docs/hosted/docker.mdx`
- `apps/cli/src/daemon-state.ts`
- `apps/cli/src/local-server-manifest.ts`
- `apps/cli/src/service.ts`
- `packages/core/sdk/src/public-origin.ts`
- `apps/host-selfhost/.env.example`
- `RUNNING.md`

---
title: "Configuration reference"
description: "Environment variables and runtime paths: `EXECUTOR_DATA_DIR`, `EXECUTOR_SCOPE_DIR`, `EXECUTOR_WEB_BASE_URL`, secret keys, bootstrap admin, ports, and client-specific overrides."
---

Executor configuration is split across three layers: environment variables (runtime behavior), on-disk paths under a data directory (persistence and discovery), and per-workspace scope (integrations and plugins). Local CLI, desktop, self-hosted Docker, and Cloudflare Workers each read overlapping variables with different defaults. This page catalogs the variables and files that govern ports, auth, secrets, bootstrap, and client overrides.

```mermaid
flowchart TB
  subgraph env["Environment variables"]
    DATA["EXECUTOR_DATA_DIR"]
    SCOPE["EXECUTOR_SCOPE_DIR"]
    ORIGIN["EXECUTOR_WEB_BASE_URL"]
    SECRETS["BETTER_AUTH_SECRET / EXECUTOR_SECRET_KEY"]
  end
  subgraph disk["Data directory (default ~/.executor)"]
    DB["data.db"]
    CTRL["server-control/"]
    AUTH["auth.json"]
    MANIFEST["server.json"]
    PROFILES["server-connections.json"]
    DAEMON["daemon-*.json"]
  end
  subgraph scope["Scope directory (default cwd)"]
    JSONC["executor.jsonc"]
  end
  DATA --> disk
  SCOPE --> scope
  ORIGIN --> OAuth["OAuth / MCP / connect URLs"]
  SECRETS --> CTRL
```

## Data directory

`EXECUTOR_DATA_DIR` is the root for all local persistence. When unset, runtimes default to `~/.executor` (CLI, desktop, local daemon) or `.executor-selfhost` under the process working directory (self-host dev server).

<ParamField body="EXECUTOR_DATA_DIR" type="string">
Absolute path to the executor data root. Holds the SQLite database, server-control files, daemon records, and generated key files.
</ParamField>

### On-disk layout

:::files
~/.executor/                          # default when EXECUTOR_DATA_DIR is unset
├── data.db                           # SQLite store (local CLI / desktop)
├── secret.key                        # self-host: generated encryption key (mode 0600)
├── auth-secret.key                   # self-host: generated session secret (mode 0600)
├── server-connections.json           # CLI saved server profiles
├── server-control/
│   ├── auth.json                     # local bearer token (mode 0600)
│   ├── server.json                   # active server manifest (mode 0600)
│   └── startup.lock                  # startup mutex
├── daemon-localhost-4788.json        # per-host/port daemon record
├── daemon-active-localhost-<hash>.json  # scope-active daemon pointer
└── logs/
    ├── daemon.log                    # supervised service stdout
    └── daemon.error.log              # supervised service stderr
:::

| File | Purpose |
| --- | --- |
| `data.db` | SQLite database for integrations, connections, policies, and executions |
| `server-control/auth.json` | Stable bearer token for local single-user auth; minted on first boot |
| `server-control/server.json` | Live server manifest: origin, port, PID, bearer token copy, owner metadata |
| `server-connections.json` | Named CLI server profiles (`executor server profile`) |
| `daemon-*.json` | Daemon process records and scope pointers for auto-start |
| `secret.key` / `auth-secret.key` | Self-host generated keys when env secrets are unset |

<Note>
The bearer token in `auth.json` is the source of truth for local auth. `server.json` carries a copy for discovery; rotating auth requires updating both the file and any MCP client configs.
</Note>

## Scope directory

`EXECUTOR_SCOPE_DIR` pins the workspace Executor binds to. It controls tenant identity, plugin loading from `executor.jsonc`, and daemon scope isolation.

<ParamField body="EXECUTOR_SCOPE_DIR" type="string">
Absolute path to the workspace root. When unset, runtimes use `process.cwd()`. The OS supervised service defaults this to `EXECUTOR_DATA_DIR` when unset.
</ParamField>

Scope identity is derived as:

- With `EXECUTOR_SCOPE_DIR` set: `scope:<resolved-path>`
- Without it: `cwd:<resolved-cwd>`

Daemon pointers and start locks are keyed by this scope ID, so two terminals in different project folders can each run an isolated daemon against the same data directory.

Per-scope plugin config lives at `<EXECUTOR_SCOPE_DIR>/executor.jsonc` (or `<cwd>/executor.jsonc`). Static plugins from `executor.config.ts` win over duplicate entries in `executor.jsonc`.

## Public origin

`EXECUTOR_WEB_BASE_URL` pins the public URL Executor uses for server-side outbound links: OAuth `redirect_uri`, MCP OAuth metadata, connect/approval URLs, and email links. This value must come from a trusted operator or platform source, never from request `Host` headers.

Resolution order (via `resolvePublicOrigin`):

1. Explicit `EXECUTOR_WEB_BASE_URL`
2. Platform-injected origin (`RAILWAY_PUBLIC_DOMAIN`, `RENDER_EXTERNAL_URL`, `VERCEL_URL`, `FLY_APP_NAME`, `WEBSITE_HOSTNAME`, `CF_PAGES_URL`, and related vars)
3. Fallback `http://localhost:<port>` with a one-time startup warning (except in `development` / `test`)

<Warning>
Set `EXECUTOR_WEB_BASE_URL` to the exact URL you load in the browser (scheme, host, port). A mismatch causes browser logins to fail with an invalid-origin error. Behind TLS or a reverse proxy, use the public `https://` URL, not the internal bind address.
</Warning>

<ParamField body="EXECUTOR_WEB_BASE_URL" type="string">
Public base URL for absolute outbound links. Required behind domains, TLS, or proxies. The local CLI daemon sets this automatically from the resolved listen URL when not already defined.
</ParamField>

Cloudflare Workers use `VITE_PUBLIC_SITE_URL` for the same purpose. When unset on a Worker, the per-request origin drives links; a startup warning recommends pinning a canonical URL on production deployments.

## Ports and bind addresses

| Runtime | Default port | Bind address | Override |
| --- | --- | --- | --- |
| Local CLI daemon (`executor daemon run`) | `4788` | `127.0.0.1` (via `--hostname`) | `--port`, `--hostname` |
| OS supervised service (`executor service install`) | `4789` | `127.0.0.1` | baked into service unit at install time |
| Self-host Docker / production | `4788` | `0.0.0.0` | `PORT`, `EXECUTOR_HOST` |
| Local dev Vite (`apps/local`) | `5173` | Vite defaults | `PORT` |
| Self-host dev Vite | project-specific | `127.0.0.1` | Vite `--port` |

<ParamField body="PORT" type="integer">
HTTP listen port. Self-host reads `PORT` (default `4788`). The CLI daemon accepts `--port` (default `4788`).
</ParamField>

<ParamField body="EXECUTOR_HOST" type="string">
Bind address. Self-host Docker defaults to `0.0.0.0`. Local daemon and supervised service bind loopback (`127.0.0.1`).
</ParamField>

<ParamField body="EXECUTOR_PORT" type="integer">
Desktop sidecar listen port. `0` means allocate an ephemeral port.
</ParamField>

Clients discover the live port from `server-control/server.json`, not from hard-coded defaults. The supervised service uses port `4789` by default so existing desktop MCP configs that targeted `4789` keep resolving after `executor install`.

## Secret keys

Self-host and local runtimes manage two independent secrets plus a local bearer token.

### Session secret

<ParamField body="BETTER_AUTH_SECRET" type="string">
Better Auth session secret. Minimum 32 characters when set explicitly. Alias: `AUTH_SECRET`. When unset, self-host generates and persists a random secret to `<data-dir>/auth-secret.key`.
</ParamField>

Rotating this secret signs every user out.

### Encryption master key

<ParamField body="EXECUTOR_SECRET_KEY" type="string">
Master key for the encrypted secrets provider. When unset on self-host, a random key is generated and persisted to `<data-dir>/secret.key`. On Cloudflare Workers, this is **required** (set via `wrangler secret put EXECUTOR_SECRET_KEY`, minimum 16 characters).
</ParamField>

### Local bearer token

Local and desktop servers gate `/api`, `/mcp`, and approval endpoints with a single bearer token stored in `server-control/auth.json`. The token is minted on first boot and reused across restarts. Rotate it with `executor auth rotate`; MCP clients must pick up the new value.

Supervised daemons (`EXECUTOR_SUPERVISED=1`) load the token from `auth.json` at boot. The OS service unit never embeds the secret.

## Bootstrap admin (self-host)

Headless first-run setup pre-creates the admin account instead of the in-browser setup screen.

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_EMAIL" type="string" required>
Admin email. Must be set together with `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` for headless bootstrap.
</ParamField>

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD" type="string" required>
Admin password for headless bootstrap.
</ParamField>

<ParamField body="EXECUTOR_BOOTSTRAP_ADMIN_NAME" type="string">
Display name for the bootstrap admin. Default: `Admin`.
</ParamField>

<ParamField body="EXECUTOR_ORG_NAME" type="string">
Display name of the single organization. Default: `Default`.
</ParamField>

<ParamField body="EXECUTOR_ORG_SLUG" type="string">
URL slug for org-prefixed console paths (`/<slug>/policies`). Default: `default`. Must be 2-48 chars of `[a-z0-9-]` and cannot collide with reserved routes (`api`, `mcp`, `login`, etc.).
</ParamField>

## Sandbox network

<ParamField body="EXECUTOR_ALLOW_LOCAL_NETWORK" type="boolean">
When `true`, sandboxed QuickJS code may reach loopback and private network addresses. Default: `false`. Enable only when you trust the code being executed.
</ParamField>

Cloudflare Workers use `ALLOW_LOCAL_NETWORK` with the same semantics.

## CLI and client overrides

These variables configure how CLI tools, MCP clients, and desktop attach to a running instance.

### Server authentication

| Variable | Used by | Purpose |
| --- | --- | --- |
| `EXECUTOR_API_KEY` | CLI, MCP | Bearer API key for hosted Executor Cloud |
| `EXECUTOR_AUTH_TOKEN` | CLI, MCP, desktop | Bearer token for local or desktop servers |

`EXECUTOR_API_KEY` takes precedence over `EXECUTOR_AUTH_TOKEN` when both are set. Local servers publish their token in `server.json`, so env overrides are mainly for remote or scripted access.

### Client identity

| Variable | Values | Purpose |
| --- | --- | --- |
| `EXECUTOR_CLIENT` | `cli`, `desktop` | Stamped into `server.json` owner metadata and telemetry |
| `EXECUTOR_CLIENT_DIR` | path | Desktop web UI asset directory passed to the sidecar |
| `EXECUTOR_SUPERVISED` | `1` | Marks an OS-managed daemon; loads token from `auth.json`, reclaims stale `server.json` on boot |
| `EXECUTOR_SERVICE_VERSION` | semver | Installed CLI version baked into the service unit for drift detection |

### Plugin and native binding overrides

| Variable | Purpose |
| --- | --- |
| `EXECUTOR_KEYCHAIN_SERVICE_NAME` | Keychain / Credential Manager service name (default: `executor`) |
| `EXECUTOR_KEYRING_NATIVE_PATH` | Absolute path to the `@napi-rs/keyring` `.node` binding in compiled binaries |
| `EXECUTOR_LIBSQL_NATIVE_PATH` | Absolute path to the libSQL native binding in compiled binaries |
| `EXECUTOR_BIN_PATH` | Override which `executor` binary the CLI launcher spawns |
| `EXECUTOR_DESKTOP_SETTINGS_DIR` | Isolated desktop settings directory (tests) |

### Observability (optional)

Passed through to supervised service units when set at install time:

| Variable | Purpose |
| --- | --- |
| `EXECUTOR_SENTRY_DSN` | Sentry crash reporting DSN |
| `EXECUTOR_SENTRY_RELEASE` | Sentry release tag |
| `EXECUTOR_SENTRY_ENVIRONMENT` | Sentry environment (default: `production`) |
| `EXECUTOR_RUN_ID` | Correlates events across desktop main and sidecar processes |
| `EXECUTOR_MCP_DEBUG` | Set to `true` for verbose MCP session logging |

## Self-host environment reference

All self-host variables are optional; a bare `docker compose up` boots a working instance.

| Variable | Default | Purpose |
| --- | --- | --- |
| `PORT` | `4788` | HTTP listen port |
| `EXECUTOR_HOST` | `0.0.0.0` (Docker), `127.0.0.1` (dev) | Bind address |
| `EXECUTOR_DATA_DIR` | `/data` (Docker), `.executor-selfhost` (dev) | Data root |
| `EXECUTOR_DB_PATH` | `<data-dir>/data.db` | SQLite file path |
| `EXECUTOR_WEB_BASE_URL` | platform-detected or `http://localhost:<port>` | Public browser URL |
| `BETTER_AUTH_SECRET` | generated in data dir | Session secret |
| `EXECUTOR_SECRET_KEY` | generated in data dir | Encryption master key |
| `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` | unset | Headless admin email |
| `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` | unset | Headless admin password |
| `EXECUTOR_BOOTSTRAP_ADMIN_NAME` | `Admin` | Admin display name |
| `EXECUTOR_ORG_NAME` | `Default` | Organization display name |
| `EXECUTOR_ORG_SLUG` | `default` | Organization URL slug |
| `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Sandbox loopback/private access |

<CodeGroup>

```bash Docker run
docker run -d \
  --name executor-selfhost \
  -p 4788:4788 \
  -v executor-data:/data \
  -e EXECUTOR_WEB_BASE_URL=https://executor.example.com \
  -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \
  -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD=change-me-strong \
  ghcr.io/rhyssullivan/executor-selfhost:latest
```

```bash Local dev
export EXECUTOR_DATA_DIR=/tmp/my-executor
export EXECUTOR_WEB_BASE_URL=http://localhost:4788
export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
cd apps/host-selfhost && bun run dev
```

</CodeGroup>

## Cloudflare Workers environment

Cloudflare hosts receive configuration through Worker bindings and `wrangler.jsonc` vars, not `process.env`.

| Binding / variable | Required | Purpose |
| --- | --- | --- |
| `EXECUTOR_SECRET_KEY` | yes (secret) | At-rest secret encryption key |
| `ACCESS_TEAM_DOMAIN` | yes | Cloudflare Access team domain |
| `ACCESS_AUD` | yes | Access application AUD tag |
| `ADMIN_EMAILS` | no | Comma-separated admin emails |
| `SELF_HOSTED_ORG_ID` | no | Organization ID (default: `default`) |
| `SELF_HOSTED_ORG_NAME` | no | Organization name (default: `Default`) |
| `SELF_HOSTED_ORG_SLUG` | no | Organization URL slug |
| `VITE_PUBLIC_SITE_URL` | no | Pinned public origin |
| `ALLOW_LOCAL_NETWORK` | no | Sandbox local network access |
| `ENABLE_DEV_AUTH` | no | Skip Access for local `wrangler dev` only |

<Warning>
Never set `ENABLE_DEV_AUTH=true` on a deployment that is not already behind Cloudflare Access. It treats every request as a fixed admin and leaves the instance wide open.
</Warning>

## Supervised service environment

`executor service install` bakes these into the launchd plist, systemd unit, or Windows scheduled task:

| Variable | Value at install | Purpose |
| --- | --- | --- |
| `EXECUTOR_SUPERVISED` | `1` | Supervised daemon mode |
| `EXECUTOR_DATA_DIR` | resolved data dir | Pinned data root |
| `EXECUTOR_SCOPE_DIR` | `EXECUTOR_SCOPE_DIR` env or data dir | Pinned workspace scope |
| `EXECUTOR_SERVICE_VERSION` | installing CLI version | Drift detection after upgrades |
| `PATH` | installer's PATH | Tool discovery for integrations |

The service also passes through `EXECUTOR_CLIENT`, `EXECUTOR_SENTRY_*`, and `EXECUTOR_RUN_ID` when present at install time.

## Verify configuration

<Steps>
  <Step title="Check the data directory">
    Confirm which data root is active:

    ```bash
    ls -la ~/.executor/server-control/
    ```

    Expect `auth.json` and, when a server is running, `server.json` (both mode `0600`).
  </Step>
  <Step title="Read the active server manifest">
    ```bash
    cat ~/.executor/server-control/server.json
    ```

    Note `connection.origin` (live URL), `dataDir`, `scopeDir`, and `owner.client`.
  </Step>
  <Step title="Probe health">
    ```bash
    curl -s http://127.0.0.1:4788/api/health
    ```

    Replace the host and port with values from `server.json` if they differ.
  </Step>
  <Step title="Check supervised service status">
    ```bash
    executor service status
    ```

    Reports registration, running state, PID (macOS), and lingering status (Linux).
  </Step>
</Steps>

## Development-only variables

These appear in contributor workflows and e2e harnesses, not production deployments.

| Variable | Purpose |
| --- | --- |
| `EXECUTOR_DEV_VITE_PORT` | HMR client port when the CLI daemon proxies to a child Vite server |
| `EXECUTOR_VERSION` | CLI build version override |
| `EXECUTOR_PREVIEW_CDN_URL` / `EXECUTOR_PREVIEW_SHA` | Preview CLI build CDN pinning |
| `EXECUTOR_PREVIEW_TARGETS` | Comma-separated preview build targets |
| `E2E_*_PORT` / `E2E_*_URL` | E2E harness port pinning or attach-to-running |
| `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` | Allow Vite dev server access from tailnet hostnames |

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Install the CLI, bootstrap a checkout, and verify ports and data directories.
  </Card>
  <Card title="Deploy self-hosted" href="/deploy-selfhost">
    Docker image defaults, volume mounts, bootstrap admin, and TLS requirements.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `executor install`, `daemon`, `service`, `web`, and port flags.
  </Card>
  <Card title="Configure credentials" href="/configure-credentials">
    Credential providers, OAuth flows, and placement-based auth templates.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Port conflicts, stale manifests, OAuth origin mismatches, and recovery commands.
  </Card>
</CardGroup>

---

## 21. SDK quickstart example

> Walkthrough of `examples/docs-sdk-quickstart`: in-memory credential provider, OpenAPI `addSpec`, connection creation, tool listing, schema inspection, and shutdown.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/21-sdk-quickstart-example.md
- Generated: 2026-06-23T19:28:11.669Z

### Source Files

- `examples/docs-sdk-quickstart/src/main.ts`
- `examples/docs-sdk-quickstart/package.json`
- `scripts/generate-doc-snippets.ts`
- `packages/plugins/openapi/src/promise.ts`
- `packages/core/sdk/src/promise.ts`
- `packages/core/sdk/src/connection.ts`

---
title: "SDK quickstart example"
description: "Walkthrough of `examples/docs-sdk-quickstart`: in-memory credential provider, OpenAPI `addSpec`, connection creation, tool listing, schema inspection, and shutdown."
---

`examples/docs-sdk-quickstart` is a single-file Promise-mode script that registers an inline OpenAPI integration, stores a credential in a writable in-memory provider, materializes per-connection tools, and reads the resulting catalog. The file is also the source of truth for generated docs snippets under `docs/snippets/sdk/quickstart/`.

## What the example demonstrates

The script exercises the minimum embed path without the CLI daemon, HTTP server, or MCP host:

| Phase | API surface | Outcome |
| --- | --- | --- |
| Bootstrap | `createExecutor` | In-process executor with OpenAPI plugin and writable credential store |
| Register integration | `executor.openapi.addSpec` | Tenant-level `inventory` integration from an inline spec blob |
| Bind credential | `executor.connections.create` | Owner-scoped connection that persists the API key and produces tools |
| Discover tools | `executor.tools.list` | Addressable tool rows for the connection |
| Inspect schema | `executor.tools.schema` | JSON Schema roots and optional TypeScript preview strings |
| Teardown | `executor.close` | Plugin and database cleanup |

Tools are persisted per connection when `connections.create` runs. `tools.list` is a read of that catalog, not a live re-parse of the OpenAPI document on every call.

## Example layout

:::files
examples/docs-sdk-quickstart/
├── package.json          # workspace deps and `bun run start`
├── src/
│   └── main.ts           # runnable example + docs:start/docs:end snippet markers
└── tsconfig.json
:::

The example depends on `@executor-js/sdk`, `@executor-js/plugin-openapi`, and `effect`. It imports Promise-mode entry points from `@executor-js/sdk/promise` and `@executor-js/plugin-openapi/promise`.

## Prerequisites

<Steps>
<Step title="Bootstrap the monorepo">

From the repository root, install workspace packages:

```bash
bun run bootstrap
```

Fresh checkouts need this before workspace imports such as `@executor-js/sdk/promise` resolve.

</Step>
<Step title="Confirm Bun is available">

The example runs with Bun (`"start": "bun run src/main.ts"`). Node is not wired in this package's scripts.

</Step>
</Steps>

## Run the example

```bash
cd examples/docs-sdk-quickstart
bun run start
```

<ResponseExample>

```text
tools.inventory.org.default.listItems: List inventory items
tools.inventory.org.default.getItem: Get an inventory item
No input required
```

</ResponseExample>

The first two lines come from `tools.list`. The third line is `schema?.inputTypeScript` for the first listed tool; `listItems` has no required input, so the fallback `"No input required"` prints when `inputTypeScript` is absent.

<Note>
Tool list order is not guaranteed. The schema line always reflects `tools[0]`, whichever operation the store returns first.
</Note>

## Architecture

```mermaid
sequenceDiagram
  participant App as main.ts
  participant Exec as createExecutor
  participant OAI as openApiPlugin
  participant Prov as memoryProvider
  participant DB as executor store

  App->>Exec: plugins, providers, onElicitation
  Exec->>OAI: register extension
  Exec->>Prov: register writable store
  App->>OAI: addSpec(slug: inventory)
  OAI->>DB: persist integration + auth template
  App->>Exec: connections.create(value)
  Exec->>Prov: set(credential id, api key)
  OAI->>DB: persist tools per connection
  App->>Exec: tools.list / tools.schema
  Exec->>DB: read tool rows
  App->>Exec: close()
```

Three identities matter:

- **Integration** (`inventory`): the API surface and auth template. Registered by `addSpec`.
- **Connection** (`org` / `default`): the saved credential for one integration and one auth method. Identity is `(owner, integration, name)`.
- **Tool**: a persisted, addressable operation owned by a connection. Address shape is `tools.<integration>.<owner>.<connection>.<operationId>`.

## Step 1: Create the executor and in-memory provider

A writable credential provider is required before `connections.create({ value })` can store an inline secret. The example uses a `Map`-backed provider whose `get` and `set` methods return `Effect.sync` values. Production hosts swap this for durable providers (keychain, 1Password, encrypted stores).

<CodeGroup>

```ts title="examples/docs-sdk-quickstart/src/main.ts"
import { Effect } from "effect";
import {
  createExecutor,
  ProviderItemId,
  ProviderKey,
  type CredentialProvider,
} from "@executor-js/sdk/promise";
import { openApiPlugin, variable } from "@executor-js/plugin-openapi/promise";

const memory = new Map<string, string>();
const memoryProvider: CredentialProvider = {
  key: ProviderKey.make("memory"),
  writable: true,
  get: (id: ProviderItemId) => Effect.sync(() => memory.get(String(id)) ?? null),
  set: (id: ProviderItemId, value: string) =>
    Effect.sync(() => {
      memory.set(String(id), value);
    }),
};

const executor = await createExecutor({
  plugins: [openApiPlugin()],
  providers: [memoryProvider],
  onElicitation: "accept-all",
});
```

</CodeGroup>

<ParamField body="plugins" type="AnyPlugin[]">
Protocol plugins to load. Here, `openApiPlugin()` exposes `executor.openapi.*`.
</ParamField>

<ParamField body="providers" type="CredentialProvider[]" required>
Writable providers registered at construction. Config providers register first; the first writable provider becomes the default store for inline `value` credentials.
</ParamField>

<ParamField body="onElicitation" type='"accept-all" | handler' required>
How to answer mid-invocation elicitation prompts. `"accept-all"` is appropriate for scripts and tests.
</ParamField>

## Step 2: Register the OpenAPI integration

The script embeds a minimal Inventory API spec (operations `listItems` and `getItem`) and passes it as a blob. `addSpec` registers the integration, stores the spec pointer, and records the auth template.

<CodeGroup>

```ts title="addSpec call"
await executor.openapi.addSpec({
  slug: "inventory",
  description: "Inventory API",
  baseUrl: "https://inventory.example.com",
  spec: {
    kind: "blob",
    value: JSON.stringify(inventoryApi),
  },
  authenticationTemplate: [
    {
      slug: "apiKey",
      type: "apiKey",
      headers: { "X-API-Key": [variable("token")] },
    },
  ],
});
```

</CodeGroup>

<ParamField body="slug" type="string" required>
Integration catalog slug. Becomes the `<integration>` segment in tool addresses.
</ParamField>

<ParamField body="spec" type="OpenApiSpecInput" required>
Spec source. The example uses `{ kind: "blob", value: string }`. URL and file inputs are also supported by the plugin.
</ParamField>

<ParamField body="authenticationTemplate" type="AuthenticationInput[]">
Declares where connection credentials render on outbound requests. `variable("token")` is the slot the resolved credential fills. Omit to derive methods from the spec's `securitySchemes`; pass `[]` to declare no auth.
</ParamField>

<ResponseField name="slug" type="IntegrationSlug">
The registered integration slug.
</ResponseField>

<ResponseField name="toolCount" type="number">
Operation count extracted from the spec. The inventory example yields `2`.
</ResponseField>

<Warning>
Re-adding an existing slug throws `IntegrationAlreadyExistsError`. Use `updateSpec` to refresh an integration in place.
</Warning>

## Step 3: Create a connection

Connections bind a credential to one integration and one auth template. Inline `value` writes through the default writable provider and triggers tool materialization for that connection.

<CodeGroup>

```ts title="connections.create"
await executor.connections.create({
  owner: "org",
  name: "default",
  integration: "inventory",
  template: "apiKey",
  value: "inventory-api-key",
});
```

</CodeGroup>

<ParamField body="owner" type="Owner" required>
Scope owner. `"org"` rows file under the executor tenant (default `"default-tenant"`).
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Connection name within `(owner, integration)`.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Must match the slug passed to `addSpec`.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
Which auth method from `authenticationTemplate` applies this credential.
</ParamField>

<ParamField body="value" type="string">
Sugar for a single `token` input. Written to the default writable provider. Alternative shapes include `from` (external provider reference), `values`, and `inputs`.
</ParamField>

The returned `Connection` includes `provider` (here `"memory"`), `address` (`tools.inventory.org.default`), and metadata fields. Credentials are applied lazily per invocation through the template, not pre-baked into stored tool definitions.

## Step 4: List tools

Filter the catalog to one integration:

<CodeGroup>

```ts title="tools.list"
const tools = await executor.tools.list({ integration: "inventory" });

for (const tool of tools) {
  console.log(`${tool.address}: ${tool.description}`);
}
```

</CodeGroup>

Each `Tool` row includes:

| Field | Example value |
| --- | --- |
| `address` | `tools.inventory.org.default.listItems` |
| `owner` | `org` |
| `integration` | `inventory` |
| `connection` | `default` |
| `name` | `listItems` |
| `pluginId` | `openapi` |
| `description` | `List inventory items` |

Optional `ToolListFilter` fields include `owner`, `connection`, `query`, `includeAnnotations`, and `includeBlocked` (defaults to omitting blocked tools).

## Step 5: Inspect a tool schema

`tools.schema` returns a `ToolSchemaView` with JSON Schema roots, shared `schemaDefinitions`, and optional TypeScript preview strings. List rows carry lightweight schema hints; this call is the schema-bearing surface.

<CodeGroup>

```ts title="tools.schema"
const firstAddress = tools[0]?.address;
const schema = firstAddress ? await executor.tools.schema(firstAddress) : null;

console.log(schema?.inputTypeScript ?? "No input required");
```

</CodeGroup>

<ResponseField name="inputSchema" type="object">
JSON Schema root for tool input.
</ResponseField>

<ResponseField name="outputSchema" type="object">
JSON Schema root for tool output when available.
</ResponseField>

<ResponseField name="inputTypeScript" type="string">
Generated TypeScript type preview for the input shape. Useful for agent prompts and codegen.
</ResponseField>

<ResponseField name="schemaDefinitions" type="Record<string, unknown>">
Shared component definitions reachable from the input/output roots.
</ResponseField>

For `getItem`, expect an input shape that includes the `id` path parameter. For `listItems`, `inputTypeScript` is typically absent.

## Step 6: Shut down

Always close embedded executors so plugins and any configured database factory can release resources:

```ts
await executor.close();
```

The Promise-mode `close` wraps the Effect executor's teardown, including per-plugin `close` hooks and optional `db.close`.

## Docs snippet generation

`main.ts` is annotated with `// docs:start <name>` and `// docs:end <name>` markers. Editing those blocks and running the root script regenerates MDX snippet files:

```bash
bun run docs:snippets
```

Output lands in `docs/snippets/sdk/quickstart/` (one `.mdx` file per marker name: `create-executor`, `add-integration`, `create-connection`, `list-tools`, `inspect-schema`, `close-executor`). The generator dedents captured lines and prefixes each file with a generated-file notice.

<Tip>
Change snippet content in `examples/docs-sdk-quickstart/src/main.ts`, not in the generated `docs/snippets/` tree.
</Tip>

## Common failure modes

<AccordionGroup>
<Accordion title="Cannot find module '@executor-js/sdk/promise'">

Run `bun run bootstrap` from the repository root so workspace packages link. The example uses monorepo `workspace:*` dependencies, not standalone npm installs.

</Accordion>

<Accordion title="connections.create rejects inline value">

`createExecutor` needs at least one writable provider in `providers` (or from a plugin's `credentialProviders`). Without one, inline `value` credentials cannot be stored.

</Accordion>

<Accordion title="tools.list returns an empty array">

Confirm `connections.create` ran for the same `integration` slug as `addSpec`, and that the connection's `template` matches a slug in `authenticationTemplate`. Tools materialize at connection creation, not at `addSpec`.

</Accordion>

<Accordion title="IntegrationAlreadyExistsError on second run">

`addSpec` blocks duplicate slugs. Use a fresh in-memory database (default for scripts without a custom `db` factory) or call `executor.openapi.removeSpec("inventory")` before re-adding.

</Accordion>
</AccordionGroup>

## Beyond this example

This script stops before `tools.invoke`. Invocation uses the same addresses and applies the connection credential through the auth template on each outbound OpenAPI request. For multi-plugin setups, OAuth flows, and production credential backends, see the broader embed and configuration docs.

## Related pages

<CardGroup>
<Card title="Embed with the SDK" href="/embed-sdk">
Compose `createExecutor` in application code with plugins, providers, integrations, connections, and tool invocation.
</Card>
<Card title="SDK reference" href="/sdk-reference">
Promise and Effect entry points, typed IDs, error tags, and executor surface area.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credential model, provider resolution, and connection identity.
</Card>
<Card title="Integrations" href="/integrations">
Tenant-level catalog identities and auth method descriptors.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Durable credential providers, OAuth minting, and placement-based auth templates.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery filters, schema inspection, and invocation paths.
</Card>
<Card title="Plugin catalog" href="/plugin-catalog">
Published protocol and provider plugins beyond OpenAPI.
</Card>
<Card title="Develop locally" href="/develop-locally">
Monorepo bootstrap, targeted tests, and contributor workflows.
</Card>
</CardGroup>

---

## 22. Plugin catalog

> Published protocol and provider plugins (OpenAPI, GraphQL, MCP, file-secrets, keychain, 1Password, Google, WorkOS Vault) and how `examples/all-plugins` wires them together.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/22-plugin-catalog.md
- Generated: 2026-06-23T19:28:31.468Z

### Source Files

- `examples/all-plugins/src/main.ts`
- `examples/all-plugins/package.json`
- `packages/plugins/openapi/package.json`
- `packages/plugins/graphql/package.json`
- `packages/plugins/mcp/package.json`
- `packages/plugins/file-secrets/package.json`
- `packages/core/integrations-registry/package.json`

---
title: "Plugin catalog"
description: "Published protocol and provider plugins (OpenAPI, GraphQL, MCP, file-secrets, keychain, 1Password, Google, WorkOS Vault) and how `examples/all-plugins` wires them together."
---

Executor extends through npm packages under `packages/plugins/*`. Each plugin is a `definePlugin` factory: protocol plugins register integrations and produce per-connection tools; credential plugins register `CredentialProvider` backends that store connection values. Host apps declare the plugin list in `executor.config.ts` via `defineExecutorConfig`; SDK embedders pass the same factories to `createExecutor({ plugins })`. The `examples/all-plugins` example composes every published plugin into one executor and walks integration registration, connection creation, tool listing, execution, and shutdown.

## Plugin roles

| Role | Plugins | What they contribute |
| --- | --- | --- |
| Protocol / integration | `@executor-js/plugin-openapi`, `@executor-js/plugin-graphql`, `@executor-js/plugin-mcp`, `@executor-js/plugin-google` | Integration catalog entries, per-connection tool production, `invokeTool`, auth template descriptors |
| Credential provider | `@executor-js/plugin-file-secrets`, `@executor-js/plugin-keychain`, `@executor-js/plugin-onepassword`, `@executor-js/plugin-workos-vault` | Writable (or read-only) `CredentialProvider` backends; connection values resolve through these at invoke time |

<Note>
In v2, a connection **is** the credential. There is no separate secret store. The connection row references a `ProviderKey` and opaque `ProviderItemId`; the provider plugin reads and writes the value.
</Note>

## Published packages

| Package | Plugin ID | Provider key | Primary extension methods |
| --- | --- | --- | --- |
| `@executor-js/plugin-openapi` | `openapi` | — | `previewSpec`, `addSpec`, `updateSpec`, `configure`, `configureAuth` |
| `@executor-js/plugin-graphql` | `graphql` | — | `addIntegration`, `configure`, `configureAuth`, `probeEndpoint` |
| `@executor-js/plugin-mcp` | `mcp` | — | `addServer`, `probeEndpoint`, `configureAuth`, `getServer` |
| `@executor-js/plugin-google` | `google` | — | `addBundle`, `updateBundle`, `removeBundle`, `configure` |
| `@executor-js/plugin-file-secrets` | `fileSecrets` | `file` | `filePath` (auth file location) |
| `@executor-js/plugin-keychain` | `keychain` | `keychain` | `isSupported`, `displayName`, `has` |
| `@executor-js/plugin-onepassword` | `onepassword` | `onepassword` | `configure`, `status`, `getConfig`, `listVaults`, `resolveSecret` |
| `@executor-js/plugin-workos-vault` | `workosVault` | `workos-vault` | `providerKey` |

Protocol plugins also expose `integrationPresets` (OpenAPI ships Stripe, GitHub REST, Vercel, Cloudflare, and others; Google ships a discovery-bundle preset). Credential plugins auto-register at `createExecutor` startup via `plugin.credentialProviders`; inline `config.providers` register first and win as the default writable store.

## SDK plugin vs HTTP plugin

Protocol plugins ship two entry points:

| Import path | Use when |
| --- | --- |
| `@executor-js/plugin-openapi` (and siblings) | SDK embedders, scripts, tests |
| `@executor-js/plugin-openapi/api` | Host runtimes that mount HTTP routes (`routes`, `handlers`, `extensionService`) |

The HTTP variant spreads the SDK plugin and adds Effect HTTP API groups. SDK-only consumers never load `@executor-js/api`. Credential plugins (`keychain`, `file-secrets`) expose only the root and `./promise` subpaths; they have no HTTP surface.

```text
  executor.config.ts                    createExecutor({ plugins })
         |                                        |
         v                                        v
  *HttpPlugin() from ./api              SDK plugin factories
  (local, cloud, self-host)             (examples/all-plugins)
         |                                        |
         +------------------+---------------------+
                            v
                   definePlugin factory
                            |
            +---------------+---------------+
            |               |               |
       extension      credentialProviders   storage
     (executor.openapi)  (auto-register)   (plugin blobs)
```

## Plugin contract

Every plugin factory returns an object shaped by `definePlugin`:

<ParamField body="id" type="string" required>
Stable plugin identifier. Becomes `executor[id]` on the merged executor surface (for example `executor.openapi`, `executor.mcp`).
</ParamField>

<ParamField body="extension" type="(ctx) => TExtension">
Plugin-specific API surfaced on the executor. Integration plugins register catalog entries here; credential plugins expose configuration helpers.
</ParamField>

<ParamField body="credentialProviders" type="CredentialProvider[] | Effect">
Optional. Registered at startup. Keychain probes write capability before registering; WorkOS Vault requires `credentials` or a pre-built `client`.
</ParamField>

<ParamField body="storage" type="(deps) => TStore">
Optional typed store backed by host-owned blob and plugin storage facades.
</ParamField>

<ParamField body="routes / handlers / extensionService" type="HTTP API">
Present only on `*HttpPlugin` variants for host mounting.
</ParamField>

<ParamField body="close" type="() => Effect">
Optional lifecycle hook. MCP tears down connection pools on shutdown.
</ParamField>

After `createExecutor`, each plugin extension is reachable as `executor[pluginId]`. TypeScript preserves the merged tuple type when plugins are passed `as const`.

## Runtime plugin lists

Hosts pick plugins for their deployment context:

<Tabs>
<Tab title="Local (`apps/local`)">

```ts
openApiHttpPlugin(),
googleHttpPlugin(),
microsoftHttpPlugin(),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: true }),
graphqlHttpPlugin(),
keychainPlugin(),
fileSecretsPlugin(),
onepasswordHttpPlugin(),
desktopSettingsPlugin({ webBaseUrl }),
```

Stdio MCP is enabled for trusted single-user local use. Keychain, file-secrets, and 1Password back local credential storage.

</Tab>
<Tab title="Cloud (`apps/cloud`)">

```ts
openApiHttpPlugin(),
googleHttpPlugin(),
microsoftHttpPlugin(),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
graphqlHttpPlugin(),
workosVaultPlugin({ credentials: workosCredentials }),
```

Multi-tenant cloud omits stdio MCP and OS-local secret backends. WorkOS Vault is the cloud credential provider.

</Tab>
<Tab title="SDK example (`examples/all-plugins`)">

```ts
keychainPlugin(),
fileSecretsPlugin(),
onepasswordPlugin(),
googlePlugin(),
graphqlPlugin(),
mcpPlugin({ dangerouslyAllowStdioMCP: false }),
openApiPlugin(),
// workosVaultPlugin({ credentials: { apiKey, clientId } }),
```

Uses SDK factories (not HTTP variants) plus an inline in-memory provider for script-friendly credential writes.

</Tab>
</Tabs>

## `examples/all-plugins` wiring

The example (`@executor-js/example-all-plugins`) demonstrates the v2 SDK bootstrap shape without HTTP or tenant persistence:

<Steps>
<Step title="Install dependencies and run">

```bash
cd examples/all-plugins
bun run start
```

Runs `src/main.ts` against the SDK ephemeral in-memory FumaDB backend.

</Step>

<Step title="Compose plugins and providers">

```ts
const executor = yield* createExecutor({
  tenant: Tenant.make("example-tenant"),
  plugins: [
    keychainPlugin(),
    fileSecretsPlugin(),
    onepasswordPlugin(),
    googlePlugin(),
    graphqlPlugin(),
    mcpPlugin({ dangerouslyAllowStdioMCP: false }),
    openApiPlugin(),
  ],
  providers: [memoryProvider],
  onElicitation: "accept-all",
});
```

`providers` registers the in-memory store first so inline connection `value` writes land there by default.

</Step>

<Step title="Register integrations and connections">

OpenAPI: `executor.openapi.addSpec({ spec, slug, baseUrl, authenticationTemplate })`, then `executor.connections.create({ owner, name, integration, template, value })`.

GraphQL: `executor.graphql.addIntegration({ endpoint, slug, introspectionJson })`, then a `none`-template connection with an empty value.

</Step>

<Step title="List, execute, and shut down">

```ts
yield* executor.tools.list({ integration });
yield* executor.execute(toolAddress, {});
yield* executor.close();
```

OpenAPI and GraphQL flows run end-to-end. Keychain, 1Password, MCP, Google, and WorkOS Vault extensions are wired but skipped unless external infra is available.

</Step>
</Steps>

## Protocol plugins

### OpenAPI (`@executor-js/plugin-openapi`)

Turns OpenAPI 3.x specs into integration catalog entries. Each operation becomes a tool; tool addresses follow `tools.<integration>.<owner>.<connection>.<operationId>`.

| Method | Purpose |
| --- | --- |
| `previewSpec(spec)` | Inspect servers, auth schemes, operation count before adding |
| `addSpec(config)` | Register integration; `spec` accepts `{ kind: "blob", value }` or `{ kind: "url", url }` |
| `updateSpec(slug, input?)` | Refresh spec in place; returns added/removed tool names |
| `configure` / `configureAuth` | Merge or replace `authenticationTemplate` entries |

Auth templates use placement-based descriptors from `@executor-js/sdk/http-auth` (headers, query, bearer, OAuth2). Annotations (for example `requiresApproval` on POST/DELETE) resolve at read time via `resolveAnnotations`.

### GraphQL (`@executor-js/plugin-graphql`)

Registers GraphQL endpoints by introspection (live or canned `introspectionJson`). Query fields map to tools; mutations default to requiring approval.

| Method | Purpose |
| --- | --- |
| `addIntegration({ endpoint, slug, introspectionJson, authenticationTemplate })` | Register catalog entry and materialize tools |
| `configure` / `configureAuth` | Update endpoint metadata or merge auth methods |
| `probeEndpoint` | Pre-flight connectivity and schema discovery |

### MCP (`@executor-js/plugin-mcp`)

Proxies upstream MCP servers into the unified tool catalog. Remote transport uses HTTP; stdio spawns a local subprocess.

| Transport | Input shape | Constraint |
| --- | --- | --- |
| `remote` | `{ endpoint, remoteTransport?, headers?, authenticationTemplate? }` | Safe for multi-tenant hosts |
| `stdio` | `{ command, args?, env?, cwd? }` | Requires `dangerouslyAllowStdioMCP: true` |

`addServer` registers the integration; `probeEndpoint` checks connectivity, OAuth requirements, and tool count before add. MCP tool annotations carry an opaque `mcp` envelope with the upstream tool name for invoke dispatch.

<Warning>
Stdio MCP inherits `process.env` and spawns subprocesses. Enable only in trusted single-user contexts (local daemon). Cloud and self-host configs set `dangerouslyAllowStdioMCP: false`.
</Warning>

### Google (`@executor-js/plugin-google`)

Wraps the OpenAPI plugin backing. Fetches Google API Discovery documents, converts them to OpenAPI, and registers a bundled integration.

| Method | Purpose |
| --- | --- |
| `addBundle({ urls, slug?, name?, baseUrl? })` | Fetch discovery docs and register tools |
| `updateBundle(slug, { urls? })` | Refresh discovery bundle |
| `configure(slug, { authenticationTemplate, mode? })` | Merge or replace auth methods |

Depends on `@executor-js/plugin-openapi` internally for compilation and invocation.

## Credential provider plugins

### File secrets (`@executor-js/plugin-file-secrets`)

Stores connection values in a flat JSON file (`auth.json`) under the XDG data directory (default `~/.local/share/executor/auth.json`). File permissions are restricted (`0o600` file, `0o700` directory). Override the directory with `fileSecretsPlugin({ directory })`.

Provider key: `file`. Extension exposes `executor.fileSecrets.filePath`.

### Keychain (`@executor-js/plugin-keychain`)

Uses `@napi-rs/keyring` for OS keychain storage (macOS Keychain, Windows Credential Manager, Linux Secret Service). Probes write capability at startup; skips registration when the keychain is unreachable (headless CI, WSL without secret-service).

Provider key: `keychain`. Service name defaults to `"executor"`; override with `keychainPlugin({ serviceName })`.

### 1Password (`@executor-js/plugin-onepassword`)

Resolves secrets from 1Password vaults via desktop-app or service-account auth. Configuration is owner-partitioned blob storage. Exposes executor static tools (`status`, `configure`, `listVaults`, `getConfig`) for setup without leaking secret values in tool output.

Provider key: `onepassword`. Configure with `executor.onepassword.configure({ auth, vaultId, name })` before connections can resolve 1Password-backed values.

### WorkOS Vault (`@executor-js/plugin-workos-vault`)

Cloud-hosted credential backend using the WorkOS Vault API. Requires `workosVaultPlugin({ credentials: { apiKey, clientId } })` or a pre-built `client` for tests.

Provider key: `workos-vault`. Used by `apps/cloud`; omitted from local single-user configs in favor of keychain and file backends.

## Package exports

Integration plugins share a common export layout:

| Subpath | Contents |
| --- | --- |
| `.` | Promise-mode entry (default for `createExecutor` promise wrappers) |
| `./core` | Effect-native plugin factory |
| `./api` | HTTP-augmented `*HttpPlugin` for host runtimes |
| `./react` / `./client` | Web UI plugin client components |
| `./presets` | Integration preset catalog entries |
| `./testing` | Test helpers and fixtures |

Credential plugins export `.` and `./promise` (and `./core` where published). Install with the package manager of your choice:

<CodeGroup>
```bash title="bun"
bun add @executor-js/sdk @executor-js/plugin-openapi @executor-js/plugin-mcp
```

```bash title="npm"
npm install @executor-js/sdk @executor-js/plugin-openapi @executor-js/plugin-mcp
```
</CodeGroup>

## Choosing plugins

```mermaid
flowchart TB
  subgraph embed["SDK embedder"]
    CE["createExecutor({ plugins })"]
  end
  subgraph host["Host runtime"]
    EC["executor.config.ts"]
    HP["*HttpPlugin from ./api"]
  end
  subgraph protocols["Protocol plugins"]
    OAPI["openApiPlugin / openApiHttpPlugin"]
    GQL["graphqlPlugin / graphqlHttpPlugin"]
    MCP["mcpPlugin / mcpHttpPlugin"]
    GOOG["googlePlugin / googleHttpPlugin"]
  end
  subgraph creds["Credential plugins"]
    MEM["inline providers[]"]
    FILE["fileSecretsPlugin"]
    KEY["keychainPlugin"]
    OP["onepasswordPlugin"]
    WV["workosVaultPlugin"]
  end
  CE --> protocols
  CE --> creds
  EC --> HP
  HP --> protocols
  HP --> creds
  OAPI --> INT["integrations.register + tools per connection"]
  GQL --> INT
  MCP --> INT
  GOOG --> OAPI
  FILE --> PROV["providers.list → connection resolve"]
  KEY --> PROV
  OP --> PROV
  WV --> PROV
  MEM --> PROV
```

<AccordionGroup>
<Accordion title="Other monorepo plugins (not in all-plugins example)">

`@executor-js/plugin-microsoft` follows the Google pattern (discovery to OpenAPI). `@executor-js/plugin-encrypted-secrets` provides an encrypted local store (`encrypted` provider key). `@executor-js/plugin-desktop-settings` is a local-only server plugin for desktop app settings. These ship in host configs but are outside the `examples/all-plugins` dependency set.

</Accordion>

<Accordion title="Integrations registry (separate from plugins)">

`@executor-js/integrations-registry` fetches a curated integration catalog from `https://integrations.sh/api.json` (overridable via `EXECUTOR_INTEGRATIONS_URL`). It is not a plugin; `apps/local` runs it as a background refresh layer for the web UI add-integration flow.

</Accordion>
</AccordionGroup>

## Verification

After wiring plugins, confirm registration:

```bash
cd examples/all-plugins && bun run start
```

Expected console sections: registered provider keys (`memory`, `file`, `keychain`, `onepassword` when reachable), OpenAPI integration `example-api` with four tools, GraphQL integration `example-graphql` with query/mutation tools, and a clean `executor.close()`.

For host runtimes, `executor providers` (CLI) or `GET /providers` (HTTP API) lists registered provider keys. Missing provider errors surface as `CredentialProviderNotRegisteredError` at connection resolve time.

## Related pages

<CardGroup>
<Card title="Embed with the SDK" href="/embed-sdk">
Compose `createExecutor` with plugins, register integrations, and invoke tools from application code.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Connection payloads, OAuth flows, and placement-based auth templates across credential providers.
</Card>
<Card title="Add integrations" href="/add-integrations">
Add OpenAPI, GraphQL, and MCP sources from the web UI or CLI after plugins are loaded.
</Card>
<Card title="SDK quickstart example" href="/sdk-quickstart-example">
Smaller walkthrough focused on OpenAPI and in-memory credentials.
</Card>
</CardGroup>

---

## 23. Develop locally

> Monorepo bootstrap, turbo dev servers, package boundaries, targeted Vitest runs, e2e boot recipes, and release-check scripts for contributors.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/23-develop-locally.md
- Generated: 2026-06-23T19:28:25.856Z

### Source Files

- `RUNNING.md`
- `AGENTS.md`
- `package.json`
- `turbo.json`
- `e2e/AGENTS.md`
- `RELEASING.md`
- `vitest.config.ts`

---
title: "Develop locally"
description: "Monorepo bootstrap, turbo dev servers, package boundaries, targeted Vitest runs, e2e boot recipes, and release-check scripts for contributors."
---

The Executor monorepo is a Bun workspace (`executor-workspace`) whose root scripts orchestrate bootstrap, Turbo dev servers, Vitest across packages, and release gates. Run `bun run bootstrap` on every fresh checkout or worktree before any other command; without it, Vite dev servers fail to resolve `@executor-js/vite-plugin`.

## Prerequisites

| Requirement | Version / note |
|-------------|----------------|
| Bun | `1.3.11` (pinned in root `packageManager`) |
| Node tooling | Turbo, Vitest, oxlint, oxfmt via workspace devDependencies |
| E2E browser tests | Playwright Chromium (installed by bootstrap) |
| Cloud local dev | 1Password CLI (`op`) and `.env.op` for real WorkOS/Autumn credentials, or use e2e emulators |

<Note>
`RUNNING.md` tracks day-to-day mechanics and may lag the code. `AGENTS.md` holds stable contributor contracts; verify boot paths against `e2e/setup/*.globalsetup.ts` and `e2e/setup/*.boot.ts` when behavior drifts.
</Note>

## Bootstrap a fresh checkout

<Steps>
<Step title="Install dependencies and build artifacts">

From the repo root:

```bash
bun run bootstrap
```

Bootstrap is idempotent. It runs `bun install` (whose `prepare` hook builds `@executor-js/vite-plugin` and `@executor-js/react`) and `bunx playwright install chromium` for e2e browser scenarios. A worktree that skips bootstrap dies with `Failed to resolve entry for package '@executor-js/vite-plugin'`.

</Step>

<Step title="Verify the toolchain">

Confirm Vitest is on the path:

```bash
ls node_modules/.bin/vitest
```

Bootstrap throws if Vitest is missing after install.

</Step>
</Steps>

Upstream forks `@executor-js/emulate` and `@executor-js/mcporter` are consumed as published npm packages only. There are no `vendor/` submodules; develop those in their standalone repos and bump versions here.

## Monorepo layout

:::files
executor/
├── apps/
│   ├── cli/              # Published `executor` npm package
│   ├── local/            # Single-user local web app
│   ├── cloud/            # Executor Cloud (Workers + PGlite dev DB)
│   ├── host-selfhost/    # Self-host Docker/worker target
│   ├── host-cloudflare/  # Cloudflare self-host worker
│   ├── desktop/          # Electron app
│   └── marketing/        # Marketing site
├── packages/
│   ├── core/             # sdk, api, config, execution, storage-*, vite-plugin
│   ├── plugins/          # Protocol and credential provider plugins
│   ├── kernel/           # Execution runtimes (QuickJS, IR, dynamic worker)
│   ├── hosts/            # MCP and Cloudflare host surfaces
│   ├── react/            # Shared React UI
│   └── app/              # Shared app shell
├── examples/             # SDK and plugin wiring examples
├── e2e/                  # Cross-target scenario suite
└── tests/                # Root-level smoke tests (release bootstrap)
:::

### Package roles

| Area | Path | Responsibility |
|------|------|----------------|
| Core contracts | `packages/core/sdk` | Plugin wiring, scopes, sources, secrets, policies, test fixtures |
| HTTP API types | `packages/core/api` | Typed API groups: tools, integrations, connections, providers, executions, oauth, policies |
| Storage | `packages/core/storage-*` | Storage adapters and test support |
| Plugins | `packages/plugins/*` | Protocol (OpenAPI, GraphQL, MCP) and provider plugins |
| React UI | `packages/react` | Shared UI and effect-atom integration |
| MCP host | `packages/hosts/mcp` | MCP surface for exposing Executor |
| Runtimes | `packages/kernel/*` | Code execution substrate |
| Entry points | `apps/local`, `apps/cloud`, `apps/cli`, `apps/desktop` | Product compositions |

### Package boundaries

Workspace packages import each other through published package exports, not relative paths across package roots.

```ts
// Good
import { createExecutor } from "@executor-js/sdk";

// Bad: reaches into another package's private tree
import { createExecutor } from "../../../core/sdk/src";
```

If a needed symbol is not exported, add the smallest public export on the owning package. Keep relative imports within a single package root only.

## Dev servers

Turbo runs `dev` across workspace packages. The root `dev` script excludes desktop and cloud:

```bash
bun run dev
```

Equivalent to `turbo run dev --filter='!@executor-js/desktop' --filter='!@executor-js/cloud'`. Turbo's `dev` task depends on `@executor-js/vite-plugin#build`, is uncached, and persistent.

| Target | Command | Notes |
|--------|---------|-------|
| All (except desktop/cloud) | `bun run dev` (repo root) | Turbo parallel dev |
| Single app | `bun run dev` from `apps/<name>` | Boots only that app |
| CLI in dev mode | `bun run dev:cli` (repo root) | Sets `EXECUTOR_DEV=1`, data dir `apps/local/.executor-dev` |
| Desktop + cloud | `bun run dev:desktop` | Includes filtered-out apps |
| Self-host | `cd apps/host-selfhost && bun run dev` | Standalone Vite dev |
| Local app | `cd apps/local && bun run dev` | Portless proxy on 1355, data in `.executor-dev` |
| Cloud app | `cd apps/cloud && bun run dev` | PGlite dev DB + Vite; needs `.env.op` or emulators |

<Warning>
Cloud's default `dev` script uses 1Password (`op run --env-file=.env.op`). For a no-`.env` boot, use the e2e cloud recipe with emulated WorkOS and Autumn (see E2E boot recipes below).
</Warning>

### Kill leaked dev servers

When a session dies without teardown, orphaned Vite or dev-db processes can squat e2e ports:

```bash
bun run reap          # Kill orphans (removed worktrees) and list live ones
bun run reap --all    # Also kill live-checkout servers
bun run reap --dry-run
```

## Unit and package tests

### Test runner rules

- Use **Effect Vitest**: import `describe`, `it`, `expect` from `@effect/vitest`, not raw `vitest` (except config/tooling files).
- **Never** run `bun test`.
- Run the narrowest useful verification for scoped work; use full gates before merge.

### Targeted Vitest runs

Each package owns a `vitest.config.ts` with `include: ["src/**/*.test.ts"]` (or `tests/**/*.test.ts` at root). Run from the package directory:

```bash
cd packages/core/sdk
vitest run src/some-area.test.ts

cd packages/plugins/openapi
vitest run

cd apps/cli
bun run test
```

From the repo root, Turbo fans out package tests (e2e excluded):

```bash
bun run test
```

Root-only smoke:

```bash
bun run test:release:bootstrap
```

### Merge-ready verification gates

```bash
bun run format:check
bun run lint
bun run typecheck
bun run test
```

`bun run ci` runs lint, typecheck, and test together. `typecheck:slow` runs full `tsc` where packages define it.

## E2E boot recipes

The e2e suite boots real dev stacks (or attaches to running ones), drives the product through public surfaces only (API, web UI, MCP, CLI), and writes artifacts under `e2e/runs/<target>/<scenario-slug>/`.

### Port allocation

Each checkout hashes its repo root into a preferred port block (`42000`–`45999`). Globalsetup calls `claimPorts`, atomically locks the block, and walks forward if squatted. Concurrent worktrees do not collide.

```bash
cd e2e
bun run ports
```

<ParamField body="E2E_*_PORT" type="number">
Pin a specific port (no probing). Example: `E2E_SELFHOST_PORT`, `E2E_CLOUD_PORT`.
</ParamField>

<ParamField body="E2E_<TARGET>_URL" type="string">
Attach to an already-running instance instead of booting. Example: `E2E_CLOUD_URL=http://127.0.0.1:<port>`.
</ParamField>

### Self-host boot contract

Canonical recipe: `e2e/setup/selfhost.boot.ts` (shared by vitest globalsetup and `bun run cli up selfhost`).

| Variable | Purpose |
|----------|---------|
| `EXECUTOR_DATA_DIR` | Fresh data dir per suite run (wiped by default) |
| `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` | Bootstrap admin email |
| `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` | Bootstrap admin password |
| `EXECUTOR_WEB_BASE_URL` | Advertised URL for cookies and redirects |
| `EXECUTOR_ALLOW_LOCAL_NETWORK` | `true` for hermetic e2e (allows localhost MCP/OAuth test servers) |
| `BETTER_AUTH_SECRET` | Auth secret for self-host |

### Cloud boot contract

Canonical recipe: `e2e/setup/cloud.boot.ts`. Boots emulated WorkOS + Autumn + the app's real dev stack with PGlite. No `.env` required. Optional motel OTLP store captures exported spans for telemetry scenarios.

### Run scenarios

```bash
cd e2e
bun run test                    # cloud + selfhost (default)
bun run test:cloud              # cloud only
bun run test:selfhost           # selfhost only
bun run test:selfhost-docker    # production Docker artifact (release gate)
bun run test:cloudflare         # Cloudflare worker host
```

Attach while iterating:

```bash
E2E_CLOUD_URL=http://127.0.0.1:<port> ../node_modules/.bin/vitest run --project cloud <file>
E2E_SELFHOST_URL=http://localhost:<port> ../node_modules/.bin/vitest run --project selfhost <file>
```

### Interactive dev CLI

Same boot primitives as scenarios, as commands:

```bash
cd e2e
bun run cli up selfhost --share   # boot, tailnet-reachable, stays up
bun run cli up cloud --share      # emulated WorkOS+Autumn
bun run cli status
bun run cli identity selfhost
bun run cli api selfhost tools.list
bun run cli mcp selfhost call execute '{"code":"return 1+1;"}'
bun run cli down selfhost
```

Instances persist until `down`. State files in `e2e/.dev/` mark deliberate long-lived instances.

### View and share run artifacts

```bash
cd e2e
bun run serve
```

Serves the scenario × target matrix over HTTP (prefers port 8901, walks forward if busy). Individual runs: `#/<target>/<slug>`. Browser scenarios produce `session.mp4`, `trace.zip`, and step screenshots.

```bash
bun e2e/scripts/pr-media.ts e2e/runs/<target>/<slug>
```

Converts a recording to GIF and uploads to the `e2e-media` branch for PR-ready markdown.

### Writing scenarios

See `e2e/AGENTS.md`. Key rules:

- Black-box only: no app internals, no DB pokes.
- Import `expect` from `@effect/vitest`; use Effect generators in scenario bodies.
- Selfhost: prefix resources with scenario slug (shared bootstrap admin).
- Cloud: `newIdentity()` gives fresh user+org isolation.
- Clean up with `Effect.ensuring`, not trailing statements.

## Release-check scripts

Releases use Changesets. Local dry runs validate publish payloads without uploading.

| Script | Purpose |
|--------|---------|
| `bun run release:check` | CLI typecheck + `test:release:bootstrap` + `release:publish:dry-run` |
| `bun run release:publish:dry-run` | Full CLI release build to `apps/cli/dist` |
| `bun run release:publish:packages:dry-run` | Pack `@executor-js/*` library packages |
| `bun run release:publish:packages` | Publish library packages to npm |
| `bun run release:beta:start` | Enter Changesets prerelease mode (`beta` dist-tag) |
| `bun run release:beta:stop` | Exit prerelease mode |
| `bun run changeset` | Add a changeset in your PR |

`release:check` is what CI runs before CLI publish. Self-host Docker validation:

```bash
docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost:local .
```

Changeset bodies are the changelog entries. Write user-visible summaries in `.changeset/*.md`; `changeset version` compiles them into per-package `CHANGELOG.md`.

## Common gotchas

<AccordionGroup>
<Accordion title="Stale Vite dep-optimizer cache after rebase">

Symptom: dev servers show pre-rebase behavior while unit tests pass. Kill the server, clear `node_modules/.vite` and `.tanstack`-adjacent caches, reboot.

</Accordion>

<Accordion title="Port squatting across worktrees">

If boot attaches to another checkout's server with baffling auth errors, an old dev server leaked. Run `bun run reap`, check `e2e/.dev/<target>.json` for deliberate instances, then `bun run cli down <target>`.

</Accordion>

<Accordion title="Cloud login over plain HTTP from non-localhost">

Cloud sets `secure: true` auth cookies. Login breaks over plain HTTP from non-localhost origins ("Invalid login state"). The e2e `cli up cloud --share` path fronts both app and WorkOS emulator with `tailscale serve` HTTPS.

</Accordion>

<Accordion title="bun.lock conflicts on rebase">

Take either side, re-run `bun install`. Never hand-merge the lockfile.

</Accordion>

<Accordion title="Scratch scripts outside the repo">

Do not write probe scripts to `/tmp`; they cannot resolve workspace packages. Use `scratch/` (gitignored) under the repo root.

</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Bootstrap a development checkout and verify the background service starts with expected ports and data directories.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Daemon port conflicts, stale Vite caches, missing bootstrap builds, and recovery commands.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`EXECUTOR_DATA_DIR`, `EXECUTOR_SCOPE_DIR`, bootstrap admin env vars, ports, and client overrides.
</Card>
<Card title="Plugin catalog" href="/plugin-catalog">
Published protocol and provider plugins and how `examples/all-plugins` wires them.
</Card>
<Card title="SDK quickstart example" href="/sdk-quickstart-example">
Walkthrough of `examples/docs-sdk-quickstart` for in-process SDK development.
</Card>
</CardGroup>

---

## 24. Troubleshooting

> Common failure modes: daemon port conflicts, unreachable local server, OAuth login state behind proxies, stale Vite caches, missing bootstrap builds, and recovery commands.

- Page Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/pages/24-troubleshooting.md
- Generated: 2026-06-23T19:29:14.085Z

### Source Files

- `RUNNING.md`
- `apps/cli/src/daemon.ts`
- `apps/cli/src/server-connection.ts`
- `apps/cli/src/local-server-manifest.ts`
- `scripts/reap-dev-servers.ts`
- `e2e/src/ports.ts`
- `packages/core/sdk/src/public-origin.ts`

---
title: "Troubleshooting"
description: "Common failure modes: daemon port conflicts, unreachable local server, OAuth login state behind proxies, stale Vite caches, missing bootstrap builds, and recovery commands."
---

Executor surfaces failures through CLI error messages, the unauthenticated `GET /api/health` liveness probe (expects body `ok`), and files under `~/.executor/server-control/`. Most local issues trace to a port conflict, a stale `server.json` manifest, a startup lock left by a crashed process, or a misconfigured public origin behind a reverse proxy.

## Quick diagnostics

<Steps>
<Step title="Check server reachability">

Probe the local daemon or service without credentials:

```bash
curl -s http://localhost:4788/api/health
```

A healthy server returns `ok`. The CLI uses the same probe with a 2-second timeout before auto-starting a local daemon.

</Step>
<Step title="Inspect daemon and service state">

```bash
executor daemon status
executor service status
```

`daemon status` reports `running`, `unreachable`, or removes a stale record when the recorded PID is dead. `service status` tolerates a registered-but-unreachable manifest and prints platform registration details.

</Step>
<Step title="Read error logs">

Supervised daemons write stderr to:

```text
~/.executor/logs/daemon.error.log
```

For foreground debugging, run `executor daemon run --foreground` and watch the terminal output.

</Step>
</Steps>

## Symptom index

| Symptom | Likely cause | First recovery step |
| --- | --- | --- |
| `Port 4788 is in use. Starting daemon on available port …` | Another process bound the preferred port | Use the reported port, or stop the squatter and restart |
| `Another local Executor server startup is already in progress` | `startup.lock` held by a live or stale PID | Wait, or remove the lock after confirming the PID is dead |
| `… is registered at … but is not reachable` | Manifest exists, process alive, health probe fails | Stop the process or fix why `/api/health` does not respond |
| `Refusing to start another local server against the same database` | Active manifest for the same `EXECUTOR_DATA_DIR` | Stop the active server (`executor daemon stop`) or use it |
| `Executor server is not reachable at …` | Remote or non-auto-start target down | Set `EXECUTOR_API_KEY` (hosted) or `EXECUTOR_AUTH_TOKEN` (local) |
| `Invalid login state` (Executor Cloud) | OAuth CSRF cookie mismatch, often over plain HTTP | Serve over HTTPS; ensure cookies reach the callback |
| `Invalid origin` / `INVALID_ORIGIN` (self-host) | Request origin does not match configured `webBaseUrl` | Set `EXECUTOR_WEB_BASE_URL` to the public URL and restart |
| `Failed to resolve entry for package '@executor-js/vite-plugin'` | Skipped `bun run bootstrap` in a fresh checkout | Run `bun run bootstrap` from the repo root |
| Dev server serves old code while tests pass | Stale Vite dependency optimizer cache | Kill dev servers, delete `node_modules/.vite`, reboot |

## Daemon port conflicts

The CLI defaults to port **4788** for `executor daemon run`. The OS-supervised background service defaults to **4789** so desktop connect-card configs keep resolving; clients discover the live port from `server.json`.

When auto-starting, `chooseDaemonPort` probes the preferred port and, if busy, picks an ephemeral fallback:

```bash
Port 4788 is in use. Starting daemon on available port 47912 instead.
```

<Warning>
If a port conflict leaves you with multiple daemons, only one should own a given `EXECUTOR_DATA_DIR`. A second start against the same data directory is refused even when ports differ.
</Warning>

<Tabs>
<Tab title="Interactive daemon">

```bash
# See what is registered
executor daemon status

# Stop by base URL (default http://localhost:4788)
executor daemon stop

# Restart cleanly
executor daemon restart
```

</Tab>
<Tab title="OS service">

```bash
executor service status
executor service uninstall   # stops daemon and removes the unit
executor service install     # reinstall; default port 4789
```

If `service install` times out waiting for a manifest, check `~/.executor/logs/daemon.error.log`.

</Tab>
</Tabs>

For contributor e2e boots, ports are derived per checkout (`cd e2e && bun run ports`) and atomically claimed across blocks in `42000–45999`. When a boot reports squatters on the preferred block, a leaked dev server from another session is the usual cause.

## Unreachable local server

The CLI treats a local server as active when `server.json` exists at `~/.executor/server-control/server.json` (or under `EXECUTOR_DATA_DIR`) and `/api/health` returns `ok`. Three failure shapes matter:

1. **No manifest, server down** — CLI auto-starts a local daemon for `http://localhost` targets without explicit auth.
2. **Manifest + dead PID** — Stale manifest is removed automatically; auto-start proceeds.
3. **Manifest + live PID + failed health probe** — Startup is blocked to prevent two processes sharing one database.

<RequestExample>

```bash
executor call tools.list
```

</RequestExample>

<ResponseExample>

```text
A local Executor cli-daemon is registered at http://localhost:4788 (pid 12345) but is not reachable.
Refusing to start another local server against the same data directory.
Stop the existing process or remove the stale server-control manifest after verifying the process is not using the database.
```

</ResponseExample>

<Steps>
<Step title="Stop the registered process">

```bash
executor daemon stop
# or, if supervised:
executor service uninstall
```

</Step>
<Step title="Verify the PID is gone">

```bash
executor daemon status
```

If the PID is dead and the server is unreachable, `daemon status` removes the stale record.

</Step>
<Step title="Start in foreground to inspect">

```bash
executor daemon run --foreground --port 4788 --hostname 127.0.0.1
```

In a dev checkout, use `bun run apps/cli/src/main.ts daemon run --foreground` instead of the compiled binary.

</Step>
</Steps>

### Remote or authenticated targets

Auto-start applies only to unauthenticated `http://` targets on `localhost`, `127.0.0.1`, or `::1`. For everything else:

<ParamField body="EXECUTOR_API_KEY" type="string">
Bearer API key for hosted Executor Cloud.
</ParamField>

<ParamField body="EXECUTOR_AUTH_TOKEN" type="string">
Bearer token for a local or desktop server (also published in the server manifest).
</ParamField>

If the server is down, the CLI reports:

```text
Executor server is not reachable at https://your-instance.example.com.
For hosted Executor, set EXECUTOR_API_KEY to a bearer API key.
For local or desktop servers, set EXECUTOR_AUTH_TOKEN to the server's bearer token.
```

## OAuth and login state behind proxies

### Executor Cloud: `Invalid login state`

Cloud auth sets a `secure: true`, `httpOnly` CSRF cookie (`wos-login-state`) on `/login` and validates it on `/callback`. A mismatch returns HTTP 400 with body `Invalid login state`.

Common triggers:

- Browsing over **plain HTTP** from a non-localhost origin (secure cookies are not sent).
- A reverse proxy that strips or rewrites cookies between login and callback.
- Hitting the callback after the state cookie expired (10-minute `Max-Age`).

<Info>
When sharing a cloud dev instance over a tailnet, both the app and the WorkOS emulator need HTTPS fronts. Set `WORKOS_API_URL` to the emulator's public origin (the browser-facing authorize URL derives from it), and allow the public hostname in Vite via `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS`.
</Info>

### Self-host: `Invalid origin`

Better Auth rejects requests whose `Origin` does not match the configured `webBaseUrl`. Behind a reverse proxy, the instance may see `https://app.example.com` while still configured for `http://localhost:4788`.

Set the pinned public origin explicitly:

<ParamField body="EXECUTOR_WEB_BASE_URL" type="string" required>
Public base URL (no trailing slash), e.g. `https://executor.example.com`. Used for OAuth redirects, MCP OAuth metadata, and connect links.
</ParamField>

Railway, Render, Fly, Vercel, and similar hosts can be detected automatically via platform env vars. For custom domains, set `EXECUTOR_WEB_BASE_URL` and restart. The server logs a one-time warning when it falls back to localhost in non-development deployments.

<Warning>
Public origin must come from operator-set env vars or platform-injected deploy-time values, never from the request `Host` header. That prevents host-header injection from poisoning OAuth redirect URLs.
</Warning>

## Stale Vite caches (contributors)

After a rebase or large dependency change, Vite's dependency optimizer can serve pre-rebase code in dev servers while unit tests pass.

<Steps>
<Step title="Stop all dev servers">

Kill turbo/vite processes for this checkout.

</Step>
<Step title="Clear optimizer caches">

```bash
rm -rf node_modules/.vite
```

Also clear any `.tanstack`-adjacent caches if present.

</Step>
<Step title="Reboot dev servers">

```bash
bun run dev
```

</Step>
</Steps>

For leaked dev stacks across worktrees, `bun run reap` lists vite, dev-db, and dev.ts processes. By default it kills **orphans** (checkouts whose path no longer exists). Pass `--all` to kill live-checkout servers when no other session needs them, or `--dry-run` to list without killing.

## Missing bootstrap builds

A fresh monorepo checkout or worktree must run bootstrap before dev servers or e2e:

```bash
bun run bootstrap
```

Bootstrap runs `bun install` (whose `prepare` hook builds `@executor-js/vite-plugin` and `packages/react`) and installs Playwright Chromium. Skipping bootstrap produces:

```text
Failed to resolve entry for package '@executor-js/vite-plugin'
```

Bootstrap is idempotent and safe to re-run after lockfile conflicts: take either side of a `bun.lock` conflict, then `bun install` again.

## Startup locks and manifests

| File | Purpose |
| --- | --- |
| `~/.executor/server-control/server.json` | Active server manifest (origin, PID, bearer token) |
| `~/.executor/server-control/startup.lock` | Prevents concurrent local server boots |
| `~/.executor/server-control/auth.json` | Bearer token for supervised daemons (mode `0600`) |
| `~/.executor/logs/daemon.error.log` | Supervised daemon stderr |

Locks embed the owning PID. If a crash leaves a lock behind, the next startup removes it when that PID is no longer alive. If startup reports `Another local Executor server startup is already in progress` and the PID is dead, remove `startup.lock` manually after verifying no Executor process is running.

<Tip>
Override data location with `EXECUTOR_DATA_DIR`. Scope identity (for daemon pointer files) uses `EXECUTOR_SCOPE_DIR` when set, otherwise the current working directory.
</Tip>

## E2e and multi-checkout port collisions

Concurrent checkouts hash the repo root into a preferred port block. `claimPorts` walks forward until it finds a fully free block. Pin explicit ports with `E2E_*_PORT` env vars, or attach to a running instance with `E2E_CLOUD_URL` / `E2E_SELFHOST_URL`.

When vite's `--strictPort` fails, fix squatters rather than letting tests attach to another checkout's server (which produces baffling auth errors instead of a clear bind failure):

```bash
cd e2e && bun run ports    # print this checkout's block
bun run reap               # kill orphaned dev stacks
```

Long-lived demo instances intentionally left running are marked in `e2e/.dev/<target>.json`; check before reaping, and tear down with `cd e2e && bun run cli down <target>`.

## Recovery command reference

<CodeGroup>

```bash title="Local daemon"
executor daemon status
executor daemon stop
executor daemon restart
executor daemon run --foreground --port 4788 --hostname 127.0.0.1
```

```bash title="OS service"
executor service status
executor service install
executor service uninstall
```

```bash title="Contributors"
bun run bootstrap
bun run reap
bun run reap --all
cd e2e && bun run ports
cd e2e && bun run cli status
cd e2e && bun run cli down selfhost
```

```bash title="Health probe"
curl -s http://localhost:4788/api/health
curl -s http://localhost:4789/api/health   # supervised default
```

</CodeGroup>

<AccordionGroup>
<Accordion title="Auto-start chose a different port than expected">

The CLI printed the actual port when the preferred one was busy. Either connect to that origin, stop the squatter on 4788, or pass an explicit `--base-url` / `--port` when starting the daemon.

</Accordion>
<Accordion title="service install succeeded but web UI does not open">

Run `executor service status` and `executor daemon status`. Read `~/.executor/logs/daemon.error.log`. Confirm the manifest at `server.json` lists a reachable origin, then run `executor web`.

</Accordion>
<Accordion title="OAuth works on localhost but fails through a tunnel">

Ensure HTTPS end-to-end, set `EXECUTOR_WEB_BASE_URL` (self-host) or front both app and identity provider with HTTPS (cloud dev), and allow the public hostname in Vite when a proxy fronts the dev server.

</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Bootstrap a checkout, install the CLI, and verify the background service starts with expected ports and data directories.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`EXECUTOR_DATA_DIR`, `EXECUTOR_SCOPE_DIR`, `EXECUTOR_WEB_BASE_URL`, ports, and client overrides.
</Card>
<Card title="CLI reference" href="/cli-reference">
`daemon`, `service`, `web`, and auto-start behavior for all subcommands.
</Card>
<Card title="Develop locally" href="/develop-locally">
Monorepo bootstrap, turbo dev servers, e2e port claiming, and contributor workflows.
</Card>
<Card title="Deploy self-hosted" href="/deploy-selfhost">
Docker and Workers deploys, `EXECUTOR_WEB_BASE_URL`, TLS, and sandbox network constraints.
</Card>
</CardGroup>

---
