Skip to content

Repository files navigation

clavenar-java-sdk

CI Release

Java SDK for Clavenar. Inspect the tool calls a model emits against your policies before your agent runs them.

Part of the by-language agent-wrapper SDK family alongside @clavenar/agent-sdk (TypeScript) and clavenar-agent-sdk (Python) — all speak the same wire contract.

Install

The Maven registry uses GitHub Packages authentication. Add https://maven.pkg.github.com/clavenar/clavenar-java-sdk as repository github and put a GitHub token with read:packages in the matching ~/.m2/settings.xml server entry. The same JAR is attached anonymously to the versioned GitHub release.

For an anonymous, byte-exact download of the current package and publication evidence:

base=https://github.com/clavenar/clavenar-java-sdk/releases/download/v1.5.3
curl -fsSLO "$base/agent-sdk-1.5.3.jar"
curl -fsSLO "$base/pom.xml"
curl -fsSLO "$base/bom.json"
curl -fsSLO "$base/bom.xml"
jar tf agent-sdk-1.5.3.jar >/dev/null

Maven dependency:

<dependency>
  <groupId>com.clavenar</groupId>
  <artifactId>agent-sdk</artifactId>
  <version>1.5.3</version>
</dependency>

Gradle:

implementation("com.clavenar:agent-sdk:1.5.3")

Requires Java 17+. The only runtime dependency is Jackson; the SDK takes no dependency on the Anthropic or OpenAI SDKs — it duck-types their responses.

Two ways to integrate

1. Inspect at the tool-dispatch boundary (recommended for frameworks)

Spring AI and LangChain4j own the model call, so gate the tool before it executes. This is the primary surface:

var inspector = new ClavenarInspector(
    ClavenarOptions.builder("http://localhost:8088").token(token).build());

// Spring AI ToolCallback / LangChain4j ToolExecutor, inside your tool body:
inspector.enforce(toolName, toolCallId, argumentsJson); // throws ClavenarDenied on a policy block
// ... reached only when the call cleared policy — run the tool

enforce throws ClavenarDenied / ClavenarPending in enforce mode; in observe mode it never throws and fires your callbacks instead.

2. Wrap the model client (wrap-and-forget)

import com.anthropic.client.AnthropicClient;

AnthropicClient client = Clavenar.wrap(
    rawAnthropicClient,
    ClavenarOptions.builder("http://localhost:8088").build());

// Use the client exactly as before; every tool_use is inspected first.
var message = client.messages().create(params); // throws ClavenarDenied on a block

The same Clavenar.wrap detects an OpenAI client (chat().completions().create()) structurally. It returns a dynamic proxy of the same interface type and passes every non-create method through unchanged.

Verdicts and the error model

ClavenarInspector.inspect returns a Verdict (ALLOW / DENY / PENDING / RATE_LIMITED). Every call explicitly selects the side-effect-free clavenar.decision/v1 contract with a UUID allocated before the first attempt; multi-tool turns use one ordered atomic decision. Proxy 0.5.0 and Lite 0.9.0 reject unselected tool calls with HTTP 426; upgrade this SDK before the gateway by following https://clavenar.com/docs/sdk-migration/. inspectAll, enforce, and the wrap facade translate, in enforce mode, to unchecked exceptions rooted at ClavenarException:

Exception Meaning
ClavenarDenied policy rejected the call — toolName, reasons, reviewReasons, intentCategory, layer, correlationId
ClavenarPending parked for human review — call resolve() to block until decided
ClavenarRateLimited 429 before evaluation — code() (rate_limited velocity gate / quota_exceeded spend gate), retryAfterSecs(); never auto-retried
ClavenarTransportException clavenar unreachable / unexpected response — status() (0 = network)
ClavenarConfigException bad options, or a model tool call with unparseable arguments

Debugging a denial

ClavenarDenied carries reasons(), layer(), and correlationId(). To see which detector fired, run the gateway with CLAVENAR_PROXY_VERBOSE_VERDICTS=true (Lite: --verbose-verdicts) — the deny then carries a per-detector detail() breakdown, and the SDK renders it to stderr when you set devMode(true):

var opts = ClavenarOptions.builder("https://clavenar.internal")
    .devMode(true) // dev/staging only — detailed denials are an attacker oracle
    .build();
// On a deny, the SDK prints a panel to stderr:
//   ━━ clavenar denied: send_email ━━
//     layer=brain  intent=Exfiltration  correlation=abc-123
//     detectors:
//       persona_drift         0.12
//       injection             0.91  ⚠ flagged
//     degraded: injection

Programmatic access (no devMode needed):

catch (ClavenarDenied e) {
  if (e.detail() != null) {
    e.detail().detectors().stream()
        .filter(d -> d.flagged() || d.score() >= 0.5)
        .forEach(d -> System.out.println("fired: " + d.detector()));
  }
}

detail() is null unless the gateway opts in; without it the panel prints a hint to enable verbose verdicts.

Enforce vs observe

var opts = ClavenarOptions.builder(endpoint)
    .observe()
    .onVerdict((verdict, ctx) -> log.info("{} -> {}", ctx.toolName(), verdict.kind()))
    .build();

Observe never blocks: verdicts surface via onVerdict, transport failures via onPolicyError, and every call passes through — the rollout knob for tuning policies against live traffic.

Pending review

try {
  inspector.enforce(toolName, id, argsJson);
} catch (ClavenarPending pending) {
  pending.resolve(); // blocks; returns on approve, throws ClavenarDenied on deny
}

Pending polling treats only network failures and 5xx responses as transient. Malformed success bodies, correlation mismatches, and every other HTTP status are terminal transport errors.

Governed execution

Use GovernedExecutionClient when policy authorization and the provider effect must form a recoverable workflow. Supply an application-owned DurableExecutionStore, a cryptographic AuthorizationVerifier, a receipt signer, and an executor that forwards the supplied idempotency ID to the provider. The client verifies all authorization bindings before committing an intent or releasing an effect.

On restart, a stored completion is integrity-checked and returned. A stored intent is passed to the optional EffectRecoverer; if it cannot conclusively find the provider effect, the client throws ClavenarRecoveryRequired instead of replaying it. Completion plus receipt-outbox persistence is bounded by the configured finalization deadline.

SecureTransportProfile reuses its connection pool. Call reload() after rotating credential files and close the profile during application shutdown.

Streaming

StreamGate holds a tool call's closing event until clavenar returns a verdict, so a denied call never reaches your loop as actionable. Drive it from your stream-reading loop with start / update / close (Anthropic block index) or closeByPrefix (OpenAI per-choice drain). See docs/SEQUENCES.md.

Realtime

Verdict v = Realtime.inspect(
    new Realtime.FunctionCallDone(callId, name, argumentsJson), opts);

Behavior parity

Matches the TypeScript reference 1:1 on the wire — see docs/PARITY.md for the map and the additive Java-idiom differences.

License

Apache-2.0.

About

Java/JVM agent-wrapper SDK for Clavenar — inspect tool calls before your agent runs them.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages