Pix

Pix Host architecture

Understand the host process, transport layers, and security boundaries that make up Pix.

Pix Host is a small Rust process that owns workspace authorization, paired device records, secure connections, Pi child processes, and Pi RPC sessions. Pi's native JSONL session remains the only durable conversation source of truth; Host does not maintain a message database.

Crates

  • pix-wire owns protocol versioning, canonical envelopes, frame limits, Noise XX/IK handshakes, encryption, replay protection, and the UniFFI API consumed by the private iOS client.
  • pix-core owns workspace boundaries, pairing, Bonjour/direct TCP, relay connections, Pi process lifecycle, RPC adaptation, the one-writer invariant, and the optional host-local TUI ownership harness.
  • pix-cli exposes serve, diagnostics, workspace management, pairing, and service operations on supported hosts.

Host service lifecycle

pix-cli/src/service/ is the platform boundary for the persistent host:

service/
├── mod.rs       shared CLI contract and lifecycle dispatch
├── linux.rs     systemd --user unit
└── macos.rs     per-user launchd LaunchAgent

Windows service integration is intentionally not included yet; the shared contract leaves room for a future windows.rs adapter.

Both managers launch the same pix serve --service process. The CLI and the macOS menu app never start a competing foreground daemon; they attach through the mode-0600 sockets derived from the selected configuration path:

  • run/host-service.sock accepts one-line commands (approve, reject, devices, sessions, refresh, pair-remote, and lifecycle commands).
  • run/host-events.sock streams transient JSONL service events and retains no history.
  • run/host-service.json is a liveness record containing only process and listener supervision fields.

This shared service instance keeps Bonjour ownership, pairing state, and encrypted transport stable while pix device pair or the menu app performs approval.

CLI ownership on macOS

The release App bundle is the canonical Pix CLI distribution. The Homebrew Cask exposes Pix.app/Contents/Resources/pix in PATH; it does not install a second implementation. The macOS app resolves that embedded binary by default, while PIX_CLI is an explicit development override.

The per-user LaunchAgent records its CLI owner in the configuration's service-owner.json. service start, stop, restart, and status operate on the installed owner without silently replacing it. service install only replaces a different owner when --adopt is supplied; the App uses that flag when it is explicitly launched so the service returns to its matching embedded CLI. The service manager exposes one per-user Pix service identity; a standalone CLI may control that existing service, but an independent CLI-only daemon must use a separate service identity rather than competing with the App-managed host.

Transport

LAN direct TCP and the outbound WebSocket relay carry the same encrypted wire frames. The relay is content-blind: it authenticates channel roles, forwards opaque binary frames, applies size/rate/connection limits, and stores no application payload.

The optional Pi TUI bridge is a separate host-local NDJSON surface. The Pi-side extension is distributed independently as the @zaincheung/pix package and is installed by Pi's package manager; Pix Host does not copy or replace Pi extensions. Its Unix socket adapter obtains peer UID/PID from the operating system, rechecks the process start identity, and passes only those credentials into the ownership registry; REGISTER payloads never declare an owner PID. TUI owner records share the same session lock as Pix RPC, survive a Host disconnect, and appear to the runtime manager as an unavailable placeholder until the TUI reconnects. After REGISTER, bounded sequenced event frames are mapped through the existing Pi compatibility adapter and can be forwarded to attached Pix clients. The bridge transport itself is not part of pix-wire; its snapshot cursor and partial assistant fields are additive wire data. Once attached, the Host can issue a bounded correlated command subset (prompt, abort, model.list, model.set, thinking.set, and session.rename) over the same local socket; the extension invokes Pi's official API and returns an acceptance result. Steer, follow-up, compact, fork, and shutdown remain intentionally unsupported for TUI owners. The v1 prompt path is text-only; host attachment references are rejected for a TUI owner until an image-content mapping is separately verified.

After a TUI has successfully attached, a socket loss starts bounded background reconnect attempts (1s, 2s, 5s, 10s, then a 30s cap). A session that was standalone because Host was reachable but its first JSONL session file did not exist gets one bounded claim retry after Pi settles the first agent run and persists that file. A session that was standalone because the Host was absent is not late-bound automatically; the user can start the Host and use /reload when they explicitly want to attach.

