Send Content-Type: application/json, preserve the JSON-RPC id, and treat the response status as the verdict.
The Clavenar wire format.
One agent-facing HTTP+JSON contract covers clavenar-lite and the full edition: POST /mcp, status-based verdicts, yellow-tier human review, correlation headers, schema files, and the ledger proof path that verifies every decision.
POST /mcp
Forward JSON-RPC tool calls with the same body your MCP client already emits.
jsonrpc + params.name
Branch
Handle three verdicts
Use the HTTP status as the switch: allow, deny, or park for review.
200 / 403 / 202
Review
Poll and decide
Capture the correlation id, poll pending state, and require an operator token to decide.
/pending/{id}
Verify
Replay proof
Validate payloads with schemas, then replay the ledger by correlation id.
/verify + /audit
Status is the contract switch.
A client can branch on the HTTP status before decoding the body. The extra columns below show which credential is required, which proof row should exist, and whether the upstream tool was allowed to run.
| Path | Auth | Status | Body contract | Side effect | Proof row |
|---|---|---|---|---|---|
| Green allow | mTLS SVID or lite bearer | 200 OK |
Upstream JSON-RPC result passes through, plus response headers. | Tool executes. | proxy.verdict and upstream.outcome |
| Red deny | mTLS SVID or lite bearer | 403 Forbidden |
{error:"policy_denied", intent_category, reasoning, correlation_id} |
Tool never executes. | proxy.verdict with authorized=false |
| Yellow review | mTLS SVID or lite bearer | 202 Accepted |
{status:"pending", correlation_id, ttl_secs, review_reasons} |
Tool is parked until an operator decides. | PendingReview, then PendingApproved or PendingDenied |
| Replay proof | Audit/console read auth | 200 OK |
Correlation trace or chain verification result. | No new tool side effect. | Reads canonical hash-chain rows. |
POST /mcp is JSON-RPC 2.0 over HTTPS.
params.name is the tool type evaluated by policy. method rides through into the ledger row. Unknown extra fields are preserved for upstream compatibility, so richer MCP clients can keep their native envelope.
// POST /mcp - JSON-RPC body { "jsonrpc": "2.0", "id": 1, "method": "call_tool", "params": { "name": "wire_transfer", "arguments": { "to": "acct-1", "amount": 100 } } }
Use Authorization: Bearer <token> when lite is configured with an agent token.
Use the enrolled workload SVID on the mTLS data-plane path; operator decisions stay on a separate credential.
Three statuses. Three client branches.
The 403 and 202 examples below match the published API schemas. A 200 is intentionally boring: Clavenar lets the upstream JSON-RPC response pass through and adds the correlation headers.
Allowed. The upstream result is returned to the agent. Capture X-Clavenar-Correlation-Id if you need to replay the proof trail later.
// 200 response HTTP/1.1 200 OK content-type: application/json x-clavenar-correlation-id: 8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a x-clavenar-mode: enforce { "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "sent" }] } }
Denied. Raise a typed denial in the SDK and point operators at the correlation id.
// 403 body { "error": "policy_denied", "intent_category": "Privileged Action", "reasoning": "wire_transfer over threshold requires approval", "correlation_id": "8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a" }
Parked for human review. Return control to the caller, persist the id, and poll /pending/<id>.
// 202 body { "status": "pending", "correlation_id": "8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a", "ttl_secs": 900, "review_reasons": [ "Review: Wire transfers require human approval before execution." ] }
Exercise the contract from a laptop.
These examples target clavenar-lite on localhost. The full edition uses the same request and verdict shapes; swap bearer auth for the enrolled mTLS client certificate.
Park a reviewable tool call.
export CLAVENAR_LITE_URL=http://localhost:8088
export CLAVENAR_LITE_TOKEN=agent-token
curl -sS -i "$CLAVENAR_LITE_URL/mcp" \
-H "Authorization: Bearer $CLAVENAR_LITE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0",
"id":1,
"method":"call_tool",
"params":{"name":"wire_transfer","arguments":{"to":"acct-1","amount":100}}
}'
Poll and decide with the operator token.
export CID=8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a
export CLAVENAR_LITE_DECIDE_TOKEN=op-token
curl -sS "$CLAVENAR_LITE_URL/pending/$CID" \
-H "Authorization: Bearer $CLAVENAR_LITE_TOKEN"
curl -sS -X POST "$CLAVENAR_LITE_URL/pending/$CID/decide" \
-H "Authorization: Bearer $CLAVENAR_LITE_DECIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"decision":"allow","note":"ok by sec"}'
Replay the proof trail.
curl -sS "$CLAVENAR_LITE_URL/audit/correlation/$CID" \ -H "Authorization: Bearer $CLAVENAR_LITE_TOKEN" curl -sS "$CLAVENAR_LITE_URL/verify" \ -H "Authorization: Bearer $CLAVENAR_LITE_TOKEN"
# Expected signals
HTTP/1.1 202 Accepted
{"status":"pending","correlation_id":"8f1d...","ttl_secs":900,"review_reasons":[...]}
ok: pending 8f1d... decided allow
{"valid":true,"rows_checked":...}
Yellow tier is a state machine, not a special error.
A 202 means policy allowed the shape of the request but required a human decision before the upstream side effect can happen. The pending row is outside HashableEntryV1; the ledger writes stay byte-compatible with the full verifier.
POST /mcp returns 202 and writes PendingReview.
GET /pending/{id} returns decision:null until review closes.
POST /pending/{id}/decide requires --decide-token.
Correlation replay joins park, decision, and final upstream outcome.
/verify proves the hash chain still covers the decision rows.
-
Park
POST /mcpreturns202with{status, correlation_id, ttl_secs, review_reasons}. SDK callers should treat this like an async work order: persist the id, return control, and poll. -
Poll
GET /pending/<correlation_id>returns the current state. Auth reuses the agent token because this is the same identity that issued the/mcpcall.// GET /pending/<id> - 200 { "correlation_id": "8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a", "agent_id": "bearer-agent", "tool_type": "wire_transfer", "method": "call_tool", "review_reasons": ["Review: Wire transfers require human approval before execution."], "requested_at": "2026-05-12T10:14:03Z", "decided_at": null, "decision": null, "decider_note": null }
-
Decide
POST /pending/<correlation_id>/decideaccepts{decision, note?}. A second decide returns409instead of silently overwriting the operator record. Auth uses--decide-tokenso an agent bearer cannot approve its own pending request.# Operator-side decision curl -sS -X POST "$CLAVENAR_LITE_URL/pending/$CID/decide" \ -H "Authorization: Bearer $CLAVENAR_LITE_DECIDE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"decision":"deny","note":"customer limit exceeded"}' # A second decide fails closed HTTP/1.1 409 Conflict {"error":"pending_already_decided","correlation_id":"8f1d..."}
Every response carries a correlation handle.
X-Clavenar-Correlation-Id is a UUID v4 minted per request, returned on the response, and persisted to the ledger. Treat it as a forensic handle, not as a hash-chain primitive.
| Header | Set by | Returned on | Ledger use | Client rule |
|---|---|---|---|---|
Authorization |
Agent or operator | Request only | Never hash credentials. | Use agent token for /mcp and poll; use decide token for decisions. |
X-Clavenar-Correlation-Id |
Proxy | All verdicts and errors | Stored as a pivot field, not inside HashableEntryV1. |
Capture the response value; do not assume a caller-supplied value wins. |
X-Clavenar-Mode |
Proxy | Verdict responses | Explains observe vs enforce in audit rows. | In observe mode, show the warning but keep upstream behavior unchanged. |
X-Clavenar-Callback-URL |
Client | Request only | Stored with pending review metadata when async callbacks are enabled. | Use HTTPS and verify the operator decision payload before resuming work. |
Content-Type |
Client/proxy | Request and response | No proof semantics. | Send and expect application/json for all JSON-RPC and HIL calls. |
// Response headers - any verdict, any error HTTP/1.1 403 Forbidden content-type: application/json x-clavenar-correlation-id: 8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a x-clavenar-mode: enforce
Because the correlation id is not part of HashableEntryV1, lite and full can verify the same canonical hash-chain rows while still giving operators a fast lookup handle.
Machine-readable contracts.
Use these files in SDK tests, CI contract checks, or partner conformance suites. The wire-specific schema narrows the public status bodies; the API schemas cover broader proxy, HIL, and ledger payloads.
JSON-RPC envelope with params.name and tool arguments.
proxy-mcp-request.schema.json
Verdicts
Wire verdict bodies
Allow, deny, pending, and rate-limit response bodies for the public wire.
wire-verdict.schema.json
Proxy
Proxy verdict schema
The broader proxy response schema used by API reference examples.
proxy-verdict.schema.json
HIL
Decision payload
Operator approval/denial body for /pending/{id}/decide.
hil-decision.schema.json
Proof
Ledger verify result
Hash-chain verification output for audit and release smoke tests.
ledger-verify.schema.json
Slack alert payload.
Pass --slack-webhook-url or CLAVENAR_LITE_SLACK_WEBHOOK_URL to fire a one-way alert each time a tool call parks. Slow or unreachable webhooks never block the agent's 202.
# Slack webhook payload - text rendered in the channel
:warning: Clavenar parked a tool call for review
*Tool:* `wire_transfer`
*Agent:* `bearer-agent`
*Correlation ID:* `8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a`
*Reasons:*
- Review: Wire transfers require human approval before execution.
Approve: `clavenar-lite pending decide 8f1d... --allow`
Deny: `clavenar-lite pending decide 8f1d... --deny --note "..."`
The lite alert is one-way: operators decide with the CLI or curl. The full edition adds SSO-gated web approvals, Slack button flows, and Teams Adaptive Card rendering on top of the same pending/decision contract.