Skip to content
Portal Control Protocol

Capability Manifest

A Capability Manifest declares the full set of operations a Tier 1 application exposes. Unlike Tier 2 apps, where capabilities are derived by inspecting the accessibility tree, Tier 1 apps define their capabilities explicitly with typed parameters, typed return values, and versioned schemas.

The manifest is shipped alongside the app binary and signed with the developer’s Ed25519 key. The daemon verifies the signature at registration time. On verification failure, the app falls back to Tier 2 (AT-SPI2-derived capabilities).

Each capability in a manifest describes one operation the app can perform.

pub struct Capability {
pub id: String, // fully qualified: domain.app.action
pub name: String, // human-readable name
pub description: String, // what this capability does
pub category: CapabilityCategory, // taxonomy placement
pub parameters: serde_json::Value, // JSON-Schema-shaped
pub returns: serde_json::Value, // JSON-Schema-shaped
pub side_effects: SideEffect, // default None
pub confirmation_required: bool, // default false
pub undo_window_ms: Option<u64>, // undo time window
pub version: String, // default "1.0"
pub auth_level: AuthLevel, // default User
}

CapabilityCategory (16 named variants + Custom)

Section titled “CapabilityCategory (16 named variants + Custom)”
Category Purpose
TextInput Type text, insert at cursor, replace selection
TextQuery Read content, search, extract text
ElementInteraction Click buttons, toggle checkboxes, select items
Navigation Open URLs, switch tabs, navigate within apps
StateQuery Get counts, check status, list items
AppManagement Launch, quit, install, configure applications
SurfaceManagement Move, resize, tile, minimize, close windows
LayoutPreset Apply predefined window arrangements
MediaPlayback Play, pause, skip, seek media
System System-level operations (audio, display, power)
Communication Send messages, make calls
FileOperation Move, copy, delete files
Hardware Interact with devices (camera, microphone)
Accessibility Screen reader, magnifier, and a11y controls
DataManagement Import, export, sync data
MediaControl Volume, brightness, and playback controls
Custom(String) Extension point for app-specific categories
pub enum SideEffect { None, Read, Write, Destructive, Network, System }

Operations with None side effects can be auto-approved. Destructive and Network operations require confirmation or elevated trust. This field drives the permission check behavior in the daemon.

pub enum AuthLevel { Public, User, Privileged, System }

Ordered from lowest to highest privilege. is_at_least() provides comparison. A capability with auth_level: Privileged can only be invoked by callers at Privileged or System level.

Level Meaning
Public No trust required. Read-only state queries.
User Standard user trust. Most capability invocations.
Privileged Elevated trust. System configuration changes.
System System identity only. Reserved for the daemon and the intelligence layer.

Tier 1 applications implement this trait to handle capability invocations. The daemon calls these methods on the per-app socket. This is the entire contract a Tier 1 app implements.

#[async_trait]
pub trait PcpServer: Send + Sync {
/// Returns the capability manifest for this application.
async fn manifest(&self) -> CapabilityManifest;
/// Execute a capability invocation.
async fn invoke(
&self,
capability_id: &str,
ctx: &InvocationContext,
params: Value,
) -> Result<CapabilityResult, PcpError>;
/// Validate parameters before execution.
async fn validate_params(
&self,
capability_id: &str,
params: &Value,
) -> Result<(), PcpError>;
/// Subscribe to event types from this app.
async fn subscribe_events(
&self,
event_types: Vec<String>,
) -> Result<(), PcpError>;
/// Query the app's current dynamic state for a capability.
async fn query_dynamic_state(
&self,
capability_id: &str,
) -> Result<Value, PcpError>;
/// Graceful shutdown. No response expected.
async fn shutdown(&self);
}

Tier 1 capabilities operate on the app’s data model in Rust. The renderer (webview, terminal canvas, or any display surface) is a pure consumer of state. APIs like evaluate_script(), document.execCommand(), or any UI automation that operates on the rendered surface rather than the underlying data model are forbidden in capability handlers.

The daemon passes an InvocationContext to every invoke() call. It carries everything the app needs to make routing and authorization decisions.

