Skip to content
Portal Control Protocol

Audit Trails

Every PCP action produces an audit entry. This is not optional and not configurable. The execution pipeline cannot complete a capability invocation without writing to the audit log. If the write fails, the invocation does not proceed.

This design makes audit logging a structural property of the protocol rather than a policy decision. An application cannot request silent execution. A compromised adapter cannot skip logging. The audit trail is part of the execution path itself, which means any attempt to bypass it requires breaking the pipeline in a way that would be detectable at the compositor level.

Audit entries serve two purposes. First, they provide a chronological record of every capability invocation across the system, which administrators can use for incident investigation and compliance reporting. Second, they feed into the runtime revocation system, allowing the compositor to make immediate decisions about capability status based on observed behavior.

Audit entries are written as JSON Lines (JSONL). Each line is a self-contained JSON object appended to a per-domain log stream. The format is designed to be machine-parseable without requiring an external schema, while remaining readable enough for manual inspection during debugging.

Every entry contains the following fields:

Field Type Description
timestamp string ISO 8601 UTC time of the invocation
transaction_id string UUID correlating multi-step operations
domain string One of app, compositor, or system
capability_id string The invoked capability, e.g. mail.send
target string The element or application ID acted upon
parameters object Invocation parameters, with sensitive fields redacted
result string success or failure with an error code
duration_ms integer Wall-clock execution time in milliseconds
source string Invocation origin: voice, gesture, or programmatic

A typical entry looks like this:

{
"timestamp": "2026-08-12T14:32:01.847Z",
"transaction_id": "a3f7b2c1-8e4d-4f6a-9b1c-3d5e7f8a0b2c",
"domain": "app",
"capability_id": "mail.send",
"target": "com.example.mailclient",
"parameters": {
"recipient": "[email protected]",
"subject": "Project update",
"body": "[REDACTED]"
},
"result": "success",
"duration_ms": 42,
"source": "voice"
}

The transaction_id field links entries that belong to the same logical operation. A voice command that triggers a confirmation flow, then executes a capability, then logs a result, shares the same transaction ID across all three entries. This makes it possible to reconstruct the full lifecycle of an operation without relying on timing or ordering heuristics.

Sensitive parameters, such as message bodies, authentication tokens, and file contents, are redacted before the entry is written. The redaction rules are defined per-capability in the capability registry. Redaction is applied by the execution pipeline, not by the calling application, so an application cannot influence what gets logged.

Each privilege domain maintains its own audit trail. The three domains, app, compositor, and system, write to separate log streams that are isolated from one another at the storage level.

An app-domain adapter can only append entries to the app audit stream. It cannot read from, write to, or modify the compositor or system streams. This isolation is enforced by the storage layer, not by application-level checks. Even if an app-domain adapter gains elevated access within its own domain, it cannot reach into another domain’s audit log.

The practical implication is that a compromised application cannot cover its tracks. If an app invokes a capability that triggers a compositor-level response, the compositor writes its own entry to its own stream. The app cannot delete or alter that entry. Conversely, the compositor cannot be tricked into writing a benign entry on behalf of a malicious app, because the compositor’s logging is driven by its own execution path, not by application-provided data.

PCP supports dynamic capability revocation at runtime. Administrators can remove an application’s access to a specific capability without restarting the application or the compositor. This is handled through the revoke.app capability, which requires AuthLevel::Privileged with AuthPrincipal::Admin.

When revocation is triggered, the compositor applies an overlay marker to the affected capability entry in the capability registry. This marker changes the entry’s state to revoked or deprecated, depending on whether the revocation is temporary or permanent. The capability remains in the registry for audit purposes, but the execution pipeline treats it as unavailable for new invocations.

The application receives a CAPABILITY_REVOKED event through its event channel. This event tells the application which capability was revoked and the stated reason. The application is expected to handle this event gracefully, falling back to alternative behavior or informing the user that a feature is unavailable. The application is not forced to terminate.

Existing in-flight invocations of the revoked capability are allowed to complete. The compositor does not interrupt running operations. Once those operations finish, any subsequent invocation attempt for the revoked capability is rejected with an error code of capability.revoked. The rejection is logged in the audit trail, creating a clear record that the revocation took effect.

Revocation itself is an audited action. The administrator’s revocation request produces an entry in the system audit trail, including the target application, the capability ID, and the reason. If the capability is later reinstated, that reinstatement is also logged.

Capabilities can be reinstated without restarting the application. An administrator issues a new request that removes the overlay marker, and the execution pipeline resumes treating the capability as available. The application receives a CAPABILITY_REINSTATED event.

// Administrator revokes a capability
pcp.revoke(RevokeRequest {
app_id: "com.example.app".into(),
capability_id: "mail.send".into(),
reason: "Security policy violation".into(),
}).await?;

The revoke.app capability is itself a privileged capability. It cannot be invoked by applications or by lower-authentication principals. The audit trail for revocation events is written to the system domain, which app-domain adapters cannot access, ensuring that revocation records are tamper-resistant even in the event of an application compromise.

Last updated: