mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
SessionMode gains 'pi', a first-class backend alongside Claude Code, OpenCode, Codex, Gemini and Antigravity: its own PTY, tmux session, rose tab identity, welcome button, run-mode entry, cron agentType, Docker and remote-SSH command defaults, and clone-repo Brain option. Pi is a different shape of CLI from the other four, and three decisions follow from that: - It has NO permission prompts and no sandbox, so there is no --dangerously-skip-permissions analog and none was invented. The privilege-shaped knob is the tri-state approveProjectTrust, which makes pi load and EXECUTE repo-local .pi/extensions TypeScript and install missing project packages. clampExternalCliBypassForOwner() therefore puts pi in the MATERIALIZE branch: a non-granted multi-user owner gets --no-approve even when no config was sent, because pi's own default is a prompt the session user could answer themselves. That helper had zero test coverage; it now has coverage for all four CLIs. - Only the PI_ prefix joins the env allowlist. Pi's ~34 provider key vars share no prefix and ALLOWED_ENV_PREFIXES is one global list with no mode context, so admitting them would widen the allowlist for every mode at once. Auth goes through pi's /login or the server's own environment. --api-key is deliberately never wired: it would put a provider secret on the spawn command line. - pi stays OUT of isAltScreenStripMode(). Its default TUI renders into the main screen with terminal-owned scrollback, and its 0.84.0 fullscreen mode is runtime-switchable via /settings; that flip was measured to put the pane into the alt screen, which the strip would have corrupted. pi-cli-resolver.ts additionally sanity-probes `pi --version` and requires semver-shaped output, because `pi` is a short generic name a stray binary can shadow; GET /api/pi/status surfaces path and version so a misresolution is diagnosable rather than presenting as a broken mode. Docker installs pi in its own --ignore-scripts step so that flag cannot affect the other four CLIs, and seeds its credentials per-file rather than whole-dir (~/.pi/agent also holds sessions, extensions and package trees). Verified end to end against pi 0.84.1 on an isolated instance: resolver search-dir fallback, flag construction, piConfig persistence across a full server restart, the trust prompt and its --no-approve suppression, the rose Run button on the default daylight-blue skin (the nested skin block eats per-mode gradients unless the rule lives inside it), and the buffer local-echo policy, which pi tolerates where codex did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# Pi (pi.dev) sessions
|
||
|
||
Codeman can drive [Pi](https://pi.dev) (`@earendil-works/pi-coding-agent`, MIT) as a
|
||
session backend, alongside Claude Code, OpenCode, Codex, Gemini and Antigravity.
|
||
`pi` is a sixth **run mode**: its own PTY, its own tmux session, its own tab colour
|
||
(rose). It is not a location overlay like Docker or remote-SSH cases, and it is not
|
||
a web tab.
|
||
|
||
Tracking issue: [#206](https://github.com/Ark0N/Codeman/issues/206). The design
|
||
rationale behind each decision below lives in `docs/pi-integration-plan.md`.
|
||
|
||
## Install
|
||
|
||
```bash
|
||
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
||
# or
|
||
curl -fsSL https://pi.dev/install.sh | sh
|
||
```
|
||
|
||
Both installers end up going through global npm, so either one uninstalls with
|
||
`npm uninstall -g @earendil-works/pi-coding-agent`.
|
||
|
||
Codeman finds the binary via `which pi` and then the usual global-bin locations
|
||
(`~/.local/bin`, `/usr/local/bin`, `~/.bun/bin`, `~/.npm-global/bin`, `~/bin`).
|
||
|
||
**`pi` is a short, generic name**, so unlike the other CLI resolvers Codeman does
|
||
not trust a `which` hit on its own: it runs `pi --version` once and requires
|
||
semver-shaped output. Anything else is rejected as "not installed" and the
|
||
rejected path is logged. Check what it resolved:
|
||
|
||
```bash
|
||
curl -s localhost:3000/api/pi/status | jq
|
||
# { "available": true, "path": "/home/you/.local/bin", "version": "0.84.1" }
|
||
```
|
||
|
||
That endpoint carries `version` on top of the shape the sibling `/api/*/status`
|
||
endpoints return, precisely so a misresolution is visible rather than presenting
|
||
as "the mode just doesn't work".
|
||
|
||
## Authenticate
|
||
|
||
Pi supports 15+ providers. Two ways in:
|
||
|
||
- **OAuth subscription login** — run `/login` inside a pi session. Six providers
|
||
support it: ChatGPT Plus/Pro, Claude Pro/Max, GitHub Copilot, xAI, OpenRouter
|
||
and Radius. Credentials land in `~/.pi/agent/auth.json` and pi refreshes them
|
||
itself. OpenRouter's flow accepts a pasted redirect URL, which is what makes it
|
||
workable over remote SSH.
|
||
- **API keys** — exported in the environment of the **Codeman server process**.
|
||
|
||
⚠️ **Provider API keys cannot be sent as per-session `envOverrides`.** Pi reads
|
||
about 34 provider variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
||
`DEEPSEEK_API_KEY`, `HF_TOKEN`, `BASETEN_API_KEY`, …) that share no common prefix.
|
||
Codeman's env allowlist is a single global list applied to every mode at once, so
|
||
admitting bare provider keys for pi would widen the allowlist for Claude, Codex,
|
||
Gemini and everything else too. Only the **`PI_*`** prefix was added, which covers
|
||
every documented pi input: `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`,
|
||
`PI_PACKAGE_DIR`, `PI_OFFLINE`, `PI_SKIP_VERSION_CHECK`, `PI_TELEMETRY`,
|
||
`PI_CACHE_RETENTION`, `PI_SHARE_VIEWER_URL`, `PI_HARDWARE_CURSOR`,
|
||
`PI_EXPERIMENTAL`.
|
||
|
||
`pi auth check` verifies credentials before you start a long run.
|
||
|
||
Note if you authenticate with a Claude Pro/Max subscription: third-party harness
|
||
usage bills as Anthropic "extra usage" per token rather than against plan limits.
|
||
|
||
## What Codeman wires up
|
||
|
||
`PiConfig` (per session, persisted in `state.json`, round-trips through respawn):
|
||
|
||
| Field | Flag | Notes |
|
||
| --------------------- | -------------------------------------- | ---------------------------------------------------------------- |
|
||
| `model` | `--model <v>` | Accepts `provider/id` and a `:<thinking>` suffix (`sonnet:high`) |
|
||
| `provider` | `--provider <v>` | `anthropic`, `openai`, `google`, … |
|
||
| `thinking` | `--thinking <v>` | `off`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max` |
|
||
| `continueSession` | `-c` | Skipped when `resumeSessionId` is set (the two conflict) |
|
||
| `resumeSessionId` | `--session <v>` | Ids only, never paths |
|
||
| `approveProjectTrust` | `--approve` / `--no-approve` / nothing | Tri-state, see below |
|
||
|
||
Every value is regex-validated and **dropped** (not escaped) if it fails, because
|
||
the result is interpolated into the pane's `bash -c "…"` command.
|
||
|
||
The Run button sends **no `PiConfig` at all**: pi has no permission prompts to
|
||
bypass, and project trust is a decision the person at the terminal makes.
|
||
|
||
## What Codeman deliberately does NOT wire up
|
||
|
||
- **`--api-key`.** Never. It would put a provider secret on the spawn command
|
||
line, visible in `ps`, tmux server state and logs. `PI_*` overrides go through
|
||
socket-scoped `tmux setenv` for exactly this reason.
|
||
- **`--tui-mode`.** Pi's default main-screen TUI is the friendly case for a
|
||
browser terminal. The fullscreen mode (0.84.0) stays your own runtime choice via
|
||
`/settings`.
|
||
- **`--name`, `--no-session`, `-p`/`--print`, `--mode json`, `--mode rpc`,
|
||
`--tools`/`--exclude-tools`, `-e`/`--extension`, `--skill`,
|
||
`--system-prompt`.** Tracked as follow-ups in the plan doc.
|
||
|
||
## Permission and trust model — read this
|
||
|
||
**Pi has no permission prompts and no sandbox.** There is no
|
||
`--dangerously-skip-permissions` analog and none is needed: tools run with the
|
||
user's own permissions, always. A pi session can read, write and execute anything
|
||
the Codeman user can. If you need isolation, use a **Docker case** — that is the
|
||
isolation story, here as everywhere else in Codeman.
|
||
|
||
Pi's "project trust" prompt is **not** a safety boundary (upstream says so too).
|
||
It gates *loading* repo-local `.pi/` config, extensions and skills, and
|
||
*installing* missing project packages. It only appears when the cwd or an ancestor
|
||
contains `.pi/settings.json`, `.pi/extensions|skills|prompts|themes`,
|
||
`.pi/SYSTEM.md`/`.pi/APPEND_SYSTEM.md`, or `.agents/skills`. A bare `.pi/`
|
||
directory does not trigger it.
|
||
|
||
`approveProjectTrust: true` answers it with `--approve`, which means pi **loads
|
||
and executes repository-supplied TypeScript** and runs an npm install for missing
|
||
project packages. Treat it exactly as seriously as that sounds.
|
||
|
||
**Multi-user mode:** for an owner without the privileged-command grant, Codeman
|
||
materializes `approveProjectTrust: false` so the pane launches with
|
||
`--no-approve` and the prompt never appears. Merely *omitting* `--approve` would
|
||
not be a clamp, since pi's own default is to ask and the session user could just
|
||
answer yes.
|
||
|
||
Also worth knowing: `pi auth print-api-key` / `print-bearer-token` and
|
||
`pi auth check` mean a pi session can print its own provider credentials by
|
||
design. Isolation is Docker.
|
||
|
||
## tmux extended keys (Shift+Enter)
|
||
|
||
Pi's editor uses `Shift+Enter` / `Ctrl+Enter` for newline-vs-submit. Without
|
||
extended keys, tmux collapses both into a plain `\r`. Upstream recommends:
|
||
|
||
```tmux
|
||
set -g extended-keys on
|
||
set -g extended-keys-format csi-u
|
||
```
|
||
|
||
`extended-keys-format` needs tmux 3.5+; on 3.2–3.4 `extended-keys on` alone works
|
||
(pi falls back to xterm `modifyOtherKeys`).
|
||
|
||
Codeman's browser input path sends `\r` for submit, so basic use works
|
||
unconfigured — what degrades is newline-in-editor, mostly when you attach to the
|
||
pane directly (`sc`).
|
||
|
||
⚠️ Upstream notes the setting may need a full `tmux kill-server` to take effect.
|
||
**Never run `tmux kill-server` on Codeman's socket** — it would kill every live
|
||
session, `w1`/`w2`/`w3` included.
|
||
|
||
**Measured (tmux 3.4, pi 0.84.1): no `kill-server` is needed.** Setting the option
|
||
server-scoped on Codeman's own socket takes effect on the ALREADY-RUNNING server;
|
||
the next pi session starts without the warning. Existing sessions keep the old
|
||
setting until they respawn.
|
||
|
||
```bash
|
||
tmux -L codeman set -s extended-keys on
|
||
tmux -L codeman set -s extended-keys-format csi-u # tmux 3.5+ only, see below
|
||
tmux -L codeman show-options -s | grep extended # verify
|
||
```
|
||
|
||
On **tmux 3.4 and older, `extended-keys-format` does not exist** and the second
|
||
line fails with `invalid option: extended-keys-format`. That is harmless — pi
|
||
falls back to xterm `modifyOtherKeys` and `extended-keys on` alone silences the
|
||
warning. Run the two lines independently rather than chained.
|
||
|
||
Pi tells you which state it is in: an unconfigured session prints
|
||
`Warning: tmux extended-keys is off. Modified Enter keys may not work.` in its
|
||
startup banner, so you can verify the change by starting a new pi session.
|
||
|
||
⚠️ Use `-L <socket>` and `-s`, never `-g` on your default socket, and never
|
||
`kill-server`. Codeman does not set this for you: it is a server-wide tmux option
|
||
and silently changing key encoding for every session of every backend is not
|
||
Codeman's call to make.
|
||
|
||
## Typing from the browser (local echo)
|
||
|
||
On touch devices Codeman buffers typed characters in the `LocalEchoOverlay` and
|
||
flushes them to the PTY on Enter. Pi gets that `'buffer'` policy, the same as
|
||
Claude, Gemini and OpenCode.
|
||
|
||
This was an explicit open question, because that policy is exactly what broke
|
||
Codex (issues #218/#219/#220/#222): Codex's composer reacts per keystroke, so
|
||
buffer-until-Enter starved it. **Measured against pi 0.84.1: it does not
|
||
reproduce.** Pi's slash-command picker re-filters on the whole composer content
|
||
rather than on per-keystroke deltas, so a one-shot flush of `/set` filters the
|
||
picker down to `settings` identically to typing it character by character, and
|
||
the delayed `\r` then selects it. Prose prompts flush and submit correctly too.
|
||
|
||
If a future pi release changes that, the cheap fallback is one `'off'` branch in
|
||
`_updateLocalEchoState` (terminal-ui.js); teaching `PredictiveEchoAddon` pi's
|
||
composer row is the larger follow-up.
|
||
|
||
## Docker cases
|
||
|
||
The agent image (`docker/agent.Dockerfile`) installs pi in its own `RUN` step with
|
||
`--ignore-scripts`, kept out of the shared npm block so the flag cannot change how
|
||
the other four CLIs install. Rebuild with:
|
||
|
||
```bash
|
||
node scripts/build-agent-image.mjs --no-cache # --no-cache is mandatory
|
||
```
|
||
|
||
Credentials are **seeded**, not shared: `~/.pi/agent/auth.json`, `settings.json`,
|
||
`trust.json`, `models.json` and `models-store.json` are mounted read-only and
|
||
copied into the container's own `~/.pi/agent`. So an in-container pi never writes
|
||
refreshed OAuth tokens back to the host, and `docker commit` exports stay
|
||
secret-free. `models.json` is in the list because it holds user-defined custom
|
||
providers, which would otherwise silently vanish inside containers.
|
||
|
||
Only those five files are seeded because `~/.pi/agent` also holds `sessions/`,
|
||
`extensions/`, `skills/` and the installed package trees (`npm/`, `git/`), which
|
||
on an active host is easily gigabytes.
|
||
|
||
**Trade-off:** in-container pi sessions are invisible host-side, so `pi -c` inside
|
||
a Docker case only sees that container's own history.
|
||
|
||
## Remote SSH cases
|
||
|
||
`pi` mode is routed through an interactive login shell
|
||
(`exec "$SHELL" -i -l -c 'pi'`), because sshd's remote-command PATH does not
|
||
include npm's global bin on most hosts. Per-session config and `envOverrides` do
|
||
not cross ssh and are rejected rather than silently ignored; use the per-host
|
||
command override instead.
|
||
|
||
## Known gaps
|
||
|
||
- **No idle/completion hook.** Pi has no hook system Codeman can install into, so
|
||
idle detection falls back to output-stabilization like the other external CLIs.
|
||
Pi 0.84.0 shipped an `agent_settled` extension event that is a genuine idle
|
||
signal; a Codeman pi extension using it is the highest-value follow-up.
|
||
- **No response viewer.** Pi writes JSONL v3 session files under
|
||
`~/.pi/agent/sessions/`; nothing reads them yet.
|
||
- **Cron jobs mis-detect readiness.** The cron readiness poll looks for `❯` or a
|
||
token count, neither of which pi prints, so a pi cron job burns its poll budget
|
||
and then sends the prompt anyway. It works; it is just slower to start.
|
||
- **Ralph, respawn heuristics, token/CLI-info parsing and the `❯` readiness probe
|
||
are off** for pi, as for every external CLI.
|