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-wireowns protocol versioning, canonical envelopes, frame limits, Noise XX/IK handshakes, encryption, replay protection, and the UniFFI API consumed by the private iOS client.pix-coreowns 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-cliexposesserve, 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 LaunchAgentWindows 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.sockaccepts one-line commands (approve,reject,devices,sessions,refresh,pair-remote, and lifecycle commands).run/host-events.sockstreams transient JSONL service events and retains no history.run/host-service.jsonis 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
PiTuiowner 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 inimages[], while the agent-compatible paths are appended to the prompt for filesystem-aware workflows. History clients that declareimage_refs.v1receiveimageRefentries 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 PiImageContentremains the durable source of truth. - Session snapshots return Pi state and messages first. Clients declaring
session_metadata.v1receive commands, usage, and thinking-level choices in a later unsolicitedsession.metadataevent. 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.v1receive only a recent, byte-bounded history window insession.snapshot. Older messages are read incrementally from Pi's native JSONL source through opaque-cursorsession.history.request/session.history.pageexchanges. 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-freeSessionHistoryIndex(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.v1receive the same window as contiguoushistory_itemskeyed 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 receivemessagesand the v1 cursor shape. - Clients declaring
history_presentation.v1receive a small presentation envelope insidehistory. 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.