Skip to content

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

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

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

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

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

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.

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

The AdapterManager in portal/pcp/registry/src/lifecycle/ wraps each adapter in a state machine with enforced transition validation. Five states govern the lifecycle: InitializingReadyDegradedFailedShutdown. 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

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

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).

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

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

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

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

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

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.

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

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

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

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

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.

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

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: Pinghandle_ping, RegistryQueryhandle_registry_query, DetectRequesthandle_detect_request, ExecuteRequesthandle_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

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

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

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

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

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

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: PendingValidatedStaleRevalidating → 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

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

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

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.