Skip to content

harnesses: add Pi - #334

Open
kajogo777 wants to merge 7 commits into
harnesses-experimentalfrom
harnesses-pi
Open

kajogo777 wants to merge 7 commits into
harnesses-experimentalfrom
harnesses-pi

Conversation

@kajogo777

Copy link
Copy Markdown
Collaborator

Stacked on #318. Adds Pi as a third harness: harnesses.pi(), over pi --mode rpc.

What it is

  • One pi --mode rpc process per conversation. The SDK mints the id (--session-id), so pi --session <id> opens the same conversation in pi's own TUI.
  • Same surface as claude_code() / codex(): run, stream, steer, stop, resume, fork, history() / sessions() (read from pi's session files), tui(), handoff (history=), the session lock, gateway, MCP servers, images, structured output. pi() takes every claude_code() argument; max_turns raises UnsupportedError.
  • Approvals: pi has no permission prompt, so the SDK loads a small extension (_adapters/pi_extension.js) whose tool_call handler asks the hook through an RPC dialog. Coverage is "all", Allow(input=) rewrites the call, and Deny(reason) reaches the agent. Startup refuses to continue unless the extension proves it loaded, so the gate can't fail open.
  • Gateway: pi's gateway provider sends x-api-key, which sandbox egress injection doesn't rewrite. The extension sends Authorization: Bearer instead. Measured: the gateway accepts it, and a wrong key comes back as a 401.
  • No model given: pi chooses exactly as it does on its own.
  • A pi conversation handed to pi keeps its model. pi restores a session's model from its last reply's author, so handed-off replies keep theirs (provider_metadata["pi"]). Measured: without this, a pushed conversation ran on pi's default model, which the gateway refused.

Also in this PR:

  • afk drives pi: it lists pi conversations and handles push, attach, peek, pull, stop and rm.
  • New example: examples/harnesses/pi.py.
  • Docs, README and skill updated.

Tested

  • Offline: 1243 tests pass, 40 of them new in test_pi_adapter.py. Format, lint, mypy and ty are clean.
  • Live pi suite, local: 92 passed, 3 skipped.
  • Live pi suite, Vercel Sandbox: 86 passed, 1 skipped. Gateway and brokered-credential tests included.
  • afk, for real on pi in a sandbox:
    • push --bg, then wait, peek and pull --files
    • TUI push, Ctrl-] detach, attach
    • stop, then attach (resumes from the snapshot)
    • attach to a --bg one (ends the agent, opens the TUI), then rm
    • The pushed agent remembered the codeword from the handed-off history in each case.
  • Example: harnesses/pi.py passes in run-examples.py.
  • Review: four rounds of Codex /review (gpt-6.1-sol). Every P1 is fixed.

Known gaps

  • A pi started in a terminal isn't shown as running: pi hides its arguments from ps.
  • Sandbox TUI test: pi's ready prompt in a sandbox isn't measured yet, so that check is skipped.
  • Extension-injected context mid-turn is in history() but not in the live session.messages. The SDK has no event for it.
  • A failed retry's partial text stays in session.messages; it is excluded from result.text.
  • Not from pi, seen while testing: RuntimeError: aclose(): asynchronous generator is already running is sometimes printed when a turn is abandoned (afk push --bg). It also happens with codex, so it comes from the shared Turn close path in Add ai.harnesses.experimental and ai.workspaces.experimental #318, not from pi.

@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown

You must have Developer access to commit code to Vercel Labs on Vercel. If you contact an administrator and receive Developer access, commit again to see your changes.

Learn more: https://vercel.com/docs/accounts/team-members-and-roles/access-roles#team-level-roles

Comment thread src/ai/harnesses/experimental/_adapters/pi.py Outdated
Comment thread src/ai/harnesses/experimental/_adapters/pi.py
@kajogo777 kajogo777 added the feature New functionality label Oct 28, 2026
@vercel-security-reviewer

Copy link
Copy Markdown

Security review details

Security review: pi reads a project's sessionDir before trust, so a
repository can commit a session file whose id is a path; resuming it
made the session lock rmtree that path as a stale lock.

- The session lock refuses any id that is not a plain name, in Python
  and again in the helper where the files are; the helper also checks
  on disk that the lock directory is a direct child of its root, so a
  planted symlink cannot lead out.
- pi's session store skips files whose id breaks pi's own id rule, and
  resume/fork/history refuse such an id before the lock is touched.
Follow-up to the pi finding, for all harnesses: Harness.resume, fork,
history and tui check the id where it enters (it may come from a Handle
out of a database), before any adapter turns it into a lock directory,
a transcript path or a CLI argument. The remote Claude store refuses a
key whose parts would leave the store.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant