1 · Quickstart

Copy, run, verify.

Use clavenar-lite for the shortest local loop. The SDK itself is provider-neutral: set the provider model through an environment variable, not in the source example, so this page does not age around provider model catalogs.

Copy

Start a local Clavenar ingress.

export CLAVENAR_LITE_MODE=observe
export CLAVENAR_LITE_TOKEN=agent-token

docker run --rm -p 8088:8088 \
  -e CLAVENAR_LITE_MODE="$CLAVENAR_LITE_MODE" \
  -e CLAVENAR_LITE_TOKEN="$CLAVENAR_LITE_TOKEN" \
  ghcr.io/vanteguardlabs/warden-lite:0.4.1
Run

Install the SDK and provider peer.

pnpm add @clavenar/agent-sdk @anthropic-ai/sdk
pnpm add @clavenar/agent-sdk openai

export ANTHROPIC_MODEL="your-anthropic-model"
export OPENAI_MODEL="your-openai-model"
Verify

Replay the correlation proof.

export CORRELATION_ID="paste-from-sdk-log"

curl -sS "http://localhost:8088/audit/correlation/$CORRELATION_ID" \
  -H "Authorization: Bearer $CLAVENAR_LITE_TOKEN"

curl -sS http://localhost:8088/verify \
  -H "Authorization: Bearer $CLAVENAR_LITE_TOKEN"
Minimal wrapper

Start with one provider. The examples below use model environment variables and the same Clavenar options, so swapping providers does not change verdict handling.

import Anthropic from '@anthropic-ai/sdk';
import { clavenarWrap, ClavenarDenied } from '@clavenar/agent-sdk';

const client = clavenarWrap(new Anthropic(), {
  endpoint: 'http://localhost:8088',
  token: process.env.CLAVENAR_LITE_TOKEN,
  onVerdict: (verdict, ctx) => {
    console.log('CLAVENAR_CORRELATION_ID=' + ctx.correlationId);
  },
});

try {
  await client.messages.create({
    model: process.env.ANTHROPIC_MODEL,
    max_tokens: 1024,
    tools: [/* your tool schemas */],
    messages: [{ role: 'user', content: 'try a protected tool call' }],
  });
} catch (e) {
  if (e instanceof ClavenarDenied) {
    console.warn('blocked', { tool: e.toolName, correlationId: e.correlationId });
  } else throw e;
}
import OpenAI from 'openai';
import { clavenarWrap, ClavenarDenied } from '@clavenar/agent-sdk';

const client = clavenarWrap(new OpenAI(), {
  endpoint: 'http://localhost:8088',
  token: process.env.CLAVENAR_LITE_TOKEN,
  onVerdict: (verdict, ctx) => {
    console.log('CLAVENAR_CORRELATION_ID=' + ctx.correlationId);
  },
});

try {
  await client.chat.completions.create({
    model: process.env.OPENAI_MODEL,
    tools: [/* your tool schemas */],
    messages: [{ role: 'user', content: 'try a protected tool call' }],
  });
} catch (e) {
  if (e instanceof ClavenarDenied) {
    console.warn('blocked', { tool: e.toolName, correlationId: e.correlationId });
  } else throw e;
}
# Expected output
CLAVENAR_CORRELATION_ID=8f1d3a40-7c0e-4d2b-9c9a-3b1d0e6a2c5a
{"valid":true,"entries_checked":...}

# If policy denies the call
blocked { tool: "protected_tool", correlationId: "8f1d..." }
2 · Providers

One wrapper, provider-specific surfaces.

Detection is structural, not by import. A client with messages.create wraps as Anthropic; a client with chat.completions.create wraps as OpenAI. Use the tabs for the exact provider surface you need.

import Anthropic from '@anthropic-ai/sdk';
import { clavenarWrap } from '@clavenar/agent-sdk';

const anthropic = clavenarWrap(new Anthropic(), {
  endpoint: process.env.CLAVENAR_ENDPOINT ?? 'http://localhost:8088',
  token: process.env.CLAVENAR_LITE_TOKEN,
  mode: process.env.CLAVENAR_SDK_MODE ?? 'enforce',
});

await anthropic.messages.create({
  model: process.env.ANTHROPIC_MODEL,
  max_tokens: 1024,
  tools: [/* ... */],
  messages: [/* ... */],
});
import OpenAI from 'openai';
import { clavenarWrap } from '@clavenar/agent-sdk';

const openai = clavenarWrap(new OpenAI(), {
  endpoint: process.env.CLAVENAR_ENDPOINT ?? 'http://localhost:8088',
  token: process.env.CLAVENAR_LITE_TOKEN,
  mode: process.env.CLAVENAR_SDK_MODE ?? 'enforce',
});

