pi-mono/apps/p2/docs/handoff.md
Armin Ronacher 7dc38ff0e9 feat(p2): MCP servers on the host, on environments and from projects
A manager on the p2 server owns every MCP connection and process. Session
workers hold one view each and lease the locations their conversations
work at; the manager runs the owner's mcp.json servers there (stdio per
session and location, or shared; HTTP shared by default) and offers their
tools as deferred Durable tools found through tool_search.

Stdio servers on an environment start through its pi-env daemon, one
connection per environment shared by its instances, through cmd.exe on
Windows. A project's .pi/mcp.json, read by the worker at each location,
may add stdio servers that run there and switch the owner's servers on or
off; the manager refuses everything else. A lease gets servers only on
environments its session works on.
2026-11-03 16:48:38 +01:00

12 KiB
Raw Blame History

p2 handoff

For whoever continues p2, human or agent, starting cold. It assumes you know pi and this repository, but not the history of p2. Everything you need is in the repository; nothing depends on earlier conversations.

What p2 is

p2 (apps/p2) is Pi Mobile rebuilt on Chord, an object runtime (Chord v2: @earendil-works/chord, packages/chord; p2 runs on Chord v2 only, see docs/p2-notes.md Chord v2; Chord v1 is deleted). Every feature is a plugin (apps/p2/plugins/*) with facets per process: server.ts, worker.ts (one worker per session, running Durable), web.tsx and tui.ts. There is no manifest and no dependency list: p2 discovers plugins by listing their facet files and loads them alphabetically. Chord resolves dependencies by contract name, so their order does not matter; a missing provider fails loudly, naming the consumer. Compatible backend reloads preserve unchanged state and resources; a successful reload publishes rebuilt web and TUI bundles.

p2 replaced Pi Mobile (formerly apps/mobile, removed; see git history). Its web UI matches Pi Mobile's (same DOM, classes, styles); the data layer is Chord objects instead of Pi Mobile's hand-written protocol. On top of Pi Mobile, p2 has hot reload with explicit reload/restart controls, logs and owner notices, chat mode with replies and @pi mentions, owner sign-in links, an isolated TUI with pi's look and keys, and security hardening (CSP, restricted guest web assets, WebSocket limits).

p2 lives in earendil-works/pi at apps/p2 and keeps the name p2.

State at handoff

Where What Port Data
Phone (Termux, Node 26) p2, the daily driver; Pi Mobile's sessions were imported when it replaced Pi Mobile 8787 ~/.pi/p2
slayer (slayer.marioslab.io) p2 in dev mode, public at https://p2.mariozechner.at 8788 ~/.pi/p2 (test data)

Done: everything Pi Mobile did (checked by a four-part audit; what differs on purpose is under Decisions in docs/p2-notes.md), the switch of the phone, the TUI port phases P0–P7 and P9–P11, and Chord in packages/chord replacing the old facet/service runtime and the pi-client, pi-protocol and pi-server packages (pi 6db4070f1). Open items are under Open work.

The phone

  • Checkout: ~/pi-private, branch private. Old data: ~/.pi/mobile (keep it: imported history refers to attachments there), ~/.pi/p2.parallel and ~/.pi/p2.trial-oct10 (leftovers, deletable).
  • p2 runs in tmux session p2 from ~/pi-private/apps/p2 (as planned for the switch; confirm with tmux ls and ps):
    PORT=8787 P2_ALLOWED_HOSTS=pim.ngrok.pro P2_PUBLIC_URL=https://pim.ngrok.pro npm run dev
    
    Update: git pull, npm install --ignore-scripts at the root if dependencies changed, then Restart in the app or npm run reload -- --restart (plugin changes: Reload).
  • Tunnel: ngrok as a termux-services (runit) service, $PREFIX/var/service/ngrok/run, forwarding https://pim.ngrok.pro to port 8787 (sv status|restart $PREFIX/var/service/ngrok; logs in $PREFIX/var/log/ngrok/current).
  • Radius: not configured (no radius.json); npm run radius:login enables it.
  • No boot scripts. Run termux-wake-lock so Android keeps the processes alive.

Running p2

cd apps/p2
npm run dev                  # dev mode: apply plugin changes, including web facets, with Reload or `npm run reload`
npm start                    # without the dev supervisor
P2_FAKE=1 npm start          # fake model, no provider needed
npm run owner-link [url]     # one-time owner sign-in link for another device (run on the machine itself)
npm run tui [url]            # TUI (Node >= 26.5.1); on loopback it signs in as owner automatically
npm run reload [plugins] [--restart]
npm run check                # Biome + tsc
npm test                     # full suite; TUI tests need Node 26, browser tests need CHROMIUM_BIN

Environment variables: docs/p2-notes.md (Running). Configuration in the data dir: models.json, settings.json (incl. codemode, default on in p2), envs.json, mcp.json, auth.json, radius.json, forwards.json.

Sign-in. Requests from the device itself are the owner. Other devices get owner rights with a link from npm run owner-link (single use, 10 minutes, long-lived signed cookie). Guests join through share links. Requests other websites make never carry owner rights. Sign out per device or all devices in the Devices sheet.

Dev mode. A file watcher only marks plugins dirty; reloads are explicit (button, npm run reload, the owner-only reload_plugins tool) because a watcher can't tell when an agent has finished a multi-file edit. Runtime code (src/hosts, src/runtime, workspace packages such as packages/chord) needs a restart; the "Server code changed" pill appears only when a module the server actually loaded changed.

slayer. ~/p2.mariozechner.at/ holds run.sh (starts npm run dev from the deploy worktree ~/p2-live), deploy.sh (moves ~/p2-live to origin/private and restarts), bridge.mjs (unix socket for Caddy → 127.0.0.1:8788), Caddyfile and docker-compose.yml (Caddy only marks requests as proxied; p2 authenticates on its own). It runs in tmux session p2. Development happens in ~/pi-private; ~/p2-verify is a scratch worktree for verifying an agent's files on their own before committing. Node 24: ~/node-v24.4.1-linux-x64/bin; Node 26 (TUI): ~/.local/bin/node26. Browser tests: CHROMIUM_BIN=/home/badlogic/.local/bin/chromium-docker.

Architecture in five minutes

  • Chord v2 (packages/chord, docs/design.md there): objects per (type, params, principal) placed on host kinds (Server, Session({ id })), JSON Schema contracts, held references, documents (defineState) and derives, natives (self.open), keyed registries, binary links over WebSocket and IPC with routes (browser → server → worker), the workers component (createWorkers) and the plugins component (createPlugins: build, load, rollout). How p2 uses it: docs/p2-notes.md Chord v2.
  • Hosts (src/hosts): server.ts (HTTP, auth, WebSocket admission), web.ts (web serving), bundles.ts (content-named web and TUI esbuild bundles), worker.ts (session worker), tui*.ts (launcher, sandbox, filter, grants), dev*.ts (dev supervisor).
  • Runtime (src/runtime): shared contracts and helpers (principals, worker and Harness, Durable registry, extension switches, input entries, settings, logs, fetch).
  • Plugins: chat, chat-mode, sessions, subagents, agent (one-shot agents), codemode, coding-tools, prompt, models, login, search, status, drafts, files, send-file, artifacts, workspace, file-index, terminals, envs, mcp, ports, radius, sharing, devices, proxy-marker, logs, dev-tools, powershell, web-shell. Before writing one, read Chord's tutorial (packages/chord/docs/tutorial/) and plugin guide (packages/chord/docs/plugin-authoring.md), then an existing p2 plugin of the same shape.
  • Locations: every conversation works at { env, cwd }: the main one at the session's, subagents spawned with a cwd at their own (on the caller's environment), the others following the session. A session's location changes in place: relocating an idle running worker fences it, updates the catalog and moves the main conversation and its followers without a restart; subagents with their own location stay. Plugins resolve a conversation's location and environment through Harness.environments, the session's through WorkerProcess.location (docs/p2-notes.md, Agent locations and Relocation).
  • Durable (packages/durable) runs the agent: conversations, inputs as custom entries (p2.input, chat.mention), display-only writes placed immediately, code mode (QuickJS sandbox; scripts call tools as tools.<name>, one-shot agents via tools.agent), compaction with visible failures, deferred tools and tool_search (MCP tools are deferred).

The TUI

npm run tui [url] (Node ≥ 26.5.1). The launcher owns the terminal, the connection and sign-in; all TUI code (pi-tui and plugin facets) comes from the server as one bundle and runs in a worker that drops every Node permission first. Output passes a strict escape-sequence allowlist; privileged actions (copy, open URL, download, external editor, attach, saving preferences, sign-out) need launcher grants, mostly with a launcher-drawn consent screen. Preferences (keybindings, theme, display settings) are the visitor's own (~/.pi/agent, ~/.pi/p2-tui/prefs.json). Threat model: DEV.md and README.md (TUI isolation). Design and as-built notes: docs/p2-tui-port.md.

Rules

From AGENTS.md and this project's history:

  • One tree. p2, the packages, root config and scripts change together in earendil-works/pi. After p2 changes run npm run check both at the root and in apps/p2.
  • Commit only verified states. When several agents edit one tree, copy an agent's files into a separate worktree on top of HEAD, run npm run check and the full npm test there, and commit exactly that content (git hash-object -w and git update-index). A browser test that times out under load and passes on its own is acceptable; anything else is not. Lockfile commits need PI_ALLOW_LOCKFILE_CHANGE=1; npm 11 strips libc fields, revert that noise. Never --no-verify.
  • Deploy only pushed commits (~/p2.mariozechner.at/deploy.sh on slayer).
  • Port from the diff. Changes copied from elsewhere start from the commit (git show <hash>), never from a summary; briefs to agents name the commit and the files.
  • No hand tests on real sessions. Use throwaway sessions under /tmp, or a separate server on another port with a temporary data dir. Never print cookies, tokens or share links.
  • Check pwd before working in a subagent, and spawn subagents with an explicit cwd.
  • pkill -f matches its own command line. Kill by PID or by /proc/<pid>/cwd instead (it once killed the live tmux session).
  • TypeScript: no any, erasable syntax only, top-level imports only (except the plugin loader's dynamic import). Use Opus-class models for documentation and tutorials.

Open work

  1. The phone's daemon binary: p2's file index uses the pi-env daemon's listFiles when the packaged daemon has it, else the old git/walk index. slayer has a fresh build in packages/env/bin/pi-env-linux-x64 (static musl; built with RUSTFLAGS="-C linker=rust-lld -C link-self-contained=yes" cargo build --release --locked --target x86_64-unknown-linux-musl in packages/env/daemon). On the phone: cargo build --release --locked there and copy it to packages/env/bin/pi-env-android-arm64/pi-env. The next pi-env release ships all targets through CI.
  2. TUI: launcher images, a filtered terminal relay, a "trust like SSH" mode, web tool cards (docs/p2-tui-port.md §10). Fork from subagent conversations; fork/clone, shell and templates have no web UI yet beyond rendering.
  3. Cleanup leftovers: two HTTP helpers were not merged because their semantics differ (byte ranges: files serves bytes=- whole, workspace answers 416; body readers: sharing answers 400 past the limit, devices 413); decide one behavior each.
  4. MCP: plugins/mcp (WP6 and WP7 of docs/mcp-durable.md: owner mcp.json, host stdio and HTTP, deferred tools, tool_search, servers on environments, project .pi/mcp.json); OAuth (WP8) and code mode namespaces (WP9) are open.
  5. npm: deprecate @earendil-works/pi-client, pi-protocol and pi-server (1.1.0), which no longer exist.
  6. docs/p2-notes.md TODO: the TypeScript 5.9.3 pin (Termux), an isolating plugin loader, sandboxing third-party facets.
  7. Slow test suite: about 11 minutes serially; files could run in parallel once their isolation is fixed. The chat-mode long-press reply browser test times out under load.
  8. After the Chord v2 port: the TUI on Chord's sandbox (C2); no per-link call limit any more (owner decision; Chord v2 has none). Chord v1 and the old-page shim are deleted (C1).

Documents

File Content
README.md, DEV.md running, deployment behind a proxy, TUI sign-in and isolation
docs/p2-notes.md decisions, configuration, plugin-by-plugin notes
docs/p2-tui-port.md TUI design, as-built notes, open phases
docs/workflows.md code mode workflows with one-shot agents
docs/remote-envs.md SSH environments
packages/chord/docs/ Chord v2's design, slices and guides; tutorial/ for writing plugins