Trust boundaries

Public ingress is intentionally tiny.

The proxy is the only data‑plane endpoint an agent should call directly. Brain, Policy, HIL, Ledger, and Identity stay private or operator‑gated, so auth failures fail closed before side effects reach an upstream tool.

Agent MCP client

Sends JSON‑RPC plus optional callback, grant, and attestation headers.

Data plane POST /mcp

Requires mTLS SVID or lite bearer auth; stamps the correlation id.

Private Brain + Policy

Service-to-service calls only; produces semantic and Rego verdict signals.

Operator HIL + Identity

Approvals and credential minting stay behind operator/session controls.

Proof Ledger export

Verification and correlation replay expose audit evidence, not write paths.

Matrix

Endpoint, auth, success, failure, proof row.

Use this table as the integration index before jumping into a per-service contract. The proof column tells you which ledger event should exist when the request finishes.

Filter by boundary or operational use. Rows keep the same wire contracts; filters only narrow the table.

Service Endpoint Auth Success Failure Proof row
Proxy Data plane POST /mcp mTLS SVID or lite bearer 200 upstream response, 202 pending 403 deny, 426 legacy client, 429 velocity, 503 dependency proxy.verdict, upstream.outcome
Brain Private POST /inspect Private service network InspectionResponse 503 model/provider unavailable brain.inspection
Policy Private POST /evaluate mTLS service caller allow, deny, or review 400 malformed input, 503 store unavailable policy.evaluated
HIL Operator POST /pending, POST /decide/{id} Proxy service token, operator session Pending created or decision accepted 401, 404, expiry closed hil.created, hil.approved, hil.denied
Ledger Proof POST /log, GET /verify mTLS service caller, read auth on console Row appended or chain valid 503 store unavailable; invalid chains are reported in the 200 body Canonical hash-chain row
Identity Operator POST /svid, POST /grant Admin token, OIDC token, SVID Certificate or delegation grant 401, attestation or envelope mismatch identity.svid_issued, identity.grant_issued
Envelope

JSON‑RPC 2.0 + correlation header.

Every MCP request is JSON‑RPC 2.0. The proxy stamps a UUID‑v4 correlation_id on ingress and threads it through every downstream call so a single ledger row joins the proxy / brain / policy / HIL events.

{
  "jsonrpc": "2.0",
  "id":      1,
  "method":  "call_tool",
  "params":  { "name": "send_email", "arguments": { /* … */ } }
}

Headers (proxy ingress):

  • X‑Clavenar‑Correlation‑Id — proxy‑stamped, propagates downstream + into the ledger
  • X‑Clavenar‑Demo‑Prefix — 8‑hex from the demo‑session JWT (console‑demo only)
  • X‑Clavenar‑Callback‑URL — async HIL: where to POST the operator decision
  • X‑Clavenar‑Grant — delegation grant from /grant (multi‑tenant)
  • X‑Clavenar‑Attestation — per‑request override of attestation claims
Runnable calls

Copy, set the host, and keep the correlation id.

These examples use placeholder hosts and credentials so they can be adapted to Lite, dev, or a private deployment. Copy buttons are added automatically by the docs runtime.

IDEMPOTENCY_ID="$(uuidgen)"
curl --http1.1 https://proxy.internal.example/mcp \
  --cert ./agent-svid.pem \
  --key ./agent-svid-key.pem \
  --cacert ./bundle.pem \
  -H 'Content-Type: application/json' \
  -H 'X-Clavenar-Server-Execution-Contract: clavenar.server-execution/v1' \
  -H "X-Clavenar-Idempotency-Id: $IDEMPOTENCY_ID" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "call_tool",
    "params": {
      "name": "refund.issue",
      "arguments": { "amount": 8500, "currency": "USD" }
    }
  }'
IDEMPOTENCY_ID="$(uuidgen)"
curl https://localhost:8787/mcp \
  -H 'Authorization: Bearer '"$CLAVENAR_LITE_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'X-Clavenar-Server-Execution-Contract: clavenar.server-execution/v1' \
  -H "X-Clavenar-Idempotency-Id: $IDEMPOTENCY_ID" \
  -H 'X-Clavenar-Callback-URL: https://agent.example/hil/callback' \
  -d '{
    "jsonrpc": "2.0",
    "id": "demo-1",
    "method": "call_tool",
    "params": {
      "name": "crm.update_customer",
      "arguments": { "customer_id": "cus_123", "tier": "enterprise" }
    }
  }'
CORRELATION_ID="3ab2e8c2-0000-4000-9000-8fb77a123456"

curl "https://console.internal.example/api/audit/correlation/${CORRELATION_ID}" \
  -H 'Authorization: Bearer '"$OPERATOR_TOKEN"

curl "https://ledger.internal.example/verify" \
  --cert ./service-svid.pem \
  --key ./service-svid-key.pem \
  --cacert ./bundle.pem
clavenar‑proxy

Data plane · POST /mcp

