# How the PI-Desktop Rust Host Core Manages State and Permissions

> Discover how the PI-Desktop Rust host core manages state and permissions using a centralized AppState struct for secure data and resource handling. Learn about strict isolation.

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

---

**The PI-Desktop Rust host core centralizes all persistent data, runtime resources, and security decisions in a single `AppState` struct that owns the SQLite database, secret store, and permission manager, enforcing strict isolation between the renderer process and sensitive system operations.**

PI-Desktop implements a "frozen process model" where the Rust host core sits at the bottom of the trust boundary (Renderer → Preload IPC → Electron Main → **Rust host core** → Agent runtime). According to the [vastsa/PI-Desktop](https://github.com/vastsa/PI-Desktop) source code, this architecture ensures that no other component can directly access the SQLite database or permission logic, making the Rust layer the authoritative source of truth for all security-critical operations.

## Centralized Application State with AppState

All long-living host resources are aggregated in the `AppState` struct defined in [`crates/host-core/src/state.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/state.rs). This struct serves as the single owner of persistent storage and runtime state, initialized via `AppState::open` when the host process starts.

### State Initialization and Lifecycle

When `AppState::open` executes, it performs several critical setup operations:

- Opens the SQLite database using `Database::open_in_dir` (defined in [`crates/host-core/src/db.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/db.rs))
- Restores orphaned sessions and in-flight replies from previous crashes
- Loads the encrypted secret store via `SecretStore::open`
- Instantiates managers for workspaces, permissions, plans, plugins, MCP servers, and user skills
- Initializes runtime-only structures including rate-limit maps, tool-budget tracking, Bash cancellation channels, and the hash-line store

The struct also implements graceful shutdown logic that marks `shutting_down`, aborts active Bash tools, cancels pending permission requests, and clears temporary maps to prevent resource leaks.

### Core State Components

The `AppState` struct contains several key fields that illustrate how the Rust host core manages state:

- `db: Database` — Persistent SQLite storage for settings, sessions, and transcripts
- `secrets: SecretStore` — Encrypted storage for user credentials and API keys
- `workspace: WorkspaceState` — Per-session filesystem sandbox management
- `permissions: PermissionManager` — Central authority for all permission decisions
- `session_grants: HashMap<String, Vec<String>>` — Cached "allow-once" grants for active sessions
- `plugin_execs` and `plugin_import_rates` — Plugin execution tracking and rate-limiting defined in [`crates/host-core/src/plugins/permissions.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/plugins/permissions.rs)
- `active_bash_cancellations` and `pending_bash_aborts` — Safe management of long-running Bash tools
- `hashline: HashlineStore` — Snapshot-based interface for text-processing tools implemented in [`crates/host-core/src/tools/hashline/mod.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/tools/hashline/mod.rs)

## Permission Management Architecture

