pi-tower

Control tower for remote pi runners: relay server, runner wrapper, and pi extension

Packages

Package details

extensionskill

Install pi-tower from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-tower
Package
pi-tower
Version
0.3.0
Published
Aug 19, 2026
Downloads
674/mo · 21/wk
Author
kettan
License
MIT
Types
extension, skill
Size
30.5 KB
Dependencies
1 dependency · 1 peer
Pi manifest JSON
{
  "extensions": [
    "./extension.ts"
  ],
  "skills": [
    "./skills"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-tower

pi-tower

Control tower for remote pi runners. Register a headless pi on any machine, then let an interactive pi session anywhere dispatch tasks to it by name — like calling a remote coding agent as a tool.

┌────────────────────── interactive pi (anywhere) ─────────────────────────────────┐
│  process: pi (interactive TUI)                                                   │
│  ┌────────────────────────────────────────────────────────────┐                  │
│  │  extension.ts                                              │                  │
│  │  ├─ registerFlag("--tower", "--tower-token")               │                  │
│  │  └─ registerTool("runner_list", "runner_task")             │                  │
│  │       execute() ──── wss ────────────────────────────────────────┐            │
│  └────────────────────────────────────────────────────────────┘     │            │
└─────────────────────────────────────────────────────────────────────┼────────────┘
                                                                      │
                                              wss (RPC JSONL frames, token auth)
                                                                      │
┌────────────────── tower (any reachable host) ───────────────────────┼────────────┐
│  process: pi-tower (tower.mjs)                                      ▼            │
│  ┌────────────────────────────────────────────────────────────┐                  │
│  │  registry: { "win-test-1" → control ws + session pipes }   │                  │
│  │  pure relay per session pipe, client ⇄ runner untouched    │                  │
│  │  one client per session; sessions run in parallel          │                  │
│  └──────────────────────────────▲─────────────────────────────┘                  │
└─────────────────────────────────┼────────────────────────────────────────────────┘
                                  │
              wss outbound (runner dials out, NAT/firewall friendly)
                                  │
┌───────────────────── runner (any machine with pi) ─┼─────────────────────────────┐
│  process: pi-runner (runner.mjs)                   │                             │
│  ┌─────────────────────────────────────────────────┴──────────┐                  │
│  │  pi-runner --hq wss://hq.example.com --id win-test-1       │                  │
│  │  control ws + one data ws per session (LF JSONL)           │                  │
│  └───────────────┬────────────────────────────────────────────┘                  │
│                  ▼                                                               │
│  child processes: pi --mode rpc × N   (one per session, tools run locally)       │
└──────────────────────────────────────────────────────────────────────────────────┘

Setup

Three roles, each runnable on any machine (even all three on one box); the runner and interactive sides also need pi installed.

Tower (any host the runner and interactive sides can both reach)

npx pi-tower --port 9000 --token <shared-token>   # or PI_TOWER_TOKEN env

Or with Docker plus a Cloudflare Tunnel (no exposed port, TLS terminates at the edge):

cp .env.example .env   # set PI_TOWER_TOKEN and TUNNEL_TOKEN
docker compose up -d

In the Zero Trust dashboard, point the tunnel's public hostname at http://tower:9000; the tower URL everywhere else is then wss://<that-hostname>.

Runner (the machine that executes tasks: a CI box, a lab PC, a server)

npx pi-runner --hq wss://hq.example.com --id win-test-1 --token <shared-token> -- --no-session

Args after -- go to the spawned pi --mode rpc and are all optional. --no-session keeps task transcripts off the runner's disk; drop it for an on-machine audit trail of what remote tasks did. The runner dials out and reconnects every 3s, so it works behind NAT. --id defaults to the hostname.

Interactive side (wherever you drive pi from)

pi-tower is a pi package bundling the extension (runner_task / runner_list tools) and the remote-runner skill. Install it from npm or GitHub, or try a local checkout without installing:

pi install npm:pi-tower
pi install git:github.com/iamken1204/pi-tower  # or from GitHub
pi -e /local/path                              # or try a local checkout (this run only)

Then start pi with the tower flags and prompt "use runner_task on win-test-1 to ...":

pi --tower wss://hq.example.com --tower-token <shared-token>

Providers with a direct API key see the extension tools natively. Providers that run their agent loop server-side (and never expose extension tools) get the remote-runner skill instead, which teaches the model the pi-task CLI below.

pi-task CLI

Some providers run their agent loop server-side and never expose extension-registered tools to the model. pi-task is the provider-agnostic fallback: any agent (or human) dispatches with one shell command instead of the extension tools.

export PI_TOWER_URL=wss://hq.example.com PI_TOWER_TOKEN=<shared-token>
pi-task --list                    # who's online
pi-task win-test-1 "run the failing job and report the error"

stdout carries only the final answer, so $(pi-task ...) captures cleanly; progress streams to stderr only in an interactive terminal, keeping piped output clean for agent callers. --session <name> picks the session (see below); --fresh resets the session's conversation first; Ctrl-C forwards an abort to the runner.

Sessions

Each runner runs one pi --mode rpc process per session, so different sessions run in parallel with full process isolation. Tasks that reuse a session name continue its conversation — context survives between tasks and across detach/reattach. The default session is main; names match [A-Za-z0-9._-]{1,64}. Idle session processes stay alive until the runner stops.

pi users get discovery via the bundled remote-runner skill automatically. For non-pi agents, add a line to the project's AGENTS.md instead:

Remote runner tasks: `pi-task <runner-id> "<prompt>"`; list runners: `pi-task --list` (env: PI_TOWER_URL, PI_TOWER_TOKEN).

Wire contract

Session pipes are pure relays: each WebSocket text frame is one pi RPC JSONL record (see pi's docs/rpc.md), untouched in both directions. The runner's control channel carries only {"type":"open","session":"<name>"} frames from the tower; the runner answers by dialing a session pipe.

Every HTTP request and WebSocket upgrade authenticates with an Authorization: Bearer <token> header. The tower pings every connection every 30s and terminates peers that miss two pings.

Endpoint Purpose
GET / health, returns pi-tower
GET /runners JSON [{id, connectedAt, sessions}]
WS /runner?id=<id> runner control channel; same id reconnect replaces the socket, live sessions survive
WS /runner-session?id=<id>&session=<name> runner-dialed data pipe, one per session
WS /attach?runner=<id>&session=<name> client attachment, one per session (session defaults to main)
Close code Meaning
4001 bad token
4004 unknown runner (reason lists online ids)
4005 session busy (another client attached or attaching)
4006 session disconnected while attached
4007 runner failed to open the session (15s timeout or runner offline)

Detaching a client leaves its session pipe idle on the tower, so a later attach with the same name resumes the conversation without a new open.

Security

Single shared token, sent as an Authorization header on every upgrade and HTTP request, so it stays out of URLs and access logs. Run the tower behind a TLS reverse proxy (caddy/nginx) so the public URL is wss://; the token and all traffic are plaintext otherwise. Anyone with the token can drive any runner — runners execute arbitrary commands, so treat the token like an SSH key.

Verify

npm run verify

Four assert-based scripts: tower relay semantics (fake runner), full chain through a real pi --mode rpc (no LLM call), the extension's tools plus the pi-task CLI driven against a fake runner, and pi-package loading via pi -e . (extension flag registered, skill listed).