The agent's MCP transport target. mTLS client cert (SVID) or bearer token (lite) authenticates the agent. The proxy fans out to Brain → Policy → HIL → Ledger and returns a verdict that proxies the upstream response (allow) or fails closed (deny / pending).

  • Auth: mTLS (proxy) or Authorization: Bearer (lite)
  • Verdict on success: upstream MCP response, plus X‑Clavenar‑Correlation‑Id
  • Verdict on deny: 403 with JSON body {error, intent_category, reasoning, correlation_id}
  • Verdict on park: 202 with JSON body {status: "pending", correlation_id, ttl_secs}
  • Rate limit: 429 with {error, scope, key, retry_after_secs, correlation_id}
  • Legacy client: 426 with {contract: "clavenar.client-migration/v1", error: "client_contract_required", executable: false}
POST /mcp
Content-Type: application/json
X-Clavenar-Server-Execution-Contract: clavenar.server-execution/v1
X-Clavenar-Idempotency-Id: 8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "call_tool",
  "params": {
    "name": "refund.issue",
    "arguments": { "amount": 8500 }
  }
}
HTTP/1.1 200 OK
X-Clavenar-Correlation-Id: 3ab2...

{ "jsonrpc": "2.0", "id": 1, "result": { /* upstream */ } }
HTTP/1.1 403 Forbidden

{
  "error": "policy_denied",
  "intent_category": "Privileged Action",
  "reasoning": "refund.issue over threshold",
  "correlation_id": "3ab2..."
}
HTTP/1.1 202 Accepted

{
  "status": "pending",
  "correlation_id": "3ab2...",
  "ttl_secs": 300
}

References: service architecture · clavenar‑lite README for the OSS variant.

clavenar‑brain

Semantic eval · POST /inspect

Returns InspectionResponse: classifier verdict + persona drift score + injection signal. The proxy folds these into the Policy input.

# request
{
  "agent_id":       "agent-001",
  "jsonrpc":        "2.0",
  "method":         "call_tool",
  "params":         { /* … */ },
  "id":             1,
  "correlation_id": "e3b0c44…"
}

# response
{
  "authorized":           true,
  "reason":               "Standard tool call",
  "intent_category":      "Direct Execution",
  "persona_drift_score":  0.12,
  "injection_detected":   false,
  "injection_confidence": 0.0
}

The supported public boundary is documented here; accuracy, latency, caveats, and reproduction guidance live in the reviewed benchmark.

clavenar‑policy‑engine

Governance · POST /evaluate

Pure‑Rust Rego evaluator (regorus). Input is PolicyInput; output is PolicyDecision { allow, reasons, review_reasons, retention_class, approval_tier, policy_bundle }.

  • clavenar.authz.allow — must be true for the request to pass
  • clavenar.authz.deny set with reasons — fails closed
  • clavenar.authz.review set — yellow tier (HIL pending)
  • clavenar.forensic.event — written to ledger regardless of verdict

The reviewed policy catalog lists the released starter policies and exact category totals.

clavenar‑hil

Human approval · POST /pending + POST /decide/{id}

Yellow‑tier verdicts park here. Sandbox annotates predicted blast radius; the operator decides via the console, Slack deep‑link, or — async — via the SDK resolve() helper backed by the X‑Clavenar‑Callback‑URL contract.

  • POST /pending — proxy enqueues and returns the complete PendingRequest
  • POST /decide/{id} — operator approves, denies, or submits a typed modification; authenticated principal and tenant are server‑derived
  • GET /pending — list active (operator UI / agent poll)
  • GET /pending/{id} — single‑row state; use /pending/by-correlation/{correlation_id} for the join key
POST /pending
{
  "agent_id": "agent-finance-01",
  "correlation_id": "3ab2f91d-9e8d-4a8a-920f-67f587229e0d",
  "method": "refund.issue",
  "request_payload": {"params": {"arguments": {"amount": 7500}}},
  "risk_summary": "money movement over the approval threshold",
  "ttl_seconds": 600,
  "state_namespace": "operator"
}
POST /decide/8bf35b7f-e50c-4cc3-a612-6e1a0e8aba19
{
  "decision": "approve",
  "reason": "Customer support ticket verified",
  "decided_via": "console"
}

The response is the updated PendingRequest. In authenticated modes HIL derives decided_by, provenance, credential, and tenant from the verified session or exact Console workload rather than trusting the JSON body.

clavenar‑ledger

