Skip to content
Portal Control Protocol

Capability Manifest

A Capability Manifest declares the full set of operations a Tier 1 application exposes to the compositor. 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 compositor verifies the signature at registration time. On verification failure, the app falls back to Tier 2 (AT-SPI2-derived capabilities).

A single capability declaration within a manifest. Each capability describes one operation the app can perform.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Capability",
"type": "object",
"required": ["id", "name", "description", "category", "parameters", "returns"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)+$",
"description": "Dot-separated capability identifier. E.g., 'mail.search', 'editor.save'."
},
"name": {
"type": "string",
"description": "Human-readable capability name."
},
"description": {
"type": "string",
"description": "What this capability does."
},
"category": {
"type": "string",
"enum": [
"APP_MANAGEMENT",
"SURFACE_MANAGEMENT",
"ELEMENT_INTERACTION",
"TEXT_INPUT",
"TEXT_QUERY",
"NAVIGATION",
"STATE_QUERY",
"SYSTEM",
"MEDIA_PLAYBACK",
"COMMUNICATION",
"FILE_OPERATION",
"HARDWARE"
],
"description": "Intent category from the taxonomy."
},
"parameters": {
"type": "object",
"description": "JSON Schema describing accepted parameters. Use required/optional/default/type."
},
"returns": {
"type": "object",
"description": "JSON Schema describing the return value."
},
"side_effects": {
"type": "string",
"enum": ["none", "read", "write", "destructive", "network", "system"],
"description": "What this capability does to the system. Used for permission checks."
},
"confirmation_required": {
"type": "boolean",
"default": false,
"description": "If true, requires user confirmation before execution."
},
"undo_window_ms": {
"type": ["integer", "null"],
"description": "If set, the action can be undone within this time window."
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+$",
"default": "1.0",
"description": "Capability version. Used for compatibility."
},
"auth_level": {
"type": "string",
"enum": ["public", "user", "privileged", "system"],
"default": "user",
"description": "Minimum trust level required."
}
}
}

The category field places each capability in the intent taxonomy. This is used for routing and permission scoping.

Category Purpose
APP_MANAGEMENT Launch, quit, install, configure applications
SURFACE_MANAGEMENT Move, resize, tile, minimize, close windows
ELEMENT_INTERACTION Click buttons, toggle checkboxes, select items
TEXT_INPUT Type text, insert at cursor, replace selection
TEXT_QUERY Read content, search, extract text
NAVIGATION Open URLs, switch tabs, navigate within apps
STATE_QUERY Get counts, check status, list items
SYSTEM System-level operations (audio, display, power)
MEDIA_PLAYBACK Play, pause, skip, seek media
COMMUNICATION Send messages, make calls
FILE_OPERATION Move, copy, delete files
HARDWARE Interact with devices (camera, microphone)

The side_effects field determines the permission check behavior. Operations with no side effects can be auto-approved. Destructive and network operations require confirmation or elevated trust.

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 compositor and System Intelligence.

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

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CapabilityManifest",
"type": "object",
"required": ["app_id", "version", "manifest_version", "capabilities"],
"properties": {
"app_id": {
"type": "string",
"description": "Application identifier matching desktop_id."
},
"version": {
"type": "string",
"description": "Application version."
},
"manifest_version": {
"type": "string",
"pattern": "^\\d+\\.\\d+$",
"default": "1.0",
"description": "Manifest schema version. Used for compatibility."
},
"capabilities": {
"type": "array",
"items": { "$ref": "Capability" },
"description": "All capabilities this app exposes."
},
"events": {
"type": "array",
"items": {
"type": "object",
"required": ["event_type", "description"],
"properties": {
"event_type": { "type": "string" },
"description": { "type": "string" },
"payload_schema": { "type": "object" }
}
},
"description": "Custom events this app emits."
},
"signature": {
"type": ["string", "null"],
"description": "Ed25519 signature over the manifest content (excluding this field). Compositor verifies against the app's public key."
}
}
}

Tier 1 applications implement this trait to handle capability invocations. The compositor calls invoke when System Intelligence dispatches an intent to a Tier 1 capability.

#[async_trait]
pub trait CapabilityHandler: Send + Sync {
/// Returns the capability manifest for this handler.
fn manifest(&self) -> &CapabilityManifest;
/// Execute a capability invocation.
async fn invoke(
&self,
capability_id: &str,
params: serde_json::Value,
context: &InvocationContext,
) -> Result<CapabilityResult, CapabilityError>;
/// Optional: validate parameters before execution.
fn validate_params(
&self,
capability_id: &str,
params: &serde_json::Value,
) -> Result<(), ValidationError> {
let cap = self.manifest()
.capabilities
.iter()
.find(|c| c.id == capability_id)
.ok_or(ValidationError::UnknownCapability(capability_id.to_string()))?;
jsonschema::validate(&cap.parameters_schema, params)
.map_err(ValidationError::SchemaViolation)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvocationContext {
pub session_id: Option<SessionId>,
pub source: InvocationSource,
pub timestamp: DateTime<Utc>,
pub app_id: AppId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InvocationSource {
SystemIntelligence,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityResult {
pub success: bool,
pub data: Option<serde_json::Value>,
pub state_after: Option<serde_json::Value>,
pub undo_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CapabilityError {
NotFound(String),
InvalidParams(String),
ExecutionFailed { reason: String, recoverable: bool },
PermissionDenied { reason: String },
Timeout { capability_id: String, elapsed_ms: u64 },
AppUnresponsive { app_id: AppId },
}

The default validate_params implementation performs JSON Schema validation using the capability’s parameters_schema. Applications can override this for custom validation logic (cross-field checks, business rules).

Manifests are signed with Ed25519 to ensure authenticity and integrity. The signing and verification flow works as follows.

Key Path Permissions
Developer private key ~/.config/portal-{app_id}/ed25519.key 0600 (owner read/write only)
Trusted public key /etc/portal/keys/trusted/{app_id}.pub 0644 (world-readable)

Private keys are 32 raw bytes. Public keys are derived from the private key at key generation time.

The signature field in the manifest uses the format "ed25519:<base64>". The base64 portion is the 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. Use compact JSON separators (no whitespace after commas or colons).
  1. The compositor 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 at /etc/portal/keys/trusted/{app_id}.pub.
  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).

Key rotation is supported. 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.

The native 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.

Tier 1 PCP capabilities operate on the application’s data model in Rust. The renderer (webview, terminal canvas, or any other display surface) is a pure consumer of state.

The following APIs are forbidden in Tier 1 capability handlers:

  • evaluate_script() / execute_script() (JavaScript injection into a webview)
  • document.execCommand() (browser formatting commands)
  • Any UI automation API that operates on the rendered surface rather than the underlying data

These restrictions apply only to PCP capability handlers. The UI layer itself (toolbar buttons, keyboard shortcuts) may use these APIs for direct user interaction.

Capability IDs use the format {domain}.{app_id}.{action} where app_id may contain dots (reverse-DNS names supported). The minimum is three dot-separated parts. Example: editor.org.xfce.mousepad.read (domain=editor, app=org.xfce.mousepad, action=read).

Last updated: