Control-plane API¶
The SDK enforces locally, but in production it talks to a control plane — a separate service that distributes signed policy bundles and receives the content-free audit stream. This document is the contract between the two: the HTTP endpoints, how they are authenticated, and the Ed25519 signing scheme.
The client lives in parapetai_agent/control_plane.py; identity and signing in
parapetai_agent/pep_identity.py and parapetai_agent/signing.py.
Roles¶
- PEP (Policy Enforcement Point) — your agent process, running this SDK. It pulls policy and pushes decisions. It never receives commands.
- Control plane — issues agent credentials, stores per-agent bundles, verifies signatures, and ingests the audit/telemetry stream.
Trust flows one way: the PEP authenticates itself to the control plane. The control plane is authenticated by TLS.
Credentials¶
Two secrets, provisioned once and never re-shown:
| Credential | Created by | Held by | Used for |
|---|---|---|---|
agent_id + agent_secret |
Issued once by the control plane at provisioning, out of band | The agent (config / env) | Bearer auth on every call; only the secret's hash is stored server-side |
| Ed25519 keypair | The agent, on first run (pep_identity.load_or_create_keypair) |
Private key never leaves the agent | Signing bundle-pull and heartbeat requests |
The private key is written to ~/.parapetai/pep_ed25519.key (0600), overridable
via PARAPETAI_PEP_KEY_PATH. Where no filesystem is writable (e.g. Lambda), an
ephemeral in-memory key is used instead — still a stable identity for the
process lifetime.
Endpoints¶
Agent API — prefix /api/v1¶
This is the complete protocol a PEP speaks. Every endpoint here is
agent-authenticated: a bearer agent_secret, and for the two that matter most,
an Ed25519 signature as well.
| Method & path | Auth | Purpose |
|---|---|---|
POST /api/v1/keys |
Bearer agent_secret |
Register this PEP's Ed25519 public key. Idempotent; rotation demotes the previous key so in-flight requests still verify. |
GET /api/v1/bundle |
Bearer + signed | Pull the agent's current signed policy bundle. Send If-None-Match: <etag>; a 304 Not Modified means keep the cached bundle. |
POST /api/v1/fleet/heartbeat |
Bearer + signed | Report liveness + the enforcing policy generation/digest. Response may carry rotate_key: true. |
POST /api/v1/audit |
Bearer + signed | Ingest content-free decision records. An alternative to the OTLP path below; this SDK uses OTLP. |
The control plane exposes other routes — provisioning, the operator console,
tenant and fleet administration. They are not part of this protocol, are not
callable with an agent secret, and are deliberately not documented here: an
adopter never needs them, and this SDK never calls them. agent_id and
agent_secret are issued to you once at provisioning, out of band.
vendor_scoped_resources on the bundle response¶
Since 0.7.0, the GET /api/v1/bundle response may carry a top-level
vendor_scoped_resources: bool field — lets a control-plane operator turn
on opt-in vendor-scoped Cedar resource construction
for a tenant without a code change on the PEP side. Resolved once, at
bootstrap by Governor.from_control_plane() / bootstrap_engine() —
unlike .cedar policy/entities content, it does not hot-reload mid-process
on a later poll; a tenant-level change takes effect on this PEP's next
full bootstrap (process restart).
OTLP receiver — standard paths¶
| Method & path | Auth | Purpose |
|---|---|---|
POST /v1/traces |
Bearer agent_secret |
Ingest OpenTelemetry spans (the decision stream). Standard OTLP/HTTP protobuf. |
POST /v1/logs |
Bearer agent_secret |
Ingest OpenTelemetry logs. |
The bearer secret in the Authorization header is what identifies which
agent's spans/logs these are. See OBSERVABILITY.md.
Request signing¶
Once a PEP has registered a public key, every subsequent bundle-pull and heartbeat must carry a valid signature (gradual enforcement: an agent with no registered key is served unsigned, for backwards compatibility, until it registers one).
Two headers are added:
The signed bytes are exactly (parapetai_agent/signing.py):
methodupper-cased;pathis the request path only (no query string).signed_atis the literal header string, signed byte-for-byte — both sides use the same string, never a re-parsed float, so formatting can't drift.bodyis the raw request body (empty for a bodylessGET).
The control plane verifies against the agent's current or previous registered key, within a bounded clock-drift window that limits replay.
Lifecycle (typical)¶
provision (operator) out of band -> agent_id + secret
first run (agent) POST /api/v1/keys register public key
steady state (agent loop)
every N seconds GET /api/v1/bundle (signed) -> bundle or 304
POST /api/v1/fleet/heartbeat (signed) -> ok / rotate_key
per decision POST /v1/traces, /v1/logs content-free spans/logs
run_bundle_poller() in control_plane.py drives the steady-state loop: it
fetches, writes the bundle to policy_dir for restart persistence, hot-applies
it to the live PolicyEngine, and heartbeats — all with the same signing key.
Running without a control plane¶
Everything above is optional. Point the SDK at local Cedar files and it enforces with no network at all:
from parapetai_agent import build_middleware
mw = build_middleware(policy_dir="./policies") # no control_plane_url / secret
Bundle pull, heartbeat, and remote audit simply don't run. Decisions can still be exported to any OTLP endpoint you configure yourself.