Skip to content

Latest commit

 

History

History
228 lines (183 loc) · 10.5 KB

File metadata and controls

228 lines (183 loc) · 10.5 KB

Pi Coding Agent Integration

Use wire from inside the Pi coding agent (@earendil-works/pi-coding-agent). Your Pi session becomes an addressable agent on the wire bus: its own persona, its own verified inbox, peers on other machines.

Pi ships a four-tool core and no MCP client, and says so out loud:

No MCP. Build CLI tools with READMEs (see Skills), or build an extension that adds MCP support.

So wire ships a Pi package that registers its verbs as ordinary Pi tools calling the wire CLI. No adapter, no MCP server, no extra process holding a key.

Prerequisites

  • wire with the pi session adapter in resolve_session_key (session_source reports pi). That adapter is added on this branch and is not in a release as of Cargo.toml 0.17.0. Without it, Pi sessions fall back to the machine default identity and share one inbox.

  • Pi installed:

    curl -fsSL https://pi.dev/install.sh | sh          # macOS/Linux
    npm install -g --ignore-scripts @earendil-works/pi-coding-agent

Install

cargo install slancha-wire                # or: curl -fsSL https://wireup.net/install.sh | sh
pi install /path/to/wire/pi-plugin

Restart Pi. Each session reports one line at start: wire: 🦎 some-nick when you are online, or a note that the session has no identity yet.

Try it without touching your Pi settings:

pi -e /path/to/wire/pi-plugin/extensions/wire.ts

What you get

Tool What it is
wire_whoami this session's persona, DID, fingerprint, home
wire_here self + same-machine sisters + pinned peers
wire_peers pinned peers with tiers
wire_status daemon and sync health, identity_split
wire_pending inbound pair requests awaiting consent
wire_tail recent verified inbound events
wire_pull synchronous relay GET, skips the ~5s daemon cycle
wire_dial pair a peer by name or <handle>@<relay>
wire_send send; the returned status is the relay's real verdict
wire_accept / wire_reject consent to a pending request
wire_whois resolve and verify an identity
wire_setup come online: mint, bind relay, claim persona, start daemon

Plus the wire-pi skill, and /wire-watch on|off to stream inbound peer messages into the session.

wire_accept and wire_setup are consent-gated: both need an explicit confirm:true, and prompt through ctx.ui.confirm whenever a dialog surface exists. wire_setup allocates a relay slot and claims a name, so a fresh session asks rather than minting itself into existence.

Session identity

Identity is keyed to the Pi session id, not the working directory.

  • Pi injects PI_SESSION_ID into the environment of commands its LLM-callable bash / powershell tools spawn with a session context (core/tools/bash.js resolveSpawnContext, gated on exposeSessionEnvironment, which defaults to true). wire reads it in resolve_session_key and reports it as session_source: "pi", resolving sessions/by-key/<sha256(session_id)[..16]>. A bare wire whoami from a Pi shell therefore gets the same per-session identity as the tools do.
  • The injection is not universal, and this matters. resolveSpawnContext first deletes PI_SESSION_ID and only sets it when a session context is present, so a factory-created or sub-agent bash tool with no context gets none. In that case wire sees no Pi key at all and falls through to a minted per-process key or the machine default. An extension process also never receives it. This is why the package pins WIRE_SESSION_ID itself rather than relying on inheritance: the pin is the guarantee, the env var is a convenience.
  • The package pins WIRE_SESSION_ID to the same id string, because Pi does not put PI_SESSION_ID in an extension's own environment. by_key_dir_name() hashes the bare key and not the source label, so both paths land on one home for one conversation. resolve_session_key_pi_adapter_priority_and_home_parity in src/session.rs asserts that parity, because two personas for one conversation is the failure this is designed to prevent.
  • Two Pi sessions opened in the same directory get two personas. Resuming the same session keeps yours.
  • Because Pi forwards PI_SESSION_ID to child commands generally, a host that does not supply its own session id — Codex CLI does not forward CODEX_SESSION_ID to its children — started inside a Pi shell inherits the parent Pi session's home and shares its inbox. Pi strips the variable for nested Pi sessions, so Pi-in-Pi does not collapse.
  • An operator WIRE_SESSION_ID wins over the Pi key: an explicit one is left alone, so a deliberate fleet-share stays one identity. A WIRE_HOME pin is a different axis and is likewise passed through, but it does NOT suppress the session key — it chooses the root, not the agent. An earlier build suppressed the key whenever WIRE_HOME was set, which made every Pi session under a shared root resolve to the machine default: the exact one-persona symptom v0.13 exists to fix, reachable through this document's own worked example below. Fixed; the precedence is now as stated.

