Who Gets to Act? The Architecture Behind PI-Desktop
PI-Desktop's central architectural question is not how to display a model response; it is who is allowed to turn that response into a local action. This chapter follows the product's four local process roles and the request path between them, so the reader can judge why a visible session, a model, and a project folder are not by themselves execution authority. The useful result is an ownership map: presentation can remain flexible, while privileged work has to pass through a durable host boundary.
Four roles, four kinds of responsibility
The first role is the React renderer, the part of PI-Desktop the user sees. It presents conversations, project and session navigation, settings, tool activity, permission cards, reviews, and work-panel resources. It is intentionally not the place where the application opens SQLite, reads arbitrary files, or runs shell commands. In the shipped Electron window, context isolation is enabled, Node integration is disabled, and the renderer runs with a sandboxed preload. The preload exposes a narrow bridge: an invocation or event channel is checked against an allowlist before it crosses into the application.
That arrangement is more than a defensive preference. A renderer displays content that may include model output, Markdown, previews, and material from a project. If it also had unrestricted Node or filesystem access, content that reached the page could become a route to the user's machine. By keeping the renderer's authority to typed, allowlisted requests, PI-Desktop makes the boundary explicit. The page can ask the application to perform a known operation; it cannot invent a new native capability by calling a module that was never exposed.
The second role is Electron Main. It owns the desktop shell and acts as an orchestrator. It creates windows, translates renderer requests, supervises the host and sidecar processes, routes events, and coordinates application lifecycle. It can also host the optional local MCP control server. But “orchestrator” is important: Main is not meant to become the place where every domain rule is reimplemented. If the renderer and a local automation client both need to open a session or request a project operation, they should converge on the same registered handler rather than acquire two subtly different authorization paths.
The third role is Rust host-core, the privileged local backend. It owns the operations that can affect the user's environment or durable state: workspace path resolution, built-in tools, permission evaluation, session and transcript persistence, approval records, shell selection, plugin host services, secrets integration, and audit logging. It speaks to Electron Main over a local standard-input/output JSON-RPC channel. The sidecar does not open the database or transcript files directly, and Main does not quietly turn the renderer into a database client.
The fourth role is the Node/TypeScript pi sidecar. It owns the pi agent loop: creating the Agent, resolving the selected provider and model, streaming model output, orchestrating turns, normalizing pi events, and deciding when to ask for a tool. It benefits from the pi ecosystem instead of reimplementing a coding-agent engine. But it does not become the authority over the project. A tool request leaves the sidecar and crosses the host bridge before a file or command is executed.
This division explains the hybrid choice. A pure TypeScript Electron process could be simpler to assemble, but it would put desktop orchestration, model execution, filesystem access, persistence, and policy in a weaker common boundary. Rewriting the pi agent loop in Rust would strengthen one side at the cost of discarding a mature engine and taking on a much larger implementation. PI-Desktop instead keeps pi's model and turn semantics where they belong and gives the host the responsibility for local authority.
The boundary is not a claim that the product has four unrelated applications. The parts form one loop, but each part answers a different question. The renderer asks what the user sees. Main asks how processes and desktop surfaces are coordinated. The sidecar asks what the Agent should do next. Host-core asks whether the requested local operation is allowed, where it may occur, how it is recorded, and what happens when it fails.
The arrows show coordination, not equal authority. The model provider supplies text and structured model events. The sidecar interprets them as part of an Agent turn. Host-core decides whether a local tool call can proceed and records the durable consequences. An external local Agent using MCP enters through Main's reviewed catalog; it does not receive a secret passage to the database or to arbitrary Electron IPC.
One request, followed across the boundary
Suppose a developer asks PI-Desktop to diagnose a failing test and make the smallest appropriate change. The renderer first submits the prompt for a particular session. Electron Main routes it to the sidecar, which loads that session's effective provider, model, operating mode, and project binding. The sidecar starts a pi turn and streams answer or reasoning events back through Main for display.
At some point the model may request a tool. A low-risk inspection might ask to read a file or search the project. A mutation might request an edit, or the model might ask Bash to run a test. The sidecar sends the tool request across the host bridge with the durable session identity. That identity is the important anchor. The host does not simply accept a folder supplied by the current window or trust a mode value repeated by the model.
Host-core resolves the session's project and applies the relevant policy. The current visible project tab is not authoritative for a background session. If the user has switched from repository A to repository B while a session belonging to A is still running, a tool call from that session remains bound to A. This prevents a particularly dangerous class of race: the user changes what they are looking at, an asynchronous event arrives, and a relative path is interpreted against the wrong project.
The host then evaluates the operation. The session's durable operating state, contract policy if applicable, permission mode, path classification, and tool risk all matter. If the call needs user confirmation, the request returns to the originating session as a permission card. The card can show the tool, risk, reason, bounded argument preview, and workspace context. The user can allow the call once, allow that tool for the session, or deny it. If the user does not answer within the configured deadline, the host denies rather than leaving the Agent suspended forever.
Only after these checks does the host execute the tool. The result comes back in a structured form. A failed command is not silently converted into success; the model receives the error information and can decide whether the diagnosis changes. A successful workspace edit can carry review evidence that identifies what changed and, where possible, preserves a guarded path back to the previous bytes. The resulting tool row and assistant message are then persisted as part of the session's record.
This ordering matters because no single layer has enough context to be the sole authority. The model knows the task but is not a trusted filesystem policy engine. The renderer knows what the user is viewing but can be stale during an asynchronous operation. Electron knows process state but should not be the second owner of durable policy. Host-core can resolve the session, path, and policy at the moment of execution. It is therefore the place where the request becomes either an allowed action or a structured refusal.
The same principle applies to mode changes. A renderer can display a selected mode, and the sidecar can carry diagnostic configuration, but a conflicting field cannot turn a Plan session into an Agent session at the host boundary. The host reads the durable session state and applies the policy associated with it. This does not eliminate the need for correct renderer behavior; it means a renderer bug is less able to become a privilege escalation.
Why supervision is part of the architecture
A process boundary is useful only if failure behavior respects it. PI-Desktop's boot order starts Electron Main, loads locale defaults, starts host-core, completes a host handshake, starts the pi sidecar, connects the sidecar's tool bridge, and then creates the renderer. If the host cannot start, the application reports a backend failure rather than letting the UI pretend that local actions are available.
The processes are supervised independently. A host or sidecar exit rejects in-flight calls and moves the application into a degraded or restarting state rather than allowing a request to wait until an arbitrary timeout. Active work is failed closed. The recovery model favors interruption over guessing what an older process intended to do: pending approvals and already queued or running contract executions are not silently replayed after a restart. A new process must re-establish the current state before new work proceeds.
The persistence outbox adds another useful separation. Electron Main can temporarily hold host-owned appends while the host is unavailable, then flush them after a successful handshake. That does not make Main the owner of the transcript; it is a delivery buffer for host operations. The system also knows that a transcript file may exist even when an index row was not committed, so recovery can restore the missing session record or rebuild derived state rather than treating a transport failure as proof that the user's content never existed.
This is a broader lesson about local agent products. “The model is local” or “the app is a desktop application” says little about reliability by itself. The meaningful questions are which process owns state, which process can authorize an action, and what each process does when another disappears. In PI-Desktop, those questions are written into the boot order, host RPC, supervision, and shutdown paths.
Reuse without widening the authority surface
The optional local MCP control plane illustrates the same architecture from another direction. When explicitly enabled, it exposes a reviewed set of project, session, Agent, workspace, and selected desktop operations to an external local MCP client. It binds to loopback, uses a bearer token stored in the application-data directory, validates browser origins where present, and limits the request and result surface. It does not automatically expose every new IPC handler.
Most importantly, the control plane delegates calls to the existing Main-process handlers. A local Agent that asks to inspect a session should pass through the same validation and lifecycle logic as the renderer. A mutation should produce the same session-change convergence rather than a parallel state that the visible desktop cannot see. This reuse reduces hidden coupling: there is one place to interpret a project operation, one place to route it to host-core, and one set of persistence and audit rules.
That reuse is not the same as harmlessness. The endpoint gives a local client the running desktop's authority for the operations it exposes. The token is not an approval request, and confirm: true is an acknowledgement from the calling Agent, not a dialog shown to the user. A process that can read the application's data directory may be able to obtain the token. The feature is therefore local automation for a trusted environment, not a multi-user remote-control service. The repository keeps the broader Gateway and WebUI design outside the current scope.
The boundary that remains
The architecture creates a strong application-level separation, but it does not create a complete operating-system sandbox. Bash and its child processes run with the user's OS privileges. The host can enforce workspace containment, permission decisions, shell identity, timeouts, output bounds, and process-tree shutdown, but it cannot turn a command the user deliberately authorized into a process with no operating-system power.
That distinction should shape how the product is evaluated. The renderer is not supposed to have direct Node or filesystem access. The sidecar is not supposed to mutate the project without host mediation. The host is supposed to reject paths and tools that violate the durable policy. Those are meaningful boundaries. They are different from claiming that a malicious command, a fully trusted local plugin, or a compromised same-user process has no access beyond the project.
PI-Desktop's architecture is therefore best understood as an authority map with controlled crossings. The UI remains flexible because it asks for named operations. The Agent remains useful because it can run a mature loop and reach local tools. The host remains accountable because it resolves durable identity, policy, path, persistence, and failure at the point where a request would have real effects. Once that map is clear, the next question is no longer abstract: what exactly does the host inspect before allowing a request to become a file change or command?