Portal Capability Protocol (PCP): Architecture, Registry, and Daemon
The Portal Capability Protocol (PCP) is Elara’s internal nervous system — the protocol through which she exercises full system agency over every application, surface, and hardware component within the Portal compositor. Unlike external-agent frameworks where AI connects to the OS through APIs, PCP treats Elara as the system itself: she is a subsystem within the compositor, not a guest process calling it from outside. PCP defines three dimensions of her agency: perception (what she can know), action (what she can do), and agency (how she decides what to do, including permission checks, confirmation gates, and audit trails). The protocol spans 18 Rust crates organized under portal/pcp/, covering core type definitions, a concurrent capability registry, a Unix-socket daemon with a framed binary wire protocol, multiple detection adapters (native .desktop, AT-SPI2 accessibility, Wine/MSAA bridge, input simulation), and subsystems for push event delivery, transaction rollback, learning, crash recovery, and multi-app coordination.
Sources: openspec/specs/pcp/pcp-v4.2-full.md#L100-L217, portal/pcp/core/src/lib.rs#L1-L79
System Identity and Design Philosophy
Section titled “System Identity and Design Philosophy”The foundational architectural decision is that Elara is not an external client connecting through APIs — she is the system identity. There is no concept of “connecting Claude to Portal” or “using GPT as the agent.” The LLM is a cognitive resource Elara draws upon, not an identity she assumes. This has three immediate implications: no agent swapping, no middleware daemon between Elara and the compositor’s core logic, and the compositor itself is the authoritative source of truth for all surface, window, and input state.
PCP re-exports what the compositor already knows, enriched by AT-SPI2 accessibility data. Elara does not “scan the screen” — she queries a structured model maintained by the system. The protocol follows seven design principles: agent-native (semantic operations, not pixel coordinates), compositor-authoritative (nothing is “discovered” — it is declared), progressive fidelity (Tier 1 apps get full semantic access, Tier 2 get structural access, Tier 3 get input simulation), real-time and bidirectional (events, sessions, continuous data), system-internal by default (no daemon, no socket, no serialization for internal operations), open protocol, closed identity (the format is documented but only Elara uses it), and audit-everything (every action is logged).
| Principle | What It Means |
|---|---|
| Agent-native | Capabilities are semantic operations (“activate the compose button”), not pixel coordinates |
| Compositor-authoritative | The compositor sees all; PCP re-exports its state enriched by AT-SPI2 |
| Progressive fidelity | Tier 1 → Tier 2 → Tier 3 with automatic degradation |
| System-internal | Elara and PCP Core share the same process; zero-copy Rust trait calls |
| Audit-everything | Every action logged with timestamp, target, parameters, and result |
Sources: openspec/specs/pcp/pcp-v4.2-full.md#L140-L218
Core Data Model
Section titled “Core Data Model”Three-Domain Privilege Architecture
Section titled “Three-Domain Privilege Architecture”PCP implements a strict three-domain privilege model with hard architectural walls between domains. The Domain enum is the foundation — every capability, every intent, and every adapter is bound to exactly one domain:
| Domain | Privilege Level | Scope | Escalation Rule |
|---|---|---|---|
| App | Lowest | Execute app capabilities, read registry, subscribe to events | Cannot emit compositor or system intents |
| Compositor | Mid | All App + surface/zone management, workspace, clipboard | Cannot emit system intents |
| System | Highest | All Compositor + audio, network, filesystem, power | Top-level domain; no escalation above |
A CapabilityId follows a reverse-DNS format with at least three dot-separated parts: {domain}.{app}.{action}. V4.2 expanded this to accept multi-dot app segments (e.g., editor.org.xfce.mousepad.read). The CapabilityDescriptor struct captures each capability’s identity, domain, JSON Schema for input/output, detection method, stability level, and version.
graph TD
subgraph "Three-Domain Privilege Model"
APP["APP DOMAIN<br/>Low Privilege<br/>mail.search, editor.read, browser.navigate"]
COMP["COMPOSITOR DOMAIN<br/>Mid Privilege<br/>surface.move, workspace.switch, clipboard.read"]
SYS["SYSTEM DOMAIN<br/>High Privilege<br/>audio.volume, network.wifi, power.shutdown"]
APP -->|"HARD WALL — blocked"| COMP
COMP -->|"HARD WALL — blocked"| SYS
end
Sources: portal/pcp/core/src/types.rs#L16-L62, openspec/specs/pcp/pcp-v4.2-full.md#L3510-L3544
Detection Methods and Tier System
Section titled “Detection Methods and Tier System”Capabilities are discovered through four detection methods, each representing a progressively lower fidelity tier. The DetectionMethod enum drives adapter ordering and determines the quality of semantic access:
| Method | Tier | Mechanism | Capabilities |
|---|---|---|---|
Desktop |
Tier 1 | .desktop category + toolkit detection + PCP manifest |
Full typed capabilities from signed manifests |
Atspi2 |
Tier 2 | AT-SPI2 accessibility tree inspection | Structural access: element roles, states, actions, text |
Wine |
Tier 2b | MSAA/UIA bridge through Wine | Element model translation from Windows accessibility |
Simulated |
Tier 3 | uinput input simulation | Compositor-only metadata; click/scroll fallback |
The static-first detection strategy (V2.0) populates the full capability registry before any app launches, using .desktop file categories combined with ELF toolkit probing. The detection pipeline reads the Categories key from .desktop files (e.g., Categories=Network;Email;), probes the binary’s ELF headers for linked libraries (libgtk-3.so → GTK, libQt6Core.so → Qt, libcef.so → Electron), and cross-references a static lookup table to predict capabilities. AT-SPI2 probing is retained only as a validation and enrichment step on first launch, not as the primary classifier.
Sources: portal/pcp/core/src/types.rs#L38-L62, openspec/specs/pcp/pcp-v4.2-full.md#L2242-L2348
Core Trait Hierarchy
Section titled “Core Trait Hierarchy”The trait system in pcp-core defines the contractual surface for all PCP participants. Four primary traits govern the protocol:
The Adapter trait is implemented by each detection method. Each adapter declares its detection_method(), performs detect(&self, app_id) returning a DetectionResult, executes capabilities via execute() or execute_with_method(), and reports health through health_check(). The PcpServer trait is implemented by Tier 1 native applications, exposing manifest(), invoke(), validate_params(), subscribe_events(), and query_dynamic_state(). The CapabilityRegistry trait defines the registry contract: register, unregister, get, query (with filter), register_app, and apps. The EventBus trait provides pub/sub semantics using RPITIT (Return Position Impl Trait In Trait) for zero-cost async dispatch.
The CapabilityResult struct carries a StateSource enum (Rust, Renderer, Stale, Unknown) that tells consumers whether to trust the returned data or perform a follow-up query — a V4.2 addition ensuring Tier 1 apps report which state model their results came from.
Sources: portal/pcp/core/src/traits.rs#L18-L375, portal/pcp/core/src/manifest/manifest.rs#L1-L98
Capability Registry Architecture
Section titled “Capability Registry Architecture”The registry is the central nervous component — it stores all discovered capabilities and applications, answers queries from the execution pipeline, and serves as the source of truth for “what can Elara do right now.” The implementation uses DashMap for lock-free concurrent access, wrapping two maps: one keyed by capability ID string, another by application ID.
InMemoryRegistry Implementation
Section titled “InMemoryRegistry Implementation”The InMemoryRegistry struct in portal/pcp/registry/src/registry.rs provides O(1) insertion and lookup through sharded concurrent maps. The register() method inserts a CapabilityDescriptor keyed by its CapabilityId string representation, unregister() removes it (returning an error if not found), and query() filters by optional domain, detection method, and app pattern — iterating all entries and applying predicates in a single pass. The registry also implements the async CapabilityRegistry trait, delegating to the same synchronous methods.
graph LR
subgraph "Registry Data Flow"
PIPELINE["DetectionPipeline<br/>scans .desktop dirs"]
ADAPTERS["Adapters<br/>Native, AT-SPI2, Wine"]
REG["InMemoryRegistry<br/>DashMap-backed"]
EVENTBUS["PushEventBus<br/>publishes events"]
QUERY["IPC Handler<br/>answers queries"]
PIPELINE --> ADAPTERS
ADAPTERS -->|"DetectionResult"| REG
REG -->|"CapabilityRegistered event"| EVENTBUS
QUERY -->|"filter by domain/method/app"| REG
end
Sources: portal/pcp/registry/src/registry.rs#L1-L160
Adapter Lifecycle Management
Section titled “Adapter Lifecycle Management”The AdapterManager in portal/pcp/registry/src/lifecycle/ wraps each adapter in a state machine with enforced transition validation. Five states govern the lifecycle: Initializing → Ready → Degraded → Failed → Shutdown. Transitions are validated by a const fn table — for example, Initializing can only move to Ready, Failed, or Shutdown (not directly to Degraded). The health monitor loop periodically polls each adapter’s health_check() and drives state transitions: healthy adapters in Initializing or Degraded are promoted to Ready, degraded results from Ready trigger demotion, and failures force a transition to Failed.
The health check function uses a blocking thread pool pattern — it spawns a spawn_blocking task with a single-threaded tokio runtime and LocalSet to safely invoke the adapter’s async health_check() without risking deadlocks in the main async context.
| State | Valid Transitions To |
|---|---|
Initializing |
Ready, Failed, Shutdown |
Ready |
Degraded, Failed, Shutdown |
Degraded |
Ready, Failed, Shutdown |
Failed |
Shutdown |
Shutdown |
(terminal) |
Sources: portal/pcp/registry/src/lifecycle/manager.rs#L1-L200, portal/pcp/registry/src/lifecycle/state.rs#L1-L52
Capability Diff Tracking
Section titled “Capability Diff Tracking”When adapters re-detect capabilities (due to app updates, state changes, or re-evaluation), the CapabilityDiffTracker computes structured deltas rather than emitting individual events. A CapabilityDiff carries app_id, previous/new adapter states, and three change vectors: added (new DerivedCapability descriptors), removed (capability ID strings), and changed (field-level CapabilityFieldChange with old/new values). This batch diff mechanism, introduced in V4.0, reduces event bus noise during capability churn.
Sources: portal/pcp/core/src/capability_diff/mod.rs#L1-L31, openspec/specs/pcp/pcp-v4.2-full.md#L2844-L2911
Execution Pipeline
Section titled “Execution Pipeline”The execution pipeline transforms a structured Intent into a capability invocation through seven sequential steps, each with its own timeout budget. The ExecutionPipelineImpl is generic over four type parameters: the registry (R: CapabilityRegistry), adapter (A: Adapter), audit logger (L: AuditLogger), and permission checker (P: PermissionChecker).
Seven-Stage Pipeline
Section titled “Seven-Stage Pipeline”| Step | Operation | Timeout Budget | Failure Behavior |
|---|---|---|---|
| Classify | Map capability ID to IntentAction (action class + confidence) |
Per-step config | Fallback to unknown class |
| ResolveTarget | Look up CapabilityDescriptor in registry by constructed CapabilityId |
Per-step config | Proceeds even if not found (uses fallback) |
| CheckPermissions | Verify caller domain can access the capability | Per-step config | Returns PermissionDenied immediately |
| ResolveAction | Map classified intent to ResolvedAction with method and confirmation requirement |
Per-step config | Skips; falls to plain execute() |
| Execute | Call adapter execute_with_method() or execute() |
Per-step config | Returns ExecutionFailed |
| Verify | Check that execution_result.success == true |
Per-step config (skippable) | Returns ExecutionFailed |
| Audit | Log AuditEntry with capability, params, result, timestamp |
Per-step config | Returns error |
The pipeline wraps all seven steps in a single tokio::time::timeout using the total_timeout from PipelineConfig. If any step times out individually, a PcpError::Timeout is returned with the step name and budget. If the total timeout fires, the result carries a “pipeline timeout” error.
graph LR
INTENT["Intent<br/>{domain, action, target_app, parameters}"]
CLASSIFY["1. Classify<br/>IntentClassifier"]
RESOLVE["2. ResolveTarget<br/>Registry lookup"]
PERM["3. CheckPermissions<br/>PermissionChecker"]
ACTION["4. ResolveAction<br/>ActionResolver"]
EXEC["5. Execute<br/>Adapter.invoke"]
VERIFY["6. Verify<br/>result.success check"]
AUDIT["7. Audit<br/>AuditLogger.log"]
INTENT --> CLASSIFY
CLASSIFY --> RESOLVE
RESOLVE --> PERM
PERM -->|"Denied"| FAIL["Return Error"]
PERM -->|"Allowed"| ACTION
ACTION --> EXEC
EXEC --> VERIFY
VERIFY -->|"Failed"| FAIL
VERIFY -->|"Success"| AUDIT
AUDIT --> RESULT["ExecutionResult"]
Sources: portal/pcp/core/src/pipeline.rs#L1-L317
Intent Classification
Section titled “Intent Classification”The IntentClassifier maps capability ID suffixes to ActionClass enums using keyword rules. Each rule associates a list of action keywords with a semantic class: navigate (open_url, navigate, scroll, switch_tab), activate (click, press, toggle, activate), edit (type_text, insert_text, edit, paste), read (read, get_text, screenshot, describe), delete (delete, remove, clear, close), and send (send, submit, share, export). The classifier iterates rules in order, returning the first match with a confidence value.
Sources: portal/pcp/core/src/intent.rs#L1-L60
Confirmation Gates
Section titled “Confirmation Gates”The ConfirmationGate system prevents potentially dangerous capabilities from executing without explicit user consent. A ConfirmationConfig lists glob patterns for gated capabilities — *.delete*, *.destroy*, system.network.*.connect*, system.audio.*.set_volume*, etc. When an intent matches a gated pattern, a single-use ConfirmationToken is issued, tied to the intent’s hash, with a configurable expiry (default 60 seconds). The token must be consumed before execution proceeds. Non-gated capabilities auto-confirm when auto_confirm_non_gated is true (the default).
| Action Class | Default Gate | Undo Window |
|---|---|---|
| Read / Navigate / Activate | Allow | — |
| Input Text | Allow | 5s |
| Delete | Confirm | 5s |
| Send | Confirm | 15s |
| Close App | Confirm | 3s |
| System Settings | Confirm | None |
| Hardware Control | Confirm | None |
Sources: portal/pcp/core/src/gates.rs#L1-L80, openspec/specs/pcp/pcp-v4.2-full.md#L4462-L4475
Permission Engine
Section titled “Permission Engine”The PermissionEngineImpl enforces the three-domain privilege model at runtime. It wraps a PermissionStore (which holds glob-based permission rules) and a DashMap of rate-limit tracking entries. The with_defaults() constructor pre-configures baseline permissions: APP domain gets app.*.*, COMPOSITOR gets compositor.*.*, SYSTEM gets system.*.*, with confirmation required for system.*.destroy* and compositor.*.reconfigure*. Domain escalation is checked by comparing the caller’s domain level against the capability’s domain — a lower-domain caller is denied access to higher-domain capabilities.
Rate limiting is enforced per-permission with a 60-second sliding window. Each invocation increments the counter in RateLimitEntry; if count exceeds max_uses_per_minute, the permission is denied.
Sources: portal/pcp/core/src/permission/engine.rs#L1-L100, portal/pcp/core/src/permission/mod.rs#L1-L34
Transaction Rollback
Section titled “Transaction Rollback”The Transaction system provides atomic execution of compound intents with rollback semantics. A Transaction holds a unique 64-bit ID, a vector of PendingOperation entries (each with capability ID, target app, params, and an optional InverseOperation), and a TransactionState (Active, Committed, or RolledBack). When commit is called, operations execute sequentially; if any operation fails and has an inverse, the inverse is called to undo the effect. Two strategies are supported: ATOMIC (fail-fast with rollback) and BEST_EFFORT (continue on failure, log discrepancies).
Sources: portal/pcp/core/src/transaction.rs#L1-L100, openspec/specs/pcp/pcp-v4.2-full.md#L3414-L3441
Daemon Architecture
Section titled “Daemon Architecture”The PCP daemon (portal-pcpd) is the standalone process that runs outside the compositor binary. While the spec describes PCP Core as a compositor subsystem, the current implementation deploys it as a separate daemon binary that exposes PCP services over a Unix-socket wire protocol. The daemon integrates all adapters, the detection pipeline, the capability registry, the IPC handler, and V4.2 runtime servers.
Daemon Initialization Flow
Section titled “Daemon Initialization Flow”The daemon entry point (portal/pcp/daemon/src/main.rs) follows a strict initialization sequence: create the registry → create the adapter manager with health monitoring → create the push event bus → initialize and health-check each adapter (Native, AT-SPI2, Wine) → sort adapters by method priority (Desktop=0, Atspi2=1, Wine=2, Simulated=3) → initialize the platform adapter → create the detection pipeline → setup V4.2 runtime servers → create the IPC handler with dispatcher → bind the stream server → enter the main tokio::select! loop.
Adapters that fail their initial health check are logged as warnings and skipped — they are not registered with the adapter manager or added to the detection pipeline’s adapter list. This ensures that a broken AT-SPI2 bus or missing Wine installation degrades gracefully without preventing daemon startup.
graph TD
START["Daemon main()"]
REG["Create InMemoryRegistry"]
ADAPT_MGR["Create AdapterManager<br/>with health monitor"]
EVENT_BUS["Create PushEventBus"]
NATIVE["Initialize NativeDetector<br/>health_check()"]
ATSPI["Initialize Atspi2Adapter<br/>health_check()"]
WINE["Initialize WineAdapter<br/>health_check()"]
SORT["Sort by method priority<br/>Desktop→Atspi2→Wine→Simulated"]
PLATFORM["Initialize PlatformAdapter"]
PIPELINE["Create DetectionPipeline"]
RUNTIMES["setup_runtimes()<br/>Learning, Recovery, Inspection, Coordination"]
IPC["Create IpcHandler<br/>with dispatcher"]
BIND["Bind StreamServer<br/>SO_PEERCRED + chmod 0600"]
LOOP["tokio::select!<br/>pipeline.run / SIGINT / SIGTERM / IPC serve"]
START --> REG
REG --> ADAPT_MGR
ADAPT_MGR --> EVENT_BUS
EVENT_BUS --> NATIVE
NATIVE --> ATSPI
ATSPI --> WINE
WINE --> SORT
SORT --> PLATFORM
PLATFORM --> PIPELINE
PIPELINE --> RUNTIMES
RUNTIMES --> IPC
IPC --> BIND
BIND --> LOOP
Sources: portal/pcp/daemon/src/main.rs#L1-L251
Detection Pipeline
Section titled “Detection Pipeline”The detection pipeline (portal/pcp/daemon/src/pipeline/) orchestrates capability discovery through a filesystem-watching loop with periodic scan fallback. It connects three core PCP components: adapters (detect capabilities), registry (store results), and event bus (publish events).
The DetectionPipeline struct holds an Arc<InMemoryRegistry>, an Arc<PushEventBus>, a Vec<Arc<dyn Adapter>> (sorted by priority), a DetectionConfig (scan intervals, desktop dirs), and two parking_lot::Mutex fields tracking known apps and last detection timestamps. The run() method first executes a full scan_cycle, then sets up notify filesystem watchers on configured desktop directories. When a .desktop file is created, modified, or removed, the pipeline triggers immediate re-detection for that app. If the watcher fails to initialize, it falls back to periodic polling at scan_interval (default 30 seconds).
The detect_app() method implements multi-adapter fallback: it iterates adapters in priority order, calling detect() on each. Results with non-empty capabilities and confidence ≥ 0.5 are collected as “best capabilities”; results with only metadata (no capabilities) are collected as “best metadata”. The pipeline prefers capability-bearing results, falling back to metadata-only if no adapter returned capabilities.
graph TD
TRIGGER["Trigger: filesystem event / periodic scan"]
SCAN["scan_for_apps()<br/>spawn_blocking → parse .desktop files"]
NEW_CHECK{"Is new app<br/>or should_rescan?"}
SKIP["Skip — cache valid"]
DETECT["detect_app()<br/>iterate adapters by priority"]
NATIVE_T{"NativeDetector<br/>detect()"}
ATSPI_T{"Atspi2Adapter<br/>detect()"}
WINE_T{"WineAdapter<br/>detect()"}
BEST["Select best result<br/>(caps > metadata, confidence ≥ 0.5)"]
REGISTER["register_detection()<br/>verify manifest signature<br/>register app + capabilities<br/>publish CapabilityRegistered events"]
TRIGGER --> SCAN
SCAN --> NEW_CHECK
NEW_CHECK -->|"No"| SKIP
NEW_CHECK -->|"Yes"| DETECT
DETECT --> NATIVE_T
NATIVE_T --> ATSPI_T
ATSPI_T --> WINE_T
WINE_T --> BEST
BEST --> REGISTER
Sources: portal/pcp/daemon/src/pipeline/pipeline.rs#L1-L49, portal/pcp/daemon/src/pipeline/run.rs#L1-L109, portal/pcp/daemon/src/pipeline/scan.rs#L1-L116, portal/pcp/daemon/src/pipeline/detect.rs#L1-L71, portal/pcp/daemon/src/pipeline/register.rs#L1-L60, portal/pcp/daemon/src/pipeline/mod.rs#L1-L60
V4.2 Runtime Servers
Section titled “V4.2 Runtime Servers”The setup_runtimes() function in runtimes.rs initializes four daemon-managed PcpServer implementations, each registered in both the registry and the InProcessDispatcher:
| Server | App ID | Purpose |
|---|---|---|
LearningServer |
system.learning |
Records invocation history, provides onboarding suggestions |
RecoveryServer |
system.recovery |
Per-app crash detection, auto-downgrade to Tier 2 |
InspectionServer |
system.inspection |
Runtime capability introspection: schema, stats, state snapshot |
CoordinationServer |
system.coordination |
Cross-app composition: sequence, parallel, conditional |
The CoordinationServer uses a DispatcherStepExecutor that adapts the InProcessDispatcher.invoke() interface to the StepExecutor trait required by the sequence executor. Each step is invoked with AuthPrincipal::System (daemon-internal invocation). A RevocationHandler is also created for dynamic capability removal — admin-only operations that write marker files to /var/lib/portal/pcp/revoked/{app_id}.marker.
Sources: portal/pcp/daemon/src/runtimes.rs#L1-L150, portal/pcp/daemon/src/revocation.rs#L1-L100
In-Process Dispatcher
Section titled “In-Process Dispatcher”The InProcessDispatcher routes capability invocations from the daemon to the correct app server. It maintains an Arc<RwLock<HashMap<String, Arc<dyn PcpServer>>>> mapping app IDs to server trait objects. When invoke() is called, it acquires a read lock, looks up the server by app_id, and delegates to server.invoke(capability_id, ctx, params). If the app is not registered, it returns PcpError::CapabilityNotFound.
Sources: portal/pcp/daemon/src/dispatch.rs#L1-L63
Wire Protocol and IPC
Section titled “Wire Protocol and IPC”The PCP stream protocol provides the binary transport between the daemon and CLI clients (or Tier 1 apps connecting their PcpServer). The protocol uses length-prefixed framing with postcard-serialized headers and CRC-32 payload verification.
Frame Format
Section titled “Frame Format”Each frame on the wire follows this layout: [4 bytes: total_len (u32 LE)] [header (postcard-serialized)] [payload (N bytes)]. The StreamHeader contains a magic number (0x50435000, ASCII “PCP0”), protocol version (1), a StreamMessageType discriminant, payload_len, and a CRC-32 checksum computed by crc32fast. On receive, the framing layer validates the magic, verifies the version, and checks the payload checksum — corrupted frames return ProtocolError::ChecksumMismatch.
The protocol defines 27 message types covering the full lifecycle: detection requests/responses, execution requests/responses, registry queries, event subscriptions, push events, manifest fetches, parameter validation, state queries, and app shutdown.
Sources: portal/pcp/stream/src/protocol.rs#L1-L200, portal/pcp/stream/src/framing.rs#L1-L100
IPC Handler
Section titled “IPC Handler”The IpcHandler in portal/pcp/daemon/src/ipc/mod.rs accepts connections from pcp-cli through the stream server. Each accepted connection is split into a read half (framed messages) and a write-command channel. A per-connection token-bucket rate limiter (100 requests/second) prevents hostile clients from exhausting resources — excess requests receive an HTTP-style 429 error response while the connection stays open.
The dispatcher matches StreamMessageType to handler methods: Ping → handle_ping, RegistryQuery → handle_registry_query, DetectRequest → handle_detect_request, ExecuteRequest → handle_execute_request, SubscribeEvents / UnsubscribeEvents → corresponding handlers. All other message types return an ErrorResponse.
sequenceDiagram
participant CLI as pcp-cli
participant SOCK as Unix Socket
participant IPC as IpcHandler
participant REG as Registry
participant DISP as Dispatcher
participant SERVER as PcpServer (App)
CLI->>SOCK: Connect + send StreamMessage
SOCK->>IPC: read_frame()
IPC->>IPC: Rate limit check (100/s)
alt ExecuteRequest
IPC->>REG: query capability by ID
REG-->>IPC: CapabilityDescriptor
IPC->>DISP: invoke(app_id, cap_id, ctx, params)
DISP->>SERVER: server.invoke(cap_id, ctx, params)
SERVER-->>DISP: CapabilityResult
DISP-->>IPC: CapabilityResult
IPC-->>CLI: CapabilityResultMsg
else RegistryQuery
IPC->>REG: query(domain, method, app_pattern)
REG-->>IPC: Vec<CapabilityDescriptor>
IPC-->>CLI: RegistryResponse
else SubscribeEvents
IPC->>IPC: handle_subscribe_events()
IPC-->>CLI: PushEvent (async stream)
end
Sources: portal/pcp/daemon/src/ipc/mod.rs#L1-L179
IPC Payloads
Section titled “IPC Payloads”Wire payloads are defined in portal/pcp/ipc/src/payloads.rs. Each payload type pairs with a message type and carries JSON-serialized content as Vec<u8> for size efficiency. The InvokeCapabilityPayload contains capability_id, InvocationContext, and params_json (JSON bytes). The CapabilityResultPayload carries success, data_json (optional JSON bytes), error, audit_hash, and state_source. The ManifestResponsePayload wraps the full CapabilityManifest as JSON bytes. All payload types provide symmetric encode/decode helpers with explicit error handling.
Sources: portal/pcp/ipc/src/payloads.rs#L1-L200, portal/pcp/ipc/src/lib.rs#L1-L24
PcpServerRunner
Section titled “PcpServerRunner”The PcpServerRunner in portal/pcp/ipc/src/runner.rs enables Tier 1 apps to expose their PcpServer over a per-app Unix socket at /run/portal/pcp/{app_id}.sock. It spawns an accept loop where each incoming connection is handled by handle_connection() — a loop that reads frames, dispatches them to process_message(), and sends responses back. The message processing covers five operations: GetManifest, InvokeCapability, ValidateParamsMsg, QueryState, and AppShutdown. Each is decoded from the payload, delegated to the PcpServer trait method, and the result is encoded back into the wire format.
Sources: portal/pcp/ipc/src/runner.rs#L1-L200
Supervision and Fault Isolation
Section titled “Supervision and Fault Isolation”The supervision layer (portal/pcp/core/src/supervision/) wraps PCP components to provide fault isolation, hang detection, and graceful recovery. The SupervisorImpl monitors seven tracked components: Registry, EventBus, AdapterManager, PermissionEngine, AuditLog, PushBus, and ResolutionEngine. Default configuration sets a 30-second watchdog timeout, 128 MiB memory budget, 5-second health check interval, and a maximum of 3 restart attempts with 2-second backoff.
The supervisor transitions through four states: Running (normal operation) → SuspectedHang (watchdog timeout) → Restarting (recovery attempt) → Degraded (3 consecutive failures; compositor continues without Elara). The MemoryBudget tracker uses atomic counters to track allocations and deallocations, returning MemoryBudgetExceeded when a component tries to allocate beyond its configured budget (threshold: 90% of total).
stateDiagram-v2
[*] --> Running
Running --> SuspectedHang: Watchdog timeout<br/>(missed heartbeats)
Running --> Restarting: Panic caught / Memory breach
SuspectedHang --> Restarting: Recovery initiated
Restarting --> Running: Successful restart + heartbeat
Restarting --> Degraded: 3 consecutive failures
Degraded --> Restarting: Retry on idle cycle
Running --> [*]: Shutdown signal
Sources: portal/pcp/core/src/supervision/mod.rs#L1-L100, openspec/specs/pcp/pcp-v4.2-full.md#L353-L627
Push Event Delivery
Section titled “Push Event Delivery”The push subsystem (portal/pcp/push/) provides reliable capability-event delivery with coalescing, journaling, and configurable overflow handling. The PushEventBus supports two subscription API versions: v1 (polling-based via subscribe_with) and v2 (typed subscriptions with SubscriptionId and tokio mpsc channels). When the bus is overwhelmed, four overflow strategies are available: BlockHandler (back-pressure), DropNewestHandler (preserve old events), DropOldestHandler (ring buffer), and DropAndLogHandler (drop with logging). The EventJournal persists events to disk for crash recovery, and the EventCoalescer merges rapid-fire events (e.g., from AT-SPI2 children_changed) within configurable windows to reduce downstream processing.
Sources: portal/pcp/push/src/lib.rs#L1-L25
Crate Dependency Structure
Section titled “Crate Dependency Structure”The PCP workspace under portal/pcp/ contains 18 crates with a layered dependency model. pcp-core is the foundation — it defines all types, traits, and error types with zero platform dependencies (only serde, chrono, thiserror). All other crates depend on pcp-core. The daemon crate (portal-pcpd) is the integration point that wires together the registry, adapters, pipeline, IPC handler, and V4.2 runtime servers.
| Crate | Role | Key Dependencies |
|---|---|---|
pcp-core |
Types, traits, errors, pipeline, permission, gates | serde, chrono, thiserror, dashmap |
pcp-registry |
InMemoryRegistry, AdapterManager, lifecycle |
pcp-core, dashmap, tokio |
pcp-atspi2 |
AT-SPI2 adapter (D-Bus, accessibility tree) | pcp-core, zbus, atspi |
pcp-native |
Tier 1 detection (.desktop, ELF, static registry) | pcp-core, ed25519-zebra |
pcp-stream |
Wire protocol, framing, socket transport | pcp-core, postcard, crc32fast, tokio |
pcp-ipc |
IPC payloads, client, runner | pcp-core, pcp-stream |
pcp-push |
Event bus with coalescing, journal, overflow | pcp-core, tokio |
pcp-daemon |
Detection pipeline, dispatcher, IPC handler, runtimes | All above crates |
pcp-platform |
Hardware/compositor API adapters | pcp-core |
pcp-wine |
Wine/MSAA bridge adapter | pcp-core |
pcp-coordination |
Multi-app composition (sequence/parallel/conditional) | pcp-core |
pcp-learning |
Invocation history, suggestions | pcp-core |
pcp-recovery |
Crash detection, warm restart | pcp-core |
pcp-inspection |
Runtime capability introspection | pcp-core |
pcp-simulator |
Input simulation fallback (Tier 3) | pcp-core |
pcp-cli |
Command-line client | pcp-core, pcp-ipc, pcp-stream |
Sources: openspec/specs/pcp/pcp-v4.2-full.md#L4616-L4665, portal/pcp/native/src/lib.rs#L1-L20, portal/pcp/coordination/src/lib.rs#L1-L25
Cache Invalidation Lifecycle
Section titled “Cache Invalidation Lifecycle”The invalidation subsystem (portal/pcp/core/src/invalidation/) tracks when cached capability data becomes stale and needs re-validation, implementing §4.2.8 of the V4.2 spec. The InvalidationStateMachine enforces valid transitions between six states: Pending → Validated → Stale → Revalidating → back to Validated or Failed. Triggers include .desktop file changes, AT-SPI2 structural events (element.children_changed, element.window_destroyed, element.property_changed), and time-based staleness thresholds.
The Debouncer prevents invalidation storms by coalescing rapid triggers within configurable windows. The InvalidationPriorityQueue orders re-validation work by priority (user-facing capabilities first, background apps last). All invalidation state is in-memory — after a crash, entries start in Pending state and are re-validated on the next detection cycle.
Sources: portal/pcp/core/src/invalidation/mod.rs#L1-L34
Performance Targets and SLOs
Section titled “Performance Targets and SLOs”The spec defines hard latency budgets for every stage of the intent lifecycle, with ARM-specific validation requirements. The full intent lifecycle for TL1 auto-approved actions targets under 100ms on x86 with a 200ms maximum acceptable threshold. ARM validation is explicitly flagged as required — the spec notes that AT-SPI2 D-Bus roundtrip latency on ARM SoCs (e.g., Orange Pi 5) may be 1.5–3× slower, setting a conservative 300ms ARM target until hardware benchmarks confirm actual performance.
| Metric | x86 Target | ARM Target (Adjusted) | Notes |
|---|---|---|---|
| Intent parsing + classification | < 5ms | Equivalent | LLM-bound, not arch-bound |
| Target resolution (app domain) | < 10ms | Validate on ARM | Hash lookups should be equivalent |
| Permission check (TL1 auto-approve) | < 2ms | Equivalent | In-process hash lookup |
| Action execution (app domain) | < 50ms | < 150ms | AT-SPI2 D-Bus roundtrip is key variable |
| Full intent lifecycle (TL1, auto) | < 100ms | < 300ms | ARM validation required |
| Warm startup (registry populated) | < 2s | < 5s | Static detection of all apps |
| Memory budget | < 128 MiB | < 128 MiB | Same budget for both architectures |
Sources: openspec/specs/pcp/pcp-v4.2-full.md#L4667-L4738
Manifest Signing and Trust
Section titled “Manifest Signing and Trust”Tier 1 applications ship CapabilityManifest files with Ed25519 signatures. The manifest’s canonical_json() method strips the signature field, sorts keys, and uses compact separators to produce deterministic canonical bytes. The verify_with() method checks the signature against a trusted 32-byte public key stored at /etc/portal/keys/trusted/{app_id}.pub. The detection pipeline’s register_detection() calls load_and_verify_manifest() on each detected app, embedding the signature verification status in the app descriptor’s metadata.
The CapabilityManifest struct carries app_id, version, manifest_version (pattern ^\d+\.\d+$), a vector of Capability definitions (each with id, name, description, category, JSON Schema parameters/returns, side_effects, confirmation_required, auth_level), optional events, and the signature field.
Sources: portal/pcp/core/src/manifest/manifest.rs#L1-L98, openspec/specs/pcp/pcp-v4.2-full.md#L4594-L4612
Further Reading
Section titled “Further Reading”PCP is one of four application framework subsystems. To understand how PCP capabilities feed into Elara’s decision-making, see Context Engine: Event Ingestion, Decision Making, and Template Synthesis. For the user-facing launcher that consumes PCP’s capability registry for action dispatch, see Universal Launcher: Action Registry, Fuzzy Search, and Plugin System. For the testing and CI strategies that validate PCP’s performance SLOs and protocol correctness, see Testing Strategy: Unit Tests, Property Tests, Fuzzing, and Benchmarks.