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

178 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](#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`):
```sh
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
```sh
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 |