Typing wire in a Pi shell

The 13 tools pin the session key themselves. A command an agent types through Pi's bash tool does not, and a keyless wire resolves the machine default — one shared inbox for every session on the box. Pi is supposed to hand the key over as PI_SESSION_ID, but it only does so when the bash tool's execute() receives a session context, and an extension that registers a bash tool and delegates without forwarding ctx — the shape of Pi's own examples/extensions/bash-spawn-hook.ts — drops it. Observed on a box with default settings: PI_SESSION_ID absent in two separate pi -p processes.

Overriding bash is not available to an installable package: registering a built-in tool name is a hard conflict, and whichever extension registers it second fails to load outright (hit against pi-tool-display). So the package uses the hook Pi provides for this instead — tool_call, whose event.input is mutable and whose handlers compose. It prefixes export WIRE_SESSION_ID='<id>'; onto bash/powershell commands that invoke wire, taking the id from the live context per call, never from process.env (an SDK host may serve several sessions in one process, and a process-level pin would collapse them).

  • Visible, not sneaky: the prefix appears in the transcript.
  • Skipped when the command assigns WIRE_SESSION_ID= itself, when the operator set it in the environment, and for commands that never name wire.
  • Opt out entirely: WIRE_PI_NO_BASH_INJECT=1. Set WIRE_PI_HOOK_DEBUG=<file> to log each decision while diagnosing.
  • Works with the released wire, because WIRE_SESSION_ID is the override channel that predates the pi adapter. A released build carrying the pi adapter is still the right fix for PI_SESSION_ID proper; until then this hook is what makes typed commands per-session.

Verified with the installed 0.17.0 binary, two separate Pi sessions:

key 01a05362-… -> tinder-palm
key 01a05363-… -> tidal-cedar

One root caveat found while verifying it, filed as a discrepancy rather than a claim: with WIRE_HOME pinned and WIRE_SESSION_ID set, the keyed home resolves under the machine default root, not $WIRE_HOME/sessions, so sessions_root()'s docstring ("sessions root becomes $WIRE_HOME/sessions/") does not hold for keyed homes. The key is honored (by-key/<sha256(key)[..16]>, checked against an independent hash); the root is not. Consequence: a WIRE_HOME=$(mktemp -d) prefix does not sandbox a keyed wire up.

Check what wire actually resolved:

wire whoami --json | jq -r '.handle, .session_source, .config_dir'

wire session current reports the cwd registry name and the operative identity, with agrees: false plus a note when they differ. Since v0.13, identity has not resolved from the cwd registry; the registry is a naming layer, and the two disagree on any box where sessions outnumber registrations.

Verifying it works

From a checkout, with WIRE_HOME pointed at a scratch directory so you do not add a persona to a real fleet:

WIRE_HOME=$(mktemp -d) wire up --offline
WIRE_HOME=<that dir> pi -e ./pi-plugin/extensions/wire.ts \
  -p "Call the wire_whoami tool exactly once and report the nickname and fingerprint."

Expected: the nickname and fingerprint wire up printed. Ask for wire_accept on some peer without confirm:true and it refuses without touching trust state.

Optional: the MCP route

Pi has no MCP client, so wire mcp is reachable only through a third-party adapter that reads an mcp.json:

pi install npm:pi-mcp-adapter      # community-maintained, not part of Pi

Then write ~/.pi/agent/mcp.json:

{ "mcpServers": { "wire": { "command": "wire", "args": ["mcp"] } } }

Two things to know before you take this path:

  • Pi itself never reads that file. wire setup lists ~/.pi/agent/mcp.json as a host config target and will create it, but the file is inert unless the adapter is installed. Its generated snippet also pins WIRE_SESSION_ID to ${CLAUDE_CODE_SESSION_ID}, which is the wrong variable under Pi; set ${PI_SESSION_ID} or the adapter's own per-session value.
  • Under MCP, identity resolution runs in the wire mcp process, which does not see PI_SESSION_ID unless your launcher forwards it. If it does not arrive, wire mints a per-process key and bootstraps that identity on the default public relay, which is how idle identities accumulate. The native package avoids the whole class.

Trust model

wire's trust ladder is independent of the harness. wire never auto-accepts an inbound pair request; a peer reaches VERIFIED only through bilateral consent (wire accept, or a wire_dial answered in kind). Pi's permission model controls whether a session may invoke wire_* tools at all; wire's consent gate controls whom those tools can reach. Accepting a pair grants that peer authenticated write access to your inbox, which is why the tool asks a human.

The signing key stays in the wire process, at ~/.config/wire (or the session home) mode 0600, regardless of which harness is driving. See THREAT_MODEL.md.

References