Skip to content
Portal Control Protocol

Quickstart

This quickstart walks through building a Tier 1 native application that exposes PCP capabilities to the System Intelligence layer. You’ll create a simple text editor that registers two capabilities: editor.read to retrieve the current document content, and editor.save to write new content back. By the end, the compositor will discover your app at startup, index its capabilities, and make them available to SI for invocation.

Prerequisites:

  • A working Rust toolchain (edition 2021)
  • A PCP-compatible compositor (one that embeds PCP Core)
  • The pcp_core crate, available from the PCP registry

No external daemon, no socket setup, no IPC configuration. The compositor handles discovery and registration automatically.

A Tier 1 application implements the CapabilityHandler trait from pcp_core. This trait tells the compositor what your app can do and how to invoke each operation. The handler receives capability IDs, parameter payloads, and an invocation context, then returns structured results or typed errors.

Here is the full handler for the text editor:

use pcp_core::prelude::*;
/// A Tier 1 text editor exposing semantic capabilities.
pub struct EditorCapabilityHandler {
manifest: CapabilityManifest,
}
#[async_trait]
impl CapabilityHandler for EditorCapabilityHandler {
fn manifest(&self) -> &CapabilityManifest {
&self.manifest
}
async fn invoke(
&self,
capability_id: &str,
params: serde_json::Value,
context: &InvocationContext,
) -> Result<CapabilityResult, CapabilityError> {
match capability_id {
"editor.read" => {
let text = self.get_document_text()?;
Ok(CapabilityResult {
success: true,
data: Some(serde_json::json!({ "text": text })),
state_after: None,
undo_token: None,
})
}
"editor.save" => {
let content = params["text"]
.as_str()
.ok_or(CapabilityError::InvalidParams(
"missing 'text'".into(),
))?;
self.save_document(content).await?;
Ok(CapabilityResult::success())
}
_ => Err(CapabilityError::NotFound(capability_id.into())),
}
}
}

A few things to note about this implementation:

  • The invoke method dispatches on the capability ID string. The compositor routes each invocation to this method with the ID, a JSON parameter payload, and an InvocationContext that carries session and auth metadata.
  • editor.read is a read-only operation. It returns the document text in the data field and sets success to true. No undo token is needed because the operation has no side effects.
  • editor.save extracts the text parameter from the payload, validates its presence, and persists it. The InvalidParams error variant makes missing or malformed inputs explicit to the caller.
  • Any unrecognized capability ID returns CapabilityError::NotFound. The compositor uses this to distinguish between an unsupported operation and a transient failure.

The get_document_text and save_document methods are application-level logic. They are not part of the PCP contract. The protocol only cares about the trait boundary: what goes in, what comes out, and whether it succeeded.

Every Tier 1 application ships a CapabilityManifest alongside its binary. This JSON document declares the app’s identity, version, and the full set of capabilities it exposes. The compositor reads this manifest during discovery to build its capability index without needing to instantiate the application.

Here is the manifest for the text editor:

{
"app_id": "com.example.editor",
"version": "1.0.0",
"manifest_version": "1.0",
"capabilities": [
{
"id": "editor.read",
"name": "Read Document",
"description": "Read the current document content",
"category": "TEXT_QUERY",
"parameters": {
"type": "object",
"properties": {}
},
"returns": {
"type": "object",
"properties": {
"text": { "type": "string" }
}
},
"side_effects": "read",
"auth_level": "user"
},
{
"id": "editor.save",
"name": "Save Document",
"description": "Save the document with provided content",
"category": "TEXT_INPUT",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "New document content"
}
},
"required": ["text"]
},
"returns": {
"type": "object",
"properties": {}
},
"side_effects": "write",
"auth_level": "user"
}
]
}

The manifest must be Ed25519-signed. The signature proves that the capabilities declared in the JSON actually belong to the app that ships it. Without a valid signature, the compositor rejects the manifest at registration. Place the signed manifest at the well-known path for your app (conventionally alongside the binary or in a platform-standard manifest directory).

Each capability entry declares its side_effects level (read or write), its auth_level (who is allowed to invoke it), and its parameter and return schemas. The compositor uses these schemas to validate invocations before they reach your handler. A call to editor.save that omits the text parameter never touches your code; the compositor catches the schema violation and returns an error to the caller.

Tier 1 applications do not register themselves manually. The compositor scans well-known paths for signed manifests at startup. When it finds your com.example.editor manifest, it:

  1. Verifies the Ed25519 signature against the trusted key set.
  2. Parses the manifest and indexes each capability by ID, category, and app origin.
  3. Associates the manifest with your application’s capability handler.

From that point on, SI can invoke editor.read or editor.save by capability ID, and the compositor routes the call to your handler in-process. No handshake, no socket, no registration RPC. Discovery and binding happen as a consequence of the manifest existing in the right place with a valid signature.

If the manifest is malformed, the signature fails, or a capability ID collides with an already-registered capability, the compositor logs the failure and skips that application. It does not crash, and it does not block other applications from registering.

Tier 1 integration comes with rules that distinguish it from lower tiers:

  • Operate on the data model, not the rendered surface. Tier 1 capabilities read and write application state directly. They do not query pixel positions, simulate mouse clicks, or read the framebuffer. The point of Tier 1 is semantic access: the compositor invokes operations on your data structures, not your UI layer.
  • No script injection. The evaluate_script mechanism is forbidden in Tier 1 handlers. If you need to execute logic, do it in Rust, not by injecting JavaScript or shell commands into the application’s rendering context.
  • Manifest integrity is enforced. The compositor verifies the Ed25519 signature on every manifest at registration time. A tampered manifest is rejected. A missing signature is rejected. There is no downgrade path to an unsigned manifest.
  • Capability IDs are global. Once registered, a capability like editor.read is addressable by any part of the SI layer. Choose IDs that are specific enough to avoid collisions across applications. The app_id namespace in the manifest helps, but capability IDs themselves must be unique within the compositor’s index.

Last updated: