How the PI-Desktop Rust Host Core Manages State and Permissions
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 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. 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 incrates/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 transcriptssecrets: SecretStore— Encrypted storage for user credentials and API keysworkspace: WorkspaceState— Per-session filesystem sandbox managementpermissions: PermissionManager— Central authority for all permission decisionssession_grants: HashMap<String, Vec<String>>— Cached "allow-once" grants for active sessionsplugin_execsandplugin_import_rates— Plugin execution tracking and rate-limiting defined incrates/host-core/src/plugins/permissions.rsactive_bash_cancellationsandpending_bash_aborts— Safe management of long-running Bash toolshashline: HashlineStore— Snapshot-based interface for text-processing tools implemented incrates/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. 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:
- Creating a request — The
create_requestmethod builds aPermissionRequestwith a truncatedargs_previewto minimize IPC payload size and registers a one-shot channel for the decision - Evaluating automatically —
evaluate_auto_with_permission_modedetermines if a tool can run without user intervention based on tool risk level, permission mode, session grants, and external-path requirements - Resolving or canceling — The
resolvemethod sends decisions back through the stored channel, whilecancelorcancel_for_toolaborts pending requests when tools terminate early - Expiration — A background sweep via
expire_staleremoves requests older thanPERMISSION_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, oraccept-edits) configured per session - Session grants from
session_grantsfor 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:
// 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:
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:
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:
// 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
AppStatestruct incrates/host-core/src/state.rsexclusively owns the SQLite database, secret store, and all runtime resources - Permission isolation — The
PermissionManagerincrates/host-core/src/permissions.rsacts 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →