Quickstart
Overview
Section titled “Overview”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 daemon 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 running PCP daemon
- The
pcp-coreandpcp-ipccrates, available from the PCP workspace
No registration RPC and no manual handshake. Your application serves its capabilities on its own PCP socket, and the daemon discovers it through its desktop entry and signed manifest.
Define Capabilities
Section titled “Define Capabilities”A Tier 1 application implements the PcpServer trait from pcp-ipc. This trait tells the daemon what your app can do and how to invoke each operation. The server receives capability IDs, parameter payloads, and an invocation context, then returns structured results or typed errors.
Here is the server for the text editor:
use pcp_core::prelude::*;
/// A Tier 1 text editor exposing semantic capabilities.pub struct EditorServer { manifest: CapabilityManifest,}
#[async_trait]impl PcpServer for EditorServer { async fn manifest(&self) -> CapabilityManifest { self.manifest.clone() }
async fn invoke( &self, capability_id: &str, _ctx: &InvocationContext, params: serde_json::Value, ) -> Result<CapabilityResult, PcpError> { 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(PcpError::InvalidParams( "missing 'text'".into(), ))?; self.save_document(content).await?; Ok(CapabilityResult::success()) } _ => Err(PcpError::NotFound(capability_id.into())), } }
async fn validate_params( &self, capability_id: &str, params: &serde_json::Value, ) -> Result<(), PcpError> { if capability_id == "editor.save" && params["text"].as_str().is_none() { return Err(PcpError::InvalidParams("missing 'text'".into())); } Ok(()) }
async fn subscribe_events(&self, _event_types: Vec<String>) -> Result<(), PcpError> { Ok(()) }
async fn query_dynamic_state(&self, _capability_id: &str) -> Result<serde_json::Value, PcpError> { Ok(serde_json::json!({ "dirty": self.is_dirty() })) }
async fn shutdown(&self) { self.flush().await; }}A few things to note about this implementation:
- The
invokemethod dispatches on the capability ID string. The daemon routes each invocation to this method over your app’s socket, with the ID, a JSON parameter payload, and anInvocationContextthat carries session and auth metadata. editor.readis a read-only operation. It returns the document text in thedatafield and setssuccesstotrue. No undo token is needed because the operation has no side effects.editor.saveextracts thetextparameter from the payload, validates its presence, and persists it. TheInvalidParamserror variant makes missing or malformed inputs explicit to the caller.validate_paramsmirrors the manifest schemas. The daemon calls it beforeinvokeso a malformed payload never reaches your execution path.- Any unrecognized capability ID returns
PcpError::NotFound. The daemon uses this to distinguish between an unsupported operation and a transient failure.
The get_document_text, save_document, and flush 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.
To serve the implementation, wrap it in a PcpServerRunner from pcp-ipc. The runner listens on your app’s socket, decodes incoming frames, and dispatches to these trait methods.
Ship the Manifest
Section titled “Ship the Manifest”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 daemon 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 daemon 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 daemon uses these schemas to validate invocations before they reach your server. A call to editor.save that omits the text parameter never touches your code; the daemon catches the schema violation and returns an error to the caller.
Register with the Daemon
Section titled “Register with the Daemon”Tier 1 applications do not register themselves manually. The daemon scans desktop entries and well-known manifest locations at startup. When it finds your com.example.editor manifest, it:
- Verifies the Ed25519 signature against the trusted key set.
- Probes the binary for sanity.
- Parses the manifest and indexes each capability by ID, category, and app origin.
- Connects to your application’s PCP socket as a client.
From that point on, SI can invoke editor.read or editor.save by capability ID, and the daemon routes the call over your app’s socket to your PcpServer implementation. Framing, connection management, and reconnection are handled by the protocol libraries on both sides. Discovery and binding happen as a consequence of the manifest existing in the right place with a valid signature and your app listening on its socket.
If the manifest is malformed, the signature fails, or a capability ID collides with an already-registered capability, the daemon logs the failure and skips that application. It does not crash, and it does not block other applications from registering.
Key Constraints
Section titled “Key Constraints”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: invocations operate on your data structures, not your UI layer.
- No script injection. The
evaluate_scriptmechanism 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.readis addressable by any part of the SI layer. Choose IDs that are specific enough to avoid collisions across applications. Theapp_idnamespace in the manifest helps, but capability IDs themselves must be unique within the daemon’s registry.