pub enum AuthPrincipal {
User, // default -- regular invocation
Admin, // requires admin flag on the invoking client
System, // daemon-internal (auto-degradation, learning recording)
}
pub struct InvocationContext {
pub app_id: String,
pub surface_id: Option<SurfaceId>,
pub element_id: Option<ElementId>,
pub user_confirmed: bool,
pub audit_trail_hash: Option<String>,
pub auth_principal: AuthPrincipal,
}
pub struct CapabilityResult {
pub success: bool,
pub data: Option<serde_json::Value>,
pub error: Option<String>,
pub audit_hash: Option<String>,
pub state_source: StateSource,
}
pub enum StateSource {
Rust, // default -- fulfilled from the app's Rust state model
Renderer, // permitted ONLY for pixel-capture capabilities (screenshots)
Stale, // could not read current state
Unknown, // app cannot determine the source
}

The state_source field tells the caller where the result’s authoritative state lives. Rust is the default for all Tier 1 capabilities. Renderer is only allowed for capabilities that need to capture rendered pixels (screenshots, frame grabs). Stale and Unknown indicate degraded state.

The container that holds all capability declarations for one application, plus optional event definitions and the Ed25519 signature.

pub struct CapabilityManifest {
pub app_id: String,
pub version: String,
pub manifest_version: String, // default "1.0"
pub capabilities: Vec<Capability>,
pub events: Vec<ManifestEvent>,
pub signature: Option<String>, // Ed25519
}
pub struct ManifestEvent {
pub event_type: String,
pub description: String,
pub payload_schema: serde_json::Value,
}

Based on the V5 deployed manifest format, with vendor-specific names replaced by a generic example app:

{
"app_id": "com.example.present",
"version": "0.1.0",
"manifest_version": "1.0",
"capabilities": [
{
"id": "slide.add",
"name": "Add slide",
"description": "Insert a new slide; appends when index is omitted",
"category": "APP_MANAGEMENT",
"parameters": {
"properties": {
"html": { "type": "string" },
"index": { "type": "integer", "minimum": 0 },
"notes": { "type": "string" }
},
"type": "object"
},
"returns": {
"properties": { "index": { "type": "integer" } },
"type": "object"
},
"side_effects": "write",
"confirmation_required": false,
"undo_window_ms": null,
"version": "1.0",
"auth_level": "user"
},
{
"id": "slide.remove",
"name": "Remove slide",
"description": "Delete a slide from the deck",
"category": "APP_MANAGEMENT",
"parameters": {
"properties": { "index": { "type": "integer", "minimum": 0 } },
"required": ["index"],
"type": "object"
},
"returns": {
"properties": { "removed_count": { "type": "integer" } },
"type": "object"
},
"side_effects": "destructive",
"confirmation_required": true,
"undo_window_ms": 10000,
"version": "1.0",
"auth_level": "user"
}
],
"events": [],
"signature": null
}

The slide.add capability is a write operation that can be auto-approved. The slide.remove capability is destructive, requires user confirmation, and allows undo within 10 seconds.

Manifests are signed with Ed25519 to ensure authenticity and integrity.

The signature field uses the format "ed25519:<base64>". The base64 portion is the 64-byte Ed25519 signature over the canonicalized manifest content.

Before signing or verifying, the manifest is canonicalized:

  1. Remove the signature field entirely from the JSON object.
  2. Sort all remaining keys lexicographically.
  3. Serialize with compact JSON (no whitespace after commas or colons).

The canonicalization step ensures that different JSON serializers produce identical byte sequences for the same logical manifest.

  • The developer generates an Ed25519 key pair (32-byte private key, 32-byte public key).
  • The public key is placed in a trusted keys location where the daemon can read it.
  • The developer signs the manifest with the private key and ships the signature inside the manifest.
  • Key rotation: when a developer generates a new key pair, the updated public key must be placed in the trusted keys directory before the new manifest is deployed.
  1. The daemon receives the manifest at registration time.
  2. It extracts the signature field value and removes it from the payload.
  3. It canonicalizes the remaining JSON.
  4. It looks up the trusted public key for the app.
  5. It verifies the Ed25519 signature against the canonicalized payload.
  6. On success, the manifest is accepted and capabilities are registered.
  7. On failure, the app falls back to Tier 2 (AT-SPI2-derived capabilities).

The manifest load and verify operation must complete in under 50ms, with a maximum acceptable latency of 100ms. This covers JSON parsing, canonicalization, and the Ed25519 signature check.

Capability IDs use the format {domain}.{app}.{action} where app may contain dots (reverse-DNS names are supported). The minimum is three dot-separated non-empty parts.

Examples:

  • app.com.example.present.slide.add (domain=app, app=com.example.present, action=slide.add)
  • editor.org.xfce.mousepad.read (domain=editor, app=org.xfce.mousepad, action=read)

The CapabilityId::new() constructor validates this format at construction time.

Last updated: