Skip to content

Add @executor-js/pi, a first-party Pi extension - #1961

Open
mangit955 wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
mangit955:feat/pi-extension
Open

Add @executor-js/pi, a first-party Pi extension#1961
mangit955 wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
mangit955:feat/pi-extension

Conversation

@mangit955

@mangit955 mangit955 commented Sep 7, 2026

Copy link
Copy Markdown

Addresses #1957.

Pi ships no MCP client, so add-mcp does not apply to it and connecting Pi to Executor meant installing a third-party bridge — an unofficial adapter holding access to every integration you have configured. This adds an Executor-maintained extension instead.

What it does

packages/hosts/pi registers Executor's core surface as native Pi tools and forwards each call over MCP:

  • executor_execute — run JavaScript in the sandbox
  • executor_skills — Executor's own docs, starting with the guide for writing execute code
  • executor_resume — resume an execution paused for approval

That is the same small surface every other agent gets, so Pi's context stays clear of individual tool schemas and lazy discovery is preserved.

pi install npm:@executor-js/pi

Configuration

Environment-only, deliberately. Pi installs this from npm, so a credential in package config would be a credential in every user's node_modules — the issue called that out by name.

Variable Purpose
EXECUTOR_MCP_URL the endpoint from the Connect an agent card
EXECUTOR_API_KEY hosted Executor, from the API Keys page
EXECUTOR_AUTH_TOKEN local or desktop Executor's own bearer token

EXECUTOR_API_KEY wins when both are set, matching the CLI's precedence in readCliServerAuth. /executor prints the resolved endpoint and the tools Executor actually serves, so an install can be checked before it is trusted.

Design notes worth review

Startup never fails. Config resolution returns a result rather than throwing, and the MCP connection opens on first use — Pi has to start cleanly when Executor is down, misconfigured, or simply not running yet. A failed connect clears its memo so the next call retries, and a transport-level failure drops the session so an Executor restart is survivable without restarting Pi.

The tool set is a literal, not a mirror of tools/list. That is what lets Pi start while Executor is unreachable. schema.test.ts asserts the registered parameters against a real Executor MCP server, so drift fails CI rather than reaching users.

elicitation_mode=model is pinned onto the URL. The resume schema here is the model-mode shape (executionId + action + content). In browser mode the server registers a resume taking executionId alone, so a pasted ?elicitation_mode=browser URL would leave us advertising parameters the server rejects. Unrelated query parameters are preserved.

Errors are thrown, not returned. Pi's AgentToolResult has no error field — the harness marks a call failed exactly when execute throws, and keeps only the message. So an MCP isError result has to leave as a thrown Error carrying the server's text. Auth failures get a remedy specific to the variable the token came from, since a hosted API key and a local server token are refilled in different places. This is why .oxlintrc.jsonc scopes the throw/catch rules off for this package only: it sits on Pi's runtime boundary, where throwing is the protocol. The rules that keep our own failures typed stay on.

Non-text content degrades rather than breaking. Pi accepts text and images only. Audio and resource blocks become a placeholder line; since Executor already precedes every binary block with a File output: … line, the model still knows the file exists and what it is.

Verification

  • format:check, lint, typecheck — clean
  • packages/hosts/pi: 27 tests across 4 files pass
  • bun run --filter='@executor-js/pi' build produces the paths publishConfig.exports and the pi manifest point at
  • e2e, both halves of the documented setup:
    • e2e/scenarios/pi-extension.test.ts (selfhost) — packs and installs the package through Pi's own package manager, then drives it through Pi's own loader: /executor, executor_execute, executor_skills, and a executor_resume call asserting the server accepts the advertised arguments. The whole user path with only the model left out.
    • e2e/local/pi-extension.test.ts (local) — boots a real executor web, takes the token it prints, and follows the README exactly as a local user would.

Both pass. The package is wired into build:packages, publish-packages.ts, and smoke-test-packed.ts, and a changeset is included.

Not included

The issue also asks for Pi on the Integrations / "Connect an agent" screen. That is a separate change to the connect-card surface and I left it out to keep this reviewable; happy to follow up, or fold it in if you would rather it ship together.

Pi ships no MCP client, so connecting it to Executor meant trusting a
third-party bridge with access to every configured integration. This adds
an Executor-maintained extension instead.

It registers Executor's core surface — executor_execute, executor_skills,
and executor_resume — as native Pi tools and forwards each call over MCP,
so Pi gets the same lazy discovery every other agent does and its context
stays clear of individual tool schemas.

Configuration is environment-only, never package config: EXECUTOR_MCP_URL
plus EXECUTOR_API_KEY (hosted) or EXECUTOR_AUTH_TOKEN (local/desktop).
/executor reports the resolved endpoint and checks the connection.

Closes UsefulSoftwareCo#1957
@mangit955
mangit955 marked this pull request as ready for review September 7, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant