# How PI-Desktop Achieves UI and Privileged Runtime Separation

> Learn how PI Desktop separates UI and privileged runtime. Discover its three-layer architecture isolating file access, secrets, and native tools in Rust for enhanced security.

- Repository: [Lan/PI-Desktop](https://github.com/vastsa/PI-Desktop)
- Tags: architecture
- Published: 2026-09-11

---

**PI-Desktop implements a strict three-layer architecture that ensures the Electron UI layer never executes privileged operations, with all file system access, secret storage, and native tool execution isolated in a compiled Rust binary.**

The `vastsa/PI-Desktop` repository demonstrates a security-first desktop application pattern where **UI and privileged runtime separation** is enforced through process isolation and strict IPC boundaries. By delegating all sensitive operations to a Rust host-core binary, the application ensures that even a compromised renderer process cannot directly access the operating system.

## The Three-Layer Security Architecture

PI-Desktop explicitly documents that "**UI and privileged runtime are separated**" in its architecture specification at [`docs/spec/02-architecture/01-architecture.md#L34`](https://github.com/vastsa/PI-Desktop/blob/main/docs/spec/02-architecture/01-architecture.md#L34). This separation manifests through three distinct components:

### Renderer Process (Unprivileged UI)

The **Renderer process** executes the React/TypeScript front-end code within Electron's sandboxed browser environment. This layer can only communicate through a narrowly defined IPC allow-list and lacks direct access to the file system, secret stores, or native binaries. According to the README, the interface is "deliberately separated from privileged host capabilities" to prevent unauthorized access even if the UI is exploited.

### Main Process (Coordination Bridge)

The **Main process** acts as a thin, trusted coordinator that forwards validated UI requests to the privileged backend. It launches the Rust host-core as a child process at startup and manages the JSON-RPC communication channel. The main process enforces the security boundary by validating all incoming IPC calls against a hard-coded allow-list before forwarding them.

### Rust Host-Core (Privileged Backend)

The **`pi-desktop-host-core`** binary is the sole component with elevated capabilities. As described in [`docs/spec/03-runtime/05-host-core-rust.md#L5`](https://github.com/vastsa/PI-Desktop/blob/main/docs/spec/03-runtime/05-host-core-rust.md#L5), this Rust binary owns all **privileged runtime** operations including workspace management, encrypted persistence, plugin loading, and native tool execution. No JavaScript code runs with these privileges.

## Enforcement Mechanisms

The boundary between unprivileged UI and privileged runtime is enforced through multiple complementary strategies:

- **Static Allow-Lists**: The renderer can only emit specific commands (such as `AppMenuCommand`) defined in ADR 0021. Arbitrary code execution is prevented by rejecting any method not in the `ALLOWED_HOST_METHODS` array.

- **JSON-RPC Isolation**: All privileged calls travel over a structured JSON-RPC channel to the host-core binary. The main process spawns the Rust binary with controlled stdio pipes, ensuring the UI never holds a direct reference to privileged execution contexts.

- **Binary Separation**: The host-core is a compiled Rust artifact that runs as a separate operating system process, providing hardware-level memory isolation between the V8 JavaScript engine and sensitive system calls.

## Implementation in Code

### Launching the Privileged Binary

The Electron main process spawns the Rust host-core at application startup, establishing the only conduit for privileged operations:

```typescript
// apps/desktop/electron/main/host-process.ts
const hostPath = join(
  process.resourcesPath || "",
  `bin/pi-desktop-host-core${exe}`
);
if (!existsSync(hostPath)) {
  console.error("[host-core] host-core binary not found. Run `cargo build -p host-core` first.");
}
this.child = spawn(hostPath, ["--json-rpc"], { stdio: ["pipe", "pipe", "pipe"] });

```

This initialization ensures the JavaScript side handles only file paths and pipe streams, while all privileged logic executes within the Rust binary.

### IPC Allow-List Validation

The main process enforces method restrictions before forwarding requests to the host-core:

```typescript
// apps/desktop/electron/main/index.ts
ipcMain.handle("host.invoke", async (event, method, params) => {
  // 'method' must be in the hard-coded allow-list; otherwise we reject.
  if (!ALLOWED_HOST_METHODS.includes(method)) throw new Error("Forbidden");
  return await hostRpc.invoke(method, params); // JSON-RPC to host-core
});

```

### Unprivileged Renderer Calls

The renderer invokes privileged operations through a thin, typesafe wrapper that cannot bypass the allow-list:

```typescript
// apps/desktop/src/api/host.ts
export async function readFile(path: string) {
  return await window.ipc.invoke("host.invoke", "files.read", { path });
}

```

Even if the renderer is compromised, attackers cannot execute arbitrary file system operations because only whitelisted methods like `files.read` traverse the boundary.

## Summary

- **PI-Desktop** enforces **UI and privileged runtime separation** through a three-layer architecture: sandboxed renderer, coordination main process, and Rust host-core.
- The **Rust host-core** binary at `crates/host-core/src/` is the only component with direct OS access, handling file operations, secrets, and native tool execution.
- **IPC boundaries** are enforced via static allow-lists (`ALLOWED_HOST_METHODS`) and JSON-RPC communication through the `host.invoke` channel.
- The Electron main process spawns the privileged binary at [`apps/desktop/electron/main/host-process.ts#L51`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/host-process.ts#L51) and validates all requests at [`apps/desktop/electron/main/index.ts#L309`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/index.ts#L309).
- This design minimizes attack surface by ensuring that JavaScript code, even within the main process, cannot perform privileged operations without passing through the Rust binary's controlled interface.

## Frequently Asked Questions

### What prevents a compromised Electron renderer from accessing the file system directly?

The renderer process runs within Electron's sandbox and lacks Node.js privileges. It must communicate through `window.ipc.invoke()`, which routes through the main process's `ALLOWED_HOST_METHODS` allow-list. Even if the renderer is exploited, it can only invoke whitelisted methods like `files.read`, and cannot access the file system directly or spawn the host-core binary itself.

### Why does PI-Desktop use a separate Rust binary instead of Node.js native modules?

The **Rust host-core** provides memory safety guarantees and true process isolation from the V8 JavaScript engine. According to the runtime documentation at [`docs/spec/03-runtime/06-host-rpc-protocol.md#L8`](https://github.com/vastsa/PI-Desktop/blob/main/docs/spec/03-runtime/06-host-rpc-protocol.md#L8), this separation ensures that vulnerabilities in the JavaScript runtime cannot compromise the privileged backend, as the host-core runs as a separate OS process with its own memory space.

### How does the main process validate which UI requests can reach the privileged runtime?

The Electron main process maintains a hard-coded allow-list of permitted methods (referenced as `AppMenuCommand` in ADR 0021). In [[`apps/desktop/electron/main/index.ts`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/index.ts)](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/index.ts), the `ipcMain.handle("host.invoke")` handler checks if the requested method exists in `ALLOWED_HOST_METHODS` before forwarding via JSON-RPC to the Rust host-core, rejecting all unauthorized requests with a "Forbidden" error.

### Can the UI layer modify the allow-list or IPC validation logic?

No. The allow-list and validation logic are defined in the main process source code (`apps/desktop/electron/main/`), which is loaded from the packaged application bundle. While the renderer could theoretically attempt to send arbitrary IPC messages, the main process's hard-coded validation logic throws errors for any method not explicitly whitelisted, preventing privilege escalation from the UI layer.