Session replacement has an explicit lifecycle boundary. Before Pi handles a /resume, the extension sends a bounded preclaim containing only the target session file. Host validates that the file is a discovered session in the current authorized workspace and, when it is free, holds the normal PiTui lease for at most five seconds. The following REGISTER consumes that same-owner reservation; an occupied target cancels the switch, while an unreachable Host fails open so Pi remains usable standalone. /new, /fork, /quit, and signal-driven shutdown emit a session_release marker before the socket closes; extension reload deliberately preserves the lease so the same Pi process can reconnect without opening a writer gap.

Apple boundary

The public macOS client owns menu-bar/settings UI, native folder pickers, Keychain integration, and the local Host service bridge. The private iOS client owns its SwiftUI presentation, native sockets, Keychain integration, and disposable view state. Both clients must use the Rust pix-wire implementation and must not reimplement cryptography, framing, or durable session storage.

Security invariants

  • Only explicitly authorized canonical workspace roots are usable.
  • A Pi session has one writer process at a time.
  • A live PiTui owner blocks a Pix RPC spawn even when its bridge socket is temporarily unreachable; TUI owners do not consume the Pix RPC process capacity budget.
  • Relay loss changes reachability only; Pi continues locally.
  • Logs are payload-free and never contain prompts, files, model output, keys, tokens, or relay secrets.
  • Wire extensions are capability-gated per connection: a host never emits a gated event or field to a client that did not declare it (protocol/schema/v1.md).
  • Image attachments are staged in bounded connection memory, then persisted atomically below the Pix configuration directory as a session-scoped attachments/v1/<session>/<attachment-key>/ asset (client attachment ID for new uploads, vision hash for recovered history). The source, agent-compatible, and vision paths are explicit; decodable images get a <=2000×2000 vision derivative and malformed/unsupported bytes remain a byte-for-byte fallback. Pi still receives the vision bytes in images[], while the agent-compatible paths are appended to the prompt for filesystem-aware workflows. History clients that declare image_refs.v1 receive imageRef entries and fetch bounded chunks lazily. Image metadata records the original pixel dimensions so a client can reserve single-image geometry before the lazy fetch; raw Pi ImageContent remains the durable source of truth.
  • Session snapshots return Pi state and messages first. Clients declaring session_metadata.v1 receive commands, usage, and thinking-level choices in a later unsolicited session.metadata event. Legacy clients keep the fields in the snapshot, but each optional probe has a short best-effort deadline so it cannot hold the connection indefinitely.
  • Clients declaring session_history.v1 receive only a recent, byte-bounded history window in session.snapshot. Older messages are read incrementally from Pi's native JSONL source through opaque-cursor session.history.request/session.history.page exchanges. The cursor fixes a revision boundary so live Pi events can continue independently while a reader pages toward the beginning; the 1 MiB encrypted-frame limit remains a frame limit, not a session-size limit. Host keeps only an ephemeral, content-free SessionHistoryIndex (message ordinals, byte anchors, sparse checkpoints, a committed JSONL fence, and an epoch/fingerprint); Pi JSONL is still the only durable source of message content.
  • Clients declaring history_items.v1 receive the same window as contiguous history_items keyed by stable source indexes. A message that is too large or not renderable as one bounded wire item becomes a semantic placeholder with a capped preview and original byte count, so a large middle record cannot hide the final user/assistant exchange. Legacy clients continue to receive messages and the v1 cursor shape.
  • Clients declaring history_presentation.v1 receive a small presentation envelope inside history. It identifies the final user message, the final terminal assistant result, the active/completed/failed/aborted/compacted Turn state, and counts for omitted thought/tool/error process records. Previews are included only when the corresponding anchor is outside the current page. For an idle or sleeping session, clients may collapse settled process rows by default; an active session keeps the current Turn's process visible while it is running.
  • The initial page is selected against the last committed complete JSONL record. A partial tail is excluded. Older-page requests use a reverse byte-range reader and an opaque v2 cursor containing the source-index bound, byte bound, epoch, and boundary fingerprint. A rewrite or truncation makes the cursor fail closed and requires a fresh attach; an append preserves the cursor's fixed boundary.

Pi RPC coverage

docs/PI_RPC_COVERAGE.md tracks which Pi RPC commands and events are exposed, capability-gated, or intentionally omitted. Pi-specific field names stop at pix-core/src/pi_bridge.rs.

On this page