await openai.chat.completions.create({
  model: process.env.OPENAI_MODEL,
  tools: [/* ... */],
  messages: [/* ... */],
});
const wrapped = clavenarWrap(providerClient, {
  endpoint: process.env.CLAVENAR_ENDPOINT ?? 'http://localhost:8088',
  token: process.env.CLAVENAR_LITE_TOKEN,
  mode: 'enforce',                 // or "observe"
  retry: { attempts: 3, baseDelayMs: 150 },
  onVerdict(verdict, ctx) {
    metrics.increment('clavenar.verdict', {
      status: verdict.status,
      tool: ctx.toolName,
      correlationId: ctx.correlationId,
    });
  },
});
3 · Streaming

Denied tool calls throw before your loop executes them.

The SDK inspects streaming tool-call deltas and holds the closing tool-call event until Clavenar returns a verdict. In enforce mode, your iterator only sees policy-cleared tool calls.

import { ClavenarDenied } from '@clavenar/agent-sdk';

try {
  const stream = await wrapped.messages.create({
    model: process.env.ANTHROPIC_MODEL,
    max_tokens: 1024,
    stream: true,
    tools: [/* ... */],
    messages: [/* ... */],
  });

  for await (const event of stream) {
    // Tool-use close events arrive only after allow.
    handleProviderEvent(event);
  }
} catch (e) {
  if (e instanceof ClavenarDenied) {
    console.warn('stream blocked', {
      tool: e.toolName,
      correlationId: e.correlationId,
    });
    return;
  }
  throw e;
}
4 · Yellow tier

Treat human review as resumable async work.

A parked tool call is not a failure from the agent's perspective. Preserve the correlation id, wait for the operator decision, then retry the original provider request only after resolve() returns.

1 SDK throws

ClavenarPending carries tool, reasons, and correlation id.

2 Operator decides

Console, CLI, or callback records allow/deny against the same id.

3 resolve()

Returns on allow, throws on deny or timeout.

4 Retry turn

Run the provider request again so the now-approved tool call can execute.

5 Replay proof

Audit shows park, decision, and final upstream outcome under one id.

import { ClavenarDenied, ClavenarPending } from '@clavenar/agent-sdk';

async function runTurn() {
  return await wrapped.messages.create({
    model: process.env.ANTHROPIC_MODEL,
    max_tokens: 1024,
    tools: [/* ... */],
    messages: [/* ... */],
  });
}

try {
  return await runTurn();
} catch (e) {
  if (e instanceof ClavenarPending) {
    console.info('pending review', {
      tool: e.toolName,
      reasons: e.reviewReasons,
      correlationId: e.correlationId,
    });
    await e.resolve({ pollIntervalMs: 2000, timeoutMs: 10 * 60 * 1000 });
    return await runTurn();
  }
  if (e instanceof ClavenarDenied) {
    console.warn('denied', { tool: e.toolName, correlationId: e.correlationId });
    return null;
  }
  throw e;
}
5 · Rollout

SDK mode and ingress mode are separate controls.

Use ingress mode for the server-side policy posture and SDK mode for app-local behavior during rollout. The safest path is observe everywhere first, then switch ingress to enforce, then remove SDK observe overrides.

Ingress mode SDK mode Deny behavior Pending behavior Use when
observe observe Provider response passes through; onVerdict fires. Provider response passes through; review is recorded as would-pend. First integration and false-positive tuning.
observe enforce SDK can throw locally when the verdict says deny. SDK can throw ClavenarPending locally. App-team rehearsal before server-side enforcement.
enforce observe SDK passes through only if ingress already allowed upstream. SDK records the pending signal without adding an app-local block. Temporary compatibility bridge for legacy callers.
enforce enforce throw ClavenarDenied throw ClavenarPending, then await e.resolve(). Production default after rollout.
6 · Exceptions

Catch by class, then act on correlation id.

Every operationally meaningful SDK exception is keyed by a correlation id when the ingress emitted one. Log it, attach it to traces, and use it as the audit replay handle.

Class Thrown when Fields Retry? Operator action
ClavenarDenied Policy returned a red verdict or a pending review resolved to deny. toolName, reasons, reviewReasons, intentCategory, correlationId No. Change input, policy, or approval state first. Replay the correlation id and confirm the blocked tool never executed.
ClavenarPending Policy returned a yellow verdict and parked the call. toolName, reviewReasons, correlationId, resolve() Only after resolve() returns allow. Approve or deny in the operator surface; second decisions fail closed.
ClavenarConfigError Wrapper options are invalid before the first provider call. Message plus option context. Fix config, then restart or recreate the wrapped client. No operator action; this is app configuration.
ClavenarTransportError Ingress is unreachable, returned malformed JSON, or timed out. status when known, endpoint, retry metadata. Yes for timeouts, 503, and 429 after retry_after_secs. Check ingress health and ledger/HIL dependencies before promotion.