All permission decisions flow through the `PermissionManager` located in [`crates/host-core/src/permissions.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/permissions.rs). This module stores pending requests in a private `HashMap<String, Pending>` and exposes a strict API that prevents unauthorized tool execution.

### Permission Request Lifecycle

The `PermissionManager` handles permission requests through a structured flow:

1. **Creating a request** — The `create_request` method builds a `PermissionRequest` with a truncated `args_preview` to minimize IPC payload size and registers a one-shot channel for the decision
2. **Evaluating automatically** — `evaluate_auto_with_permission_mode` determines if a tool can run without user intervention based on tool risk level, permission mode, session grants, and external-path requirements
3. **Resolving or canceling** — The `resolve` method sends decisions back through the stored channel, while `cancel` or `cancel_for_tool` aborts pending requests when tools terminate early
4. **Expiration** — A background sweep via `expire_stale` removes requests older than `PERMISSION_TIMEOUT_MS` (120 seconds) and notifies waiting callers with a denial

Permission requests are exposed to the UI via the `permissions.pending` RPC method, which returns `PendingPermission` objects containing creation timestamps, expiration times, and remaining duration.

### Automatic Evaluation Logic

The `PermissionManager` evaluates auto-allow decisions using multiple criteria:

- **Tool risk classification** (low/medium/high) inferred from tool names or explicit manifest declarations via `tool_risk_with_declared`
- **Effective permission mode** (`auto`, `ask`, or `accept-edits`) configured per session
- **Session grants** from `session_grants` for cached "allow-once" or "allow-session" entries
- **External-path requirements** for tools accessing paths outside the sandbox
- **Contract mode overrides** for plugins performing plan-safe actions

## Integrating State and Security

`AppState` acts as the glue layer between the permission system and the rest of the host core. When a tool triggers a permission request, `AppState` registers it for lifecycle management:

```rust
// Register a pending permission when a tool triggers a request
pub fn register_pending_permission(
    &mut self,
    request_id: &str,
    session_id: &str,
    tool_call_id: &str,
) {
    self.pending_permissions.insert(
        request_id.to_string(),
        (session_id.to_string(), tool_call_id.to_string()),
    );
}

```

When the UI sends a permission decision from the renderer, `AppState::resolve_permission` forwards the decision to `PermissionManager::resolve` and clears the bookkeeping entry. During shutdown, `AppState::shutdown` iterates over `pending_permissions` and calls `self.permissions.cancel(&request_id)` to ensure waiting UI components receive definitive denials rather than hanging indefinitely.

## Concurrency and Safety Guarantees

The Rust host core implements several safety mechanisms to prevent race conditions and resource leaks:

**Rate limiting** — Import and delete operations for plugins are throttled per-plugin using timestamp vectors (`plugin_import_rates`, `plugin_delete_rates`) to prevent abuse.

**Bash cancellation** — Active Bash processes are tracked with `tokio::sync::watch::Sender<bool>` channels. A tombstone map (`pending_bash_aborts`) handles races between abort signals and process startup, ensuring cancellation requests are not lost.

**Thread-safety** — All mutable maps live inside `AppState`, owned by a single Tokio task. The public API returns clones or channel receivers rather than references, preventing simultaneous mutable access across thread boundaries.

**Graceful teardown** — The `shutdown` method sets `shutting_down` to true, aborts active Bash tools, cancels pending permission requests, and clears temporary maps. This sequence guarantees that no background work continues after the host process exits.

## Practical Implementation Examples

### Initializing the Host Core and Creating Permission Requests

The following example demonstrates opening the host core and requesting permission for a Bash tool:

```rust
use pi_desktop::host_core::AppState;
use std::path::Path;

// Initialise the host core (normally done at process start)
let data_dir = Path::new("/home/user/.pi-desktop");
let mut app_state = AppState::open(data_dir).expect("failed to start host core");

// Create a permission request for a Bash tool
let (request, rx) = app_state
    .permissions
    .create_request(
        "session-42",
        "tool-call-7",
        "Bash",
        serde_json::json!({ "command": "ls -la" }),
        "Run a shell command",
    );

// Register the request so the shutdown logic can cancel it if needed
app_state.register_pending_permission(
    &request.request_id,
    &request.session_id,
    &request.tool_call_id,
);

// Later, when the UI approves the request:
app_state
    .resolve_permission(&request.request_id, pi_desktop::host_core::PermissionDecision::AllowOnce)
    .expect("resolution failed");

// The `rx` future resolves with the decision (use `.await` in an async context)
let decision = tokio::runtime::Handle::current().block_on(rx).unwrap();
println!("Permission decision: {:?}", decision);

```

### Evaluating Session Grants Automatically

This example shows how the permission manager evaluates cached session grants:

```rust
use std::collections::HashMap;
use pi_desktop::host_core::PermissionManager;

let pm = PermissionManager::default();
let mut grants = HashMap::new();
grants.insert("session-42".to_string(), vec!["Read".to_string()]);

let decision = pm.evaluate_auto_with_permission_mode(
    "session-42",
    "Read",
    "agent",
    "auto",
    &grants,
);
assert_eq!(decision, Some(pi_desktop::host_core::PermissionDecision::AllowOnce));

```

### Handling Graceful Shutdown

When the host process receives a shutdown signal, `AppState` ensures clean termination:

```rust
// Trigger a graceful shutdown (e.g., on SIGTERM)
app_state.shutdown();

// Any subsequent attempts to start a new Bash tool will error:
let err = app_state.register_bash_cancellation("session-42", "tool-call-8")
    .expect_err("should be rejected");
assert_eq!(err, "HOST_SHUTTING_DOWN");

```

## Summary

The PI-Desktop Rust host core implements a fortress-like architecture for state and permission management:

- **Centralized ownership** — The `AppState` struct in [`crates/host-core/src/state.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/state.rs) exclusively owns the SQLite database, secret store, and all runtime resources
- **Permission isolation** — The `PermissionManager` in [`crates/host-core/src/permissions.rs`](https://github.com/vastsa/PI-Desktop/blob/main/crates/host-core/src/permissions.rs) acts as the sole authority for security decisions, with 120-second timeouts and automatic expiration
- **Safe concurrency** — Single-task ownership with Tokio channels prevents data races, while tombstone maps handle Bash cancellation races
- **Graceful degradation** — Shutdown logic ensures all pending permissions are cancelled and background tasks aborted, returning the system to a clean state

## Frequently Asked Questions

### What is the frozen process model in PI-Desktop?

The frozen process model is PI-Desktop's security architecture that establishes strict process boundaries: Renderer → Preload IPC → Electron Main → **Rust host core** → Agent runtime. According to the vastsa/PI-Desktop source code, this model ensures that only the Rust host core can access the SQLite database and permission logic, while renderer processes remain sandboxed and unable to touch persistent data directly.

### How does the Rust host core handle permission timeouts?

The `PermissionManager` defines a `PERMISSION_TIMEOUT_MS` constant set to 120 seconds. A background sweep via `expire_stale` removes requests older than this threshold and notifies waiting callers with a denial. This prevents indefinite resource holds when the UI fails to respond to permission prompts.

### What happens to active Bash processes during shutdown?

When `AppState::shutdown` executes, it first sets the `shutting_down` flag, then iterates through `active_bash_cancellations` to signal cancellation via `tokio::sync::watch::Sender<bool>` channels. The `pending_bash_aborts` tombstone map ensures that any Bash process starting during shutdown is immediately cancelled, preventing zombie processes.

### How does the permission manager determine auto-allow decisions?

The `evaluate_auto_with_permission_mode` method combines multiple factors: tool risk classification (low/medium/high) from `tool_risk_with_declared`, the effective permission mode (`auto`, `ask`, or `accept-edits`), cached session grants from `session_grants`, external-path requirements, and contract mode overrides for plugins. Only when all criteria indicate low-risk, previously-authorized behavior will the system bypass explicit user confirmation.