Centralized policy decision service for multi-agent deployments. Moves enforcement logic out of individual agent processes into a shared, auditable service.
Agent 1 ──> Sidecar 1 ──┐
├──> Control Plane (port 9090)
Agent 2 ──> Sidecar 2 ──┘ │
├── Policy Engine (YAML rules)
├── Global Taint Registry
├── Request Nonce Store (replay protection)
├── SQLite Audit Log
└── Ed25519 Decision Signing
Each sidecar sends a POST /decision request before executing a tool call. The control plane evaluates the request against loaded policies, enriches taint labels from the global registry, signs the verdict with Ed25519, and returns a signed decision receipt. The sidecar enforces the decision locally.
- Agent issues tool call to sidecar
- Sidecar serializes the call into a
DecisionRequest(with optionalrequestNonce) - Sidecar sends
POST /decisionto control plane - Control plane checks
requestNoncefor replay (rejects duplicates with 409) - Control plane enriches taint labels from the global registry
- Control plane evaluates policies via
PolicyEngine - Control plane generates
decisionId, signs the receipt with Ed25519 - Sidecar receives
DecisionResponsewith full signed receipt - If
allow: sidecar executes the tool call locally - If
denyorrequire-approval: sidecar rejects without executing
Request:
{
"principalId": "agent-1",
"toolClass": "http",
"action": "POST",
"parameters": { "url": "https://api.example.com/data" },
"taintLabels": [],
"runId": "run-abc123",
"timestamp": "2026-03-10T12:00:00.000Z",
"requestNonce": "client-unique-nonce-001"
}The requestNonce field is optional. When provided, the control plane rejects duplicate nonces within the TTL window (5 minutes), returning HTTP 409.
Response (signed receipt):
{
"decision": "deny",
"decisionId": "dec-1741612800000-a1b2c3d4",
"reason": "Tainted request blocked",
"policyVersion": "1.0.0",
"policyHash": "3f2a7b9c1e4d5a08",
"kernelBuild": "arikernel-cp-0.1.2",
"timestamp": "2026-03-10T12:00:00.001Z",
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"signature": "ed25519-hex-signature-128-chars...",
"matchedRule": { "id": "deny-tainted-http", "..." : "..." },
"taintLabels": [{ "source": "web", "origin": "injected", "..." : "..." }]
}Every response includes:
| Field | Description |
|---|---|
decisionId |
Unique identifier for this specific decision |
policyVersion |
Version label of the loaded policy |
policyHash |
SHA-256 prefix (16 hex chars) of the loaded policy ruleset |
kernelBuild |
Kernel build identifier |
nonce |
16-byte cryptographic nonce (server-generated) |
signature |
Ed25519 signature over the canonical receipt payload |
Register taint labels in the global cross-agent registry.
{
"principalId": "agent-a",
"runId": "run-1",
"labels": [{ "source": "web", "origin": "https://evil.com", "confidence": 0.95, "addedAt": "..." }],
"resourceIds": ["/shared/data.json"]
}Query taint labels on a resource.
{ "resourceId": "/shared/data.json" }Unauthenticated liveness probe. Returns { "status": "ok" }.
The control plane provides two layers of replay protection:
Clients can include a requestNonce in the decision request. The control plane stores recent nonces in a time-windowed store (5-minute TTL). If a duplicate nonce is received, the request is rejected with HTTP 409:
{ "error": "Duplicate requestNonce — request already processed" }Every signed response includes a unique 16-byte nonce. Clients verifying responses use a NonceStore to track seen nonces and reject replayed decisions:
import { DecisionVerifier, NonceStore } from "@arikernel/control-plane";
const verifier = new DecisionVerifier(publicKeyHex);
const nonces = new NonceStore();
const valid = verifier.verify(response, nonces);
// Second verification of the same response returns falseEvery decision is signed with Ed25519. The canonical payload is a deterministic JSON string of the receipt fields (sorted keys), excluding the signature itself.
Signed receipt fields (canonicalized):
{ decision, decisionId, kernelBuild, nonce, policyHash, policyVersion, reason, timestamp }
Signing flow:
- Serialize the 8 fields above as sorted-key JSON
- Sign the UTF-8 bytes with Ed25519 private key
- Encode signature as hex (128 characters)
CLI verification:
arikernel verify-receipt receipt.json --public-key <hex-encoded-public-key>This verifies:
- All required fields are present
- Ed25519 signature is valid (if public key provided)
- Nonce and signature format are correct
Programmatic verification:
import { DecisionVerifier, NonceStore } from "@arikernel/control-plane";
const verifier = new DecisionVerifier(publicKeyHex);
const nonces = new NonceStore();
// Verify signature + check nonce freshness
const valid = verifier.verify(response, nonces);┌─────────────────────────────────────────────────┐
│ Trust Boundary │
│ │
│ Control Plane (trusted) │
│ - Holds Ed25519 private key │
│ - Evaluates policy │
│ - Signs every decision │
│ - Maintains global taint registry │
│ - Records audit trail │
│ │
├──────────────────────────────────────────────────┤
│ │
│ Sidecars (semi-trusted) │
│ - Hold Bearer auth token │
│ - Hold public key for verification │
│ - Enforce decisions locally │
│ - Fail-closed when CP unreachable │
│ │
├──────────────────────────────────────────────────┤
│ │
│ Agents (untrusted) │
│ - Tool calls intercepted by sidecar │
│ - Cannot bypass enforcement for mediated calls │
│ - Cannot forge decision receipts │
│ │
└──────────────────────────────────────────────────┘
Key trust properties:
- Non-repudiation: Every decision is signed; the control plane cannot deny having made it
- Tamper evidence: Modifying any receipt field invalidates the Ed25519 signature
- Replay resistance: Request nonces prevent duplicate processing; response nonces prevent replay attacks
- Policy binding:
policyHashin the receipt cryptographically binds the decision to the specific policy version evaluated - Fail-closed: Sidecars deny tool calls when the control plane is unreachable
Recommended: Use local sidecar enforcement (the default). The sidecar evaluates policies in-process with no control plane dependency. This eliminates network latency, availability risk, and the receipt-substitution attack surface.
import { SidecarServer } from "@arikernel/sidecar";
const sidecar = new SidecarServer({
preset: "safe",
// decisionMode defaults to "local" — no control plane needed
});Warning: Remote decision mode is experimental and adds latency, availability dependency, and receipt-substitution attack surface. It may be removed in a future release. If you use it,
controlPlanePublicKeyis strongly recommended.
Set decisionMode: "remote" to delegate policy decisions to the control plane:
import { SidecarServer } from "@arikernel/sidecar";
const sidecar = new SidecarServer({
preset: "safe",
decisionMode: "remote",
controlPlaneUrl: "http://localhost:9090",
controlPlaneAuthToken: "shared-secret",
controlPlaneTimeoutMs: 5000,
controlPlanePublicKey: "<64-hex-char-ed25519-public-key>",
});Receipt verification: When controlPlanePublicKey is configured, the sidecar verifies the Ed25519 signature and response nonce on every decision receipt before trusting it. Invalid signatures, tampered fields, and replayed nonces all cause fail-closed denial. Strongly recommended when using remote mode.
Fail-closed behavior: The sidecar returns HTTP 503 and does not execute the tool call when:
- The control plane is unreachable within the timeout window
- Receipt signature verification fails (when public key is configured)
- A replayed response nonce is detected
import { ControlPlaneServer, generateSigningKey } from "@arikernel/control-plane";
const server = new ControlPlaneServer({
signingKey: generateSigningKey(),
policy: "./policies/safe-defaults.yaml",
authToken: "dev-secret",
port: 9090,
});
await server.listen();
console.log(`Public key: ${server.publicKeyHex}`);
console.log(`Policy hash: ${server.policyHash}`);- Generate an Ed25519 signing key:
generateSigningKey()returns a 64-char hex seed - Store the seed securely (environment variable, secrets manager)
- Start the control plane with the signing key and YAML policies
- Configure each sidecar with
decisionMode: "remote"and the control plane URL - Distribute the public key (
server.publicKeyHex) to clients for signature verification
┌─────────────────────────────────┐
│ Control Plane │
│ signingKey: $CP_SIGNING_KEY │
│ policy: /etc/ari/policies.yaml │
│ authToken: $CP_AUTH_TOKEN │
│ auditLog: /var/lib/ari/cp.db │
│ port: 9090 │
└─────────────┬───────────────────┘
│
┌────────┼────────┐
│ │ │
Sidecar Sidecar Sidecar
:8787 :8788 :8789
| Variable | Description |
|---|---|
CP_SIGNING_KEY |
64-char hex Ed25519 seed |
CP_AUTH_TOKEN |
Bearer token for sidecar authentication |
CP_POLICY_PATH |
Path to YAML policy file |
CP_AUDIT_LOG |
Path to SQLite audit database |
CP_PORT |
Listen port (default: 9090) |
The control plane maintains a cross-agent taint registry. When Agent A contaminates a shared resource (file, URL, database table), the control plane tracks this. When Agent B later makes a request touching that resource, the control plane enriches the request's taint labels with Agent A's contamination data before evaluating policies.
This enables detection of cross-agent relay attacks without requiring sidecars to share state directly.
All decisions are stored in a SQLite database with the following schema:
| Column | Description |
|---|---|
principal_id |
Who made the request |
tool_class |
Tool type (http, file, shell, etc.) |
action |
Action attempted |
decision |
Verdict (allow, deny, require-approval) |
reason |
Why the decision was made |
timestamp |
When the decision was made |
policy_version |
Policy version used |
run_id |
Run correlation ID |
signature |
Ed25519 signature for tamper evidence |
Query the audit log programmatically:
server.audit.queryRecent(100);
server.audit.queryByPrincipal("agent-1");Export the audit log as JSONL for external analysis:
arikernel control-plane export-audit --db ./control-plane-audit.db --out audit.jsonlOr programmatically:
const jsonl = server.audit.exportJsonl();Each line is a JSON object with the full audit row.
The control plane targets sub-50ms decision latency. Key design choices:
- In-memory policy engine (no disk I/O on the decision path)
- In-memory taint registry (no database queries during evaluation)
- In-memory nonce store for replay protection
- WAL-mode SQLite for non-blocking audit writes
- Ed25519 signing (~microseconds per signature)