Framework adapters
Sanction stays outside the framework's identity system and inside the
pre-action path: the framework asks, Sanction decides, the agent acts only on an
approved decision or redeemed grant. The TypeScript adapters below ship in
@sanction/sdk (npm install @sanction/sdk); the Python recipes are copy-in
until their packages land.
TypeScript: SanctionMiddleware (ships)
Framework-agnostic — use it with LangChain.js, LangGraph, Mastra, or any custom agent runtime. It authorizes first and runs the tool only on approval:
import { SanctionClient, SanctionMiddleware, SanctionToolBlocked } from "@sanction/sdk"
const client = new SanctionClient(process.env.SANCTION_AGENT_KEY!)
const runTool = SanctionMiddleware(client)
try {
const result = await runTool({
server: "github",
tool: "create_pr",
input: { title, body },
run: () => octokit.pulls.create({ ... }), // runs ONLY if approved
})
} catch (e) {
if (e instanceof SanctionToolBlocked) {
// e.status: "escalated" (poll e.requestId for the grant) | "denied" (replan)
}
}
Prefer branching on the decision instead of catching? Use authorizeToolCall,
which returns { decision, run } without throwing.
The invariant: client.authorizeTool fails closed — if Sanction is
unreachable it returns denied, so an ungoverned tool never runs.
TypeScript: Vercel AI SDK (sanctionTool, ships)
Wrap an AI SDK tool so its execute is gated — the model can pick the tool, but
it only runs on an approved decision:
import { tool } from "ai"
import { z } from "zod"
import { SanctionClient, sanctionTool } from "@sanction/sdk"
const client = new SanctionClient(process.env.SANCTION_AGENT_KEY!)
const deploy = sanctionTool(client, "deploy", tool({
description: "Deploy the app to an environment",
parameters: z.object({ env: z.string() }),
execute: async ({ env }) => shipIt(env),
}), { server: "ci" })
// pass `deploy` in your generateText/streamText `tools` map as usual
A non-approved decision throws SanctionToolBlocked, which the AI SDK surfaces
as a tool error the model can see and react to.
Python: LangChain / LangGraph callback
Wrap tool execution in a callback or runnable decorator:
import os
import httpx
SANCTION_API = os.getenv("SANCTION_API", "https://getsanction.com/api/v1")
SANCTION_KEY = os.environ["SANCTION_AGENT_KEY"]
def authorize_tool(server: str, tool: str, payload: dict | None = None) -> dict:
res = httpx.post(
f"{SANCTION_API}/authorize/tool",
headers={"x-api-key": SANCTION_KEY},
json={"server": server, "tool": tool, "input": payload or {}},
timeout=10,
)
res.raise_for_status()
return res.json()
def sanctioned_tool(server: str, tool: str, fn):
def wrapper(*args, **kwargs):
decision = authorize_tool(server, tool, {"args": args, "kwargs": kwargs})
if decision["status"] == "approved":
return fn(*args, **kwargs)
if decision["status"] == "escalated":
raise RuntimeError(f"Sanction escalation required: {decision['request_id']}")
raise RuntimeError(f"Sanction denied {tool}: {decision.get('code') or decision.get('reason')}")
return wrapper
Use the same pattern for spend (/authorize), provisioning
(/authorize/provision), and capability acquisition (/authorize/capability).
LiteLLM callback
For token spend, the simplest path is still the gateway base URL. If LiteLLM is already the gateway of record, add a callback that logs usage and uses Sanction as the budget authority for high-risk operations:
import os
import httpx
SANCTION_API = os.getenv("SANCTION_API", "https://getsanction.com/api/v1")
SANCTION_KEY = os.environ["SANCTION_AGENT_KEY"]
def log_tokens(model: str, tokens_in: int, tokens_out: int, cost_usd: float, task: str):
httpx.post(
f"{SANCTION_API}/tokens",
headers={"x-api-key": SANCTION_KEY},
json={
"model": model,
"tokens_in": tokens_in,
"tokens_out": tokens_out,
"cost_usd": cost_usd,
"task": task,
},
timeout=10,
).raise_for_status()
For a fail-closed budget wall before the provider call, route LiteLLM's provider
base URL through /api/gateway/<provider> with x-sanction-key. Manual
/tokens logging is useful for parity and reporting; it is not a pre-call wall.
Adapter checklist
- Fail closed on missing/invalid Sanction credentials.
- Treat
deniedas a normal planning outcome, not an exception to retry blindly. - Treat
escalatedas a wait-for-grant state. - Pass idempotency keys for retried actions.
- Store or link
request_idso evidence can be replayed later. - Keep provider keys in the runtime only until gateway vault-injected keys ship.