Start lite
docker run --rm -p 8088:8088 \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.anthropic.com \ -e CLAVENAR_LITE_MODE=observe \ ghcr.io/vanteguardlabs/warden-lite:0.4.1
Boot the OSS edition clavenar‑lite, install one of the SDKs, wrap your Anthropic or OpenAI client, and watch every inspected tool call get logged to the hash‑chained ledger. Defaults to observe mode so you can wire it in without breaking anything.
Pick once. Install, wrap, and first-call examples update together.
clavenar-lite binary, or a Fly.io account.pnpm, or Python 3.11+ with pip.8088 free locally, or set another listen port in your lite config.Keep provider credentials and the optional Clavenar bearer in one shell session while you work through the page.
# Anthropic path export ANTHROPIC_API_KEY="sk-ant-..." export ANTHROPIC_MODEL="claude-opus-4-7" export CLAVENAR_LITE_MODE="observe" export CLAVENAR_LITE_TOKEN="" # optional bearer
# OpenAI path export OPENAI_API_KEY="sk-..." export OPENAI_MODEL="gpt-4-turbo" export CLAVENAR_LITE_MODE="observe" export CLAVENAR_LITE_TOKEN="" # optional bearer
Use Docker + TypeScript for the shortest deterministic path. The first-call script below forces one tool call, captures the verdict callback, then prints the correlation ID you use for the audit lookup.
docker run --rm -p 8088:8088 \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.anthropic.com \ -e CLAVENAR_LITE_MODE=observe \ ghcr.io/vanteguardlabs/warden-lite:0.4.1
curl http://localhost:8088/health
pnpm add @clavenar/agent-sdk @anthropic-ai/sdk
# Save the TypeScript script from step 5 as quickstart-anthropic.ts
pnpm dlx tsx quickstart-anthropic.ts
export CORRELATION_ID="paste-from-script"
curl http://localhost:8088/audit/correlation/$CORRELATION_ID
curl http://localhost:8088/verify
docker run --rm -p 8088:8088 \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.openai.com \ -e CLAVENAR_LITE_MODE=observe \ ghcr.io/vanteguardlabs/warden-lite:0.4.1
curl http://localhost:8088/health
pnpm add @clavenar/agent-sdk openai
# Save the TypeScript script from step 5 as quickstart-openai.ts
pnpm dlx tsx quickstart-openai.ts
export CORRELATION_ID="paste-from-script"
curl http://localhost:8088/audit/correlation/$CORRELATION_ID
curl http://localhost:8088/verify
The script prints CORRELATION_ID=..., and both audit commands return JSON without a transport error.
Run the sections below in order. Each step has one health signal and one targeted failure check.
Clavenar‑lite is one Rust binary that fronts your agent's MCP traffic. Run it locally for development; deploy on Fly.io for a shared environment; or move to the full multi‑service edition when you need Brain / Policy / HIL / Ledger / Identity split out.
# Docker + Anthropic upstream docker run --rm -p 8088:8088 \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.anthropic.com \ -e CLAVENAR_LITE_MODE=observe \ ghcr.io/vanteguardlabs/warden-lite:0.4.1
# Docker + OpenAI upstream docker run --rm -p 8088:8088 \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.openai.com \ -e CLAVENAR_LITE_MODE=observe \ ghcr.io/vanteguardlabs/warden-lite:0.4.1
# Host binary (musl, no deps)
curl -sSL https://github.com/clavenar/clavenar-lite/releases/latest/download/clavenar-lite-linux-x86_64 \
-o clavenar-lite
chmod +x clavenar-lite
CLAVENAR_LITE_MODE=observe ./clavenar-lite
# Fly.io
fly launch --name my-clavenar-lite --no-deploy
fly secrets set CLAVENAR_LITE_MODE=observe
fly deploy
curl -s http://localhost:8088/health | jq .
{
"status": "ok",
"mode": "observe",
"ledger": "ready"
}
clavenar-lite listening on :8088 appears in logs and /health returns status: ok.
Check for a port conflict on 8088, a missing upstream URL, or a mismatched bearer token.
The SDKs are peer‑agnostic: install whichever provider client you already use. The wrapper auto‑detects shape from the method surface.
# TypeScript + Anthropic
pnpm add @clavenar/agent-sdk @anthropic-ai/sdk
# TypeScript + OpenAI
pnpm add @clavenar/agent-sdk openai
# Python + Anthropic
pip install clavenar-agent-sdk anthropic
# Python + OpenAI
pip install clavenar-agent-sdk openai
Your lockfile adds the Clavenar wrapper plus exactly the provider SDK you already call in production.
Confirm Node 20+ for TypeScript, Python 3.11+ for Python, and that your package manager is running inside the agent project.
Every tool call the model emits is forwarded to clavenar‑lite for inspection before your tool‑execution loop sees it. Deny verdicts throw ClavenarDenied; yellow‑tier parks throw ClavenarPending.
import Anthropic from '@anthropic-ai/sdk'; import { clavenarWrap } from '@clavenar/agent-sdk'; const client = clavenarWrap(new Anthropic(), { endpoint: 'http://localhost:8088', token: process.env.CLAVENAR_LITE_TOKEN || undefined, mode: 'observe', });
import OpenAI from 'openai'; import { clavenarWrap } from '@clavenar/agent-sdk'; const client = clavenarWrap(new OpenAI(), { endpoint: 'http://localhost:8088', token: process.env.CLAVENAR_LITE_TOKEN || undefined, mode: 'observe', });
from anthropic import AsyncAnthropic import os from clavenar_agent_sdk import ClavenarOptions, clavenar_wrap client = clavenar_wrap( AsyncAnthropic(), ClavenarOptions( endpoint="http://localhost:8088", token=os.environ.get("CLAVENAR_LITE_TOKEN") or None, mode="observe", ), )
from openai import AsyncOpenAI import os from clavenar_agent_sdk import ClavenarOptions, clavenar_wrap client = clavenar_wrap( AsyncOpenAI(), ClavenarOptions( endpoint="http://localhost:8088", token=os.environ.get("CLAVENAR_LITE_TOKEN") or None, mode="observe", ), )
Your app still calls client.messages.create or client.chat.completions.create; only construction changed.
Confirm the wrapped client object is the one passed into your agent loop, not the original provider client.
The examples below use the SDK verdict callback to capture the correlation ID in observe mode. Switch to enforce after the policy has been tuned against real traffic.
| Mode | Verdict behavior | Use when |
|---|---|---|
observe |
Provider response passes through; onVerdict records allow, would_deny, or pending. |
First integration, policy tuning, production shadow mode. |
enforce |
Deny throws ClavenarDenied; yellow tier throws ClavenarPending until the operator decides. |
Policies are tuned and the agent can safely block side effects. |
# quickstart-anthropic.ts import Anthropic from '@anthropic-ai/sdk'; import { clavenarWrap, ClavenarDenied, ClavenarPending } from '@clavenar/agent-sdk'; let correlationId = ''; const client = clavenarWrap(new Anthropic(), { endpoint: 'http://localhost:8088', token: process.env.CLAVENAR_LITE_TOKEN || undefined, mode: 'observe', onVerdict: (verdict, ctx) => { correlationId = verdict.correlationId || correlationId; console.log(`[clavenar] ${verdict.kind} ${ctx.toolName} ${correlationId}`); }, }); try { await client.messages.create({ model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-7', max_tokens: 256, tools: [{ name: 'lookup_account', description: 'Look up account risk for a customer account.', input_schema: { type: 'object', properties: { account_id: { type: 'string' } }, required: ['account_id'], }, }], tool_choice: { type: 'tool', name: 'lookup_account' }, messages: [{ role: 'user', content: 'Use lookup_account for acct_123 and summarize risk.' }], }); } catch (e) { if (e instanceof ClavenarPending) correlationId = e.correlationId; else if (e instanceof ClavenarDenied) correlationId = e.correlationId || correlationId; else throw e; } console.log(`CORRELATION_ID=${correlationId}`);
# quickstart-openai.ts import OpenAI from 'openai'; import { clavenarWrap, ClavenarDenied, ClavenarPending } from '@clavenar/agent-sdk'; let correlationId = ''; const client = clavenarWrap(new OpenAI(), { endpoint: 'http://localhost:8088', token: process.env.CLAVENAR_LITE_TOKEN || undefined, mode: 'observe', onVerdict: (verdict, ctx) => { correlationId = verdict.correlationId || correlationId; console.log(`[clavenar] ${verdict.kind} ${ctx.toolName} ${correlationId}`); }, }); try { await client.chat.completions.create({ model: process.env.OPENAI_MODEL || 'gpt-4-turbo', messages: [{ role: 'user', content: 'Use lookup_account for acct_123 and summarize risk.' }], tools: [{ type: 'function', function: { name: 'lookup_account', description: 'Look up account risk for a customer account.', parameters: { type: 'object', properties: { account_id: { type: 'string' } }, required: ['account_id'], }, }, }], tool_choice: { type: 'function', function: { name: 'lookup_account' } }, }); } catch (e) { if (e instanceof ClavenarPending) correlationId = e.correlationId; else if (e instanceof ClavenarDenied) correlationId = e.correlationId || correlationId; else throw e; } console.log(`CORRELATION_ID=${correlationId}`);
# quickstart_anthropic.py import asyncio import os from anthropic import AsyncAnthropic from clavenar_agent_sdk import ClavenarDenied, ClavenarOptions, ClavenarPending, clavenar_wrap correlation_id = "" async def on_verdict(verdict, ctx): global correlation_id correlation_id = getattr(verdict, "correlation_id", None) or correlation_id print(f"[clavenar] {verdict.kind} {ctx.tool_name} {correlation_id}") async def main(): global correlation_id client = clavenar_wrap( AsyncAnthropic(), ClavenarOptions( endpoint="http://localhost:8088", token=os.environ.get("CLAVENAR_LITE_TOKEN") or None, mode="observe", on_verdict=on_verdict, ), ) try: await client.messages.create( model=os.environ.get("ANTHROPIC_MODEL", "claude-opus-4-7"), max_tokens=256, tools=[{ "name": "lookup_account", "description": "Look up account risk for a customer account.", "input_schema": { "type": "object", "properties": {"account_id": {"type": "string"}}, "required": ["account_id"], }, }], tool_choice={"type": "tool", "name": "lookup_account"}, messages=[{"role": "user", "content": "Use lookup_account for acct_123 and summarize risk."}], ) except ClavenarPending as e: correlation_id = e.correlation_id except ClavenarDenied as e: correlation_id = e.correlation_id or correlation_id print(f"CORRELATION_ID={correlation_id}") asyncio.run(main())
# quickstart_openai.py import asyncio import os from openai import AsyncOpenAI from clavenar_agent_sdk import ClavenarDenied, ClavenarOptions, ClavenarPending, clavenar_wrap correlation_id = "" async def on_verdict(verdict, ctx): global correlation_id correlation_id = getattr(verdict, "correlation_id", None) or correlation_id print(f"[clavenar] {verdict.kind} {ctx.tool_name} {correlation_id}") async def main(): global correlation_id client = clavenar_wrap( AsyncOpenAI(), ClavenarOptions( endpoint="http://localhost:8088", token=os.environ.get("CLAVENAR_LITE_TOKEN") or None, mode="observe", on_verdict=on_verdict, ), ) try: await client.chat.completions.create( model=os.environ.get("OPENAI_MODEL", "gpt-4-turbo"), messages=[{"role": "user", "content": "Use lookup_account for acct_123 and summarize risk."}], tools=[{ "type": "function", "function": { "name": "lookup_account", "description": "Look up account risk for a customer account.", "parameters": { "type": "object", "properties": {"account_id": {"type": "string"}}, "required": ["account_id"], }, }, }], tool_choice={"type": "function", "function": {"name": "lookup_account"}}, ) except ClavenarPending as e: correlation_id = e.correlation_id except ClavenarDenied as e: correlation_id = e.correlation_id or correlation_id print(f"CORRELATION_ID={correlation_id}") asyncio.run(main())
A line like [clavenar] allow lookup_account 3ab2..., followed by CORRELATION_ID=3ab2.... In observe mode, would-deny signals still pass through so your agent keeps running.
onVerdict or on_verdict fires before your tool loop handles the call.
Confirm the model was forced to call lookup_account, and that the wrapped client is used for the provider call.
Each request gets a UUID‑v4 correlation_id stamped by the proxy. The ledger writes one canonical JSON entry per terminal pipeline outcome, and each row's entry_hash chains SHA‑256(prev_hash || row_bytes) so tampering breaks the chain at the next /verify walk.
# Pull the row by correlation id curl http://localhost:8088/audit/correlation/$CORRELATION_ID # Walk the chain end-to-end curl http://localhost:8088/verify
{
"correlation_id": "3ab2...",
"agent_id": "quickstart-local",
"tool_type": "lookup_account",
"authorized": true,
"entry_hash": "b89d...",
"prev_hash": "4c21..."
}
The audit verifier recomputes receipt hashes in the browser, so an auditor can inspect the proof without trusting the server that served the row.
Open Audit verifier/audit/correlation/{id} returns at least one row with the same correlation ID, and /verify reports a valid chain.
Search by the exact ID printed by the script. If the chain does not verify, open the browser verifier and inspect the first broken row.
The full receipts schema lives in the wire format spec.
Most first-run failures are environment mismatches: port already bound, token mismatch, upstream URL missing, provider credentials missing, or the app still calling the unwrapped provider client.
address already in use means another process owns 8088. Stop it or run lite on another port and pass that endpoint into the SDK wrapper.
A 401 usually means the SDK has CLAVENAR_LITE_TOKEN but lite was started without the same token, or the reverse.
Confirm the app uses the wrapped client object, not the original provider client. Then search by the correlationId or correlation_id from the callback or exception.
Use the forced tool_choice example once. If that works, remove the force and tune your prompt/tool descriptions.
Run CLAVENAR_LITE_MODE=observe, collect would_deny rows, then adjust policy before flipping to enforce.
If the provider returns auth errors before Clavenar logs anything, verify ANTHROPIC_API_KEY or OPENAI_API_KEY in the same shell.
After the first local verdict, decide whether to keep lite embedded, deploy lite as a shared gateway, or move to the full stack for operator approvals, identity, and HA ledger storage.