Start lite
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/install.sh | \ sudo sh -s -- --upstream https://api.anthropic.com
Boot the single-binary 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. This page contains no Kubernetes or full-cluster operator steps.
Pick once. Install, wrap, and first-call examples update together.
pnpm, or Python 3.11+ with pip.8088 free locally, or set another listen port in your lite config.This guide installs Clavenar Lite only. To install the multi-service customer stack and full admin Console on an existing Kubernetes or K3s cluster, use the separate Full cluster install.
This page is governed by clavenar.executable-documentation/v1; every listed SDK and quickstart path runs against both the staged candidate and immutable public release. Lite package, binary, and image publication is governed by clavenar.external-install/v1.
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 the checksum-verified native service + TypeScript for the shortest host 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.
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/install.sh | \ sudo sh -s -- --upstream https://api.anthropic.com
curl http://localhost:8088/health
pnpm add @clavenar/agent-sdk@1.6.3 @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" sudo -u clavenar-lite clavenar-lite audit \ --ledger /var/lib/clavenar-lite/clavenar-lite.db anonymous sudo -u clavenar-lite clavenar-lite verify \ --ledger /var/lib/clavenar-lite/clavenar-lite.db # Confirm the audit output contains $CORRELATION_ID.
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/install.sh | \ sudo sh -s -- --upstream https://api.openai.com
curl http://localhost:8088/health
pnpm add @clavenar/agent-sdk@1.6.3 openai
# Save the TypeScript script from step 5 as quickstart-openai.ts
pnpm dlx tsx quickstart-openai.ts
export CORRELATION_ID="paste-from-script" sudo -u clavenar-lite clavenar-lite audit \ --ledger /var/lib/clavenar-lite/clavenar-lite.db anonymous sudo -u clavenar-lite clavenar-lite verify \ --ledger /var/lib/clavenar-lite/clavenar-lite.db # Confirm the audit output contains $CORRELATION_ID.
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. The native installer is the default host path: no Docker, Compose, Rust toolchain, OPA, or database server. Docker remains an isolated alternative. Hosted Lite deployment belongs in Recipes.
# Protected v1.0.0 · detects x86_64/aarch64 and verifies checksum
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/install.sh | \
sudo sh -s -- --upstream https://mcp.your-company.com/rpc
systemctl status clavenar-lite
curl http://127.0.0.1:8088/health
The service runs as clavenar-lite, listens only on loopback, stores configuration in /etc/clavenar-lite/config.env, and persists its SQLite ledger under /var/lib/clavenar-lite. Rerun the installer for an in-place upgrade.
# Docker + Anthropic upstream docker run --rm --name clavenar-lite -p 8088:8088 \ -v clavenar-lite-data:/var/lib/clavenar-lite \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.anthropic.com \ -e CLAVENAR_LITE_MODE=observe \ -e CLAVENAR_LITE_LEDGER=/var/lib/clavenar-lite/ledger.db \ ghcr.io/clavenar/clavenar-lite:1.0.0
# Docker + OpenAI upstream docker run --rm --name clavenar-lite -p 8088:8088 \ -v clavenar-lite-data:/var/lib/clavenar-lite \ -e CLAVENAR_LITE_UPSTREAM_URL=https://api.openai.com \ -e CLAVENAR_LITE_MODE=observe \ -e CLAVENAR_LITE_LEDGER=/var/lib/clavenar-lite/ledger.db \ ghcr.io/clavenar/clavenar-lite:1.0.0
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.
Run journalctl -u clavenar-lite, then check for a port conflict on 8088, a malformed 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@1.6.3 @anthropic-ai/sdk
# TypeScript + OpenAI
pnpm add @clavenar/agent-sdk@1.6.3 openai
# Python + Anthropic
pip install clavenar-agent-sdk==1.5.3 anthropic
# Python + OpenAI
pip install clavenar-agent-sdk==1.5.3 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.
# Read the retained rows and confirm the printed correlation id. sudo -u clavenar-lite clavenar-lite audit \ --ledger /var/lib/clavenar-lite/clavenar-lite.db anonymous # Walk the chain end-to-end. sudo -u clavenar-lite clavenar-lite verify \ --ledger /var/lib/clavenar-lite/clavenar-lite.db
{
"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 verifierclavenar-lite audit anonymous returns at least one row with the same correlation ID, and clavenar-lite 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.
Set CLAVENAR_LITE_MODE=observe in /etc/clavenar-lite/config.env, restart the service, 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.
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/uninstall.sh | sudo sh
curl -fsSL https://github.com/clavenar/clavenar-lite/releases/download/v1.0.0/uninstall.sh | \ sudo sh -s -- --purge