Forensic store · /log · /verify · /audit/*

Append‑only. Each row contains the full request, verdict, policy version, operator NameID (if HIL), and a chain pointer SHA‑256(prev_hash · canonical_row_bytes).

  • POST /log — write a forensic event (proxy / policy / HIL caller)
  • GET /verify — always returns VerifyResult; inspect valid, first_invalid_seq, optional unsupported_chain_version, and the complete-chain commitment
  • GET /audit/correlation/{uuid} — pull all rows joined to a single proxy request
  • GET /audit/agents/{agent_id}/narrative?window=7d — the "story view"
  • GET /export/iceberg?from=&to= — Parquet cold tier (S3)
  • egress.streams.{splunk,datadog,generic} — push targets configured per‑sink
GET /verify

{
  "valid": true,
  "entries_checked": 85200,
  "first_invalid_seq": null,
  "commitment": {
    "contract": "clavenar.verified-chain/v1",
    "head_hash": "a5d3e8ef0a5d65d47cd3e787f0760f2217daa04151401539dbd58a5d4439b926",
    "length": 85200,
    "tail_chain_version": 6
  },
  "anchors": [],
  "anchor_mismatch": null
}
GET /audit/correlation/3ab2...

[
  { "method": "proxy.verdict", "authorized": false },
  { "method": "hil.approved", "decided_by": "saml:finance-oncall" },
  { "method": "upstream.outcome", "status": 200 }
]

Backend is pluggable: SqliteLedgerStore (default) or PostgresLedgerStore behind the postgres Cargo feature; the chain bytes are byte‑identical across both.

Trace

One call through every contract.

A single high‑risk tool call should be explainable from ingress to upstream outcome. Use the correlation id as the join key and expect these ledger rows in order.

1 Ingress

POST /mcp accepts JSON‑RPC and returns 202 when Policy asks for review.

proxy.verdict
2 Signals

Brain scores intent/drift/injection, then Policy evaluates the merged signal object.

brain.inspection + policy.evaluated
3 Review

POST /pending parks the call; POST /decide/{id} applies the authenticated operator decision.

hil.created + hil.approved
4 Replay

GET /audit/correlation/{uuid} returns the joined rows for incident review.

upstream.outcome
5 Verify

GET /verify walks the hash chain and proves those rows were not rewritten later.

{ "valid": true }
clavenar‑identity

Trust roots · POST /svid + POST /grant + POST /actor-token

The only component that mints credentials. Every other service consumes; nothing else issues. Identity is a single failure domain on purpose — rotating the trust root is one place to reason about.

  • POST /svid — agent enrolment; mints an SVID (mTLS client cert) keyed on (tenant, agent_name)
  • POST /grant — exchange OIDC id_token + agent SVID → delegation grant; intersects with the agent's registered envelope
  • POST /sign — sign a JWT for a registered agent (delegation, attestation, etc.)
  • POST /actor-token + /actor-token/redeem — A2A federation across tenants
  • GET /.well-known/spiffe-bundle — SPIFFE bundle for federation peers
  • POST /agents + lifecycle — the WAO (agent onboarding) registry
All services

Uniform /health · /readyz · /metrics shape.

Every service hits the same trio with the same wire shape so Helm probes, dashboards, and Prometheus configs are one set of YAML. Metric naming is clavenar_<service>_<event>_<unit>.

  • GET /health — process up; 200 always
  • GET /readyz — dependencies reachable; 503 when not ready
  • GET /metrics — Prometheus text exposition format
Retries

Replay only when the contract says it is safe.

The caller allocates X-Clavenar-Idempotency-Id before network I/O. Decision requests are side-effect free and may retry with that original UUID; server executions never enter an automatic transport retry loop.

Surface Retry behavior Idempotency rule Client action
decision/v1 Bounded transport retries are allowed for network errors and retryable dependency responses. Reuse the original canonical X-Clavenar-Idempotency-Id. Stop on 403; park on 202 until callback or HIL poll resolves.
server-execution/v1 Do not automatically retry an effect-capable request after a transport or upstream failure. The original UUID binds the durable intent and retained result. An exact caller-driven replay returns a completed result; an uncertain outcome stays non-executable.
POST /pending Retry only when the proxy did not receive a response. One pending row per correlation id; duplicate creates should return the existing state. Keep the original TTL and callback URL; do not mint a new correlation id.
POST /decide/{id} Retry 503; do not retry 401, 403, or expired rows. The first accepted decision wins; later writes must fail closed. After success, replay the correlation id to confirm hil.approved or hil.denied.
GET /verify Safe to repeat; use normal dashboard polling cadence. Read-only. The response describes the current ledger head. Treat valid: false, an unsupported chain version, or an incomplete commitment as an incident and stop promotion until investigated.
Errors

Status codes you'll actually see.

Status code carries the type; the JSON body carries the reason and a stable error code for programmatic handling.

  • 200 — verdict allow (or would_deny in observe mode)
  • 202 — verdict park — pending operator decision; correlation id returned
  • 400 — malformed envelope / unsupported method
  • 401 / 403 — auth failure (no SVID, expired grant, scope outside envelope, attestation fail)
  • 403 deny — policy verdict; {error, intent_category, reasoning, correlation_id}
  • 409 — HIL row already decided or another state transition conflict
  • 426 — unselected legacy tool call; upgrade using the SDK migration guide
  • 429 — rate limit; {error, scope, key, retry_after_secs, correlation_id}
  • 503 — downstream not ready (Brain / Policy / Ledger unreachable)
Previous ← Concepts
Next Recipes →