@raxitlabs/raxit
v0.0.6
Published
Thin, zero-dependency TypeScript client for the raxit / raxIT runtime governance daemon (permit / defer / deny on every tool call).
Downloads
546
Readme
raxit (TypeScript SDK)
Thin, zero-dependency TypeScript client for the raxIT runtime governance daemon
— permit / defer / deny on every tool call, decided by the engine before
the tool runs.
Zero-code governance (raxit run)
The fastest path is no SDK calls at all. raxit run boots the daemon, sets the
child environment, and preloads the auto-patcher so every tool call is
governed. raxit init scans your repo and generates the .raxit/ policy
workspace first — raxit run fail-closes without it:
raxit init # scan -> generate .raxit/
raxit run -- node agent.js # inherits the agent id from .raxit/security.yamlUnder the hood this sets RAXIT_AUTOLOAD=1 and
NODE_OPTIONS=--import raxit/autopatch-preload. The preload (an ESM module —
--import, not --require, so it shares the agent's module cache) patches the
tool-dispatch choke point of each detected framework.
| Framework | How it's governed |
| --- | --- |
| LangChain.js (StructuredTool.invoke / …) | Auto-patched (importable) — conformance-live |
| LangGraph.js (ToolNode.invoke / …) | Auto-patched (importable) — conformance-live |
| MCP servers (Server tools/call) | installMcpAutoPatch(server) per instance (opt-in); accepts a raw Server OR a high-level McpServer (unwraps .server itself) — conformance-live |
| Vercel AI SDK (registerTelemetry executeTool seam) | Auto-patched (importable) — permit/deny/defer; MODIFY via governedTool(); 4 documented bypasses — conformance-live |
| Mastra (Agent.prototype.convertTools seam) | Auto-patched (importable) — permit/deny/defer and MODIFY on the regular Agent path; durable/direct/clientTools NOT covered, explicit governedTool() required there — conformance-live |
Honest scope: JS has no Python sitecustomize. LangChain.js, LangGraph.js,
Vercel AI SDK, and Mastra auto-patch; MCP is per-instance. The Vercel AI SDK is
governed by registering a raxit integration on its global telemetry registry
(globalThis.AI_SDK_TELEMETRY_INTEGRATIONS, via the exported
registerTelemetry), whose executeTool wrapper runs before every tool
body — permit/deny only. Four documented silent fail-open bypasses come
with that seam (experimental_telemetry:{isEnabled:false}, a caller-supplied
experimental_telemetry:{integrations:[…]}, a direct tool.execute() call
outside generateText/streamText/Agent, and providerExecuted tools) —
see the Vercel AI SDK section below; zero-tolerance fleets must use
governedTool(). Mastra's seam has no such bypass on the regular Agent
path (see the Mastra section below for what it does NOT cover).
Coverage tier (mirrors the Python SDK's autopatch-unit-tested vs
conformance-live split — see tests/corpus/coverage-matrix.md): the unit
tests (sdk/ts/test/autopatch.test.ts, vercel.test.ts, mastra.test.ts)
drive hand-rolled fakes, but each auto-patched framework now also has a
conformance-live corpus entry that installs the REAL npm package end-to-end
under raxit run —
tests/corpus/frameworks-ts/{langchain-js,langgraph-js,mastra,mcp-server,vercel-ai}
— the TS analogue of tests/corpus/frameworks/ for Python, run nightly against
real npm installs (nightly-live.yml's frameworks-ts-live job); the
per-push bun test unit tests above remain fake-fast. installMcpAutoPatch
is real, working code, but it is opt-in — the developer must call it
explicitly, it is not wired into the raxit run auto-preload the way
LangChain.js/LangGraph.js/Vercel-AI/Mastra are. The
Python SDK's live-wire probe (a positive-interception canary that drives a
sentinel tool through the real dispatch path at install time) is also
deliberately deferred for TS — see the installAutopatch comment in
sdk/ts/src/autopatch.ts.
A blocked call throws RaxitDenied; a MODIFY verdict runs the tool on the
engine's transformed arguments; a permit runs it unchanged.
Manual install (without raxit run)
import { installAutopatch, installMcpAutoPatch } from "@raxitlabs/raxit/autopatch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
await installAutopatch(); // LangChain.js + LangGraph.js + Vercel AI SDK + Mastra
// MCP: call this BEFORE registering any tools (registerTool / setRequestHandler).
// Current upstream idiom wraps a low-level Server inside a high-level McpServer
// -- installMcpAutoPatch accepts either shape directly:
const mcpServer = new McpServer({ name: "my-server", version: "1.0.0" });
installMcpAutoPatch(mcpServer); // unwraps mcpServer.server itself
// (installMcpAutoPatch(mcpServer.server) also works -- same underlying Server)
mcpServer.registerTool("my_tool", { ... }, handler);Calling installMcpAutoPatch after a tools/call handler is already
registered is an ordering mistake: it retroactively wraps the already-registered
handler where the SDK's internals allow, but either way it emits a loud
[raxit] WARNING on stderr — never a silent ungoverned handler.
Vercel AI SDK — auto-patched (with caveats), plus governedTool for MODIFY
Under raxit run, the AI SDK is auto-patched: raxit registers a telemetry
integration on globalThis.AI_SDK_TELEMETRY_INTEGRATIONS (via the exported
registerTelemetry) whose executeTool wrapper governs every tool call
(toolName + input) BEFORE the tool body runs — permit runs it, deny/
defer throw and it never runs. This is decide-only (no per-tool code).
Four documented silent fail-open bypasses ship with this seam (see
tests/corpus/frameworks-ts/vercel-ai/README.md for the exact source
locations), and MODIFY is NOT supported here (the SDK captures input before
the wrapper, so it can't rewrite it):
experimental_telemetry: { isEnabled: false }disables the telemetry dispatcher → the raxit integration never fires.experimental_telemetry: { integrations: [...] }REPLACES the global registry → the raxit integration never fires.- A direct
tool.execute()called outsidegenerateText/streamText/Agentbypasses the dispatcher entirely — nothing to intercept. providerExecutedtools skip local execution (the provider runs them server-side), so there is no local tool body to govern.
For MODIFY (redaction) or zero-tolerance governance that can't be bypassed,
wrap the tool explicitly with governedTool instead:
import { GovernanceClient, governedTool } from "@raxitlabs/raxit";
const gov = new GovernanceClient();
const sendEmail = governedTool("send_email", gov, (args) => doSend(args));
// deny/defer -> a refusal string; MODIFY -> runs on the engine's modified args.Mastra — auto-patched, INCLUDING MODIFY on the regular Agent path
Under raxit run, Mastra is auto-patched: raxit wraps Agent.prototype.convertTools
(the seam every regular Agent.generate()/stream() call uses to build its
{ toolName: CoreTool } map) to post-wrap each returned tool's execute with
governance. Unlike the Vercel AI SDK, Mastra passes tool args as execute's
first parameter, so governance can rewrite them before the body runs —
MODIFY is supported live through the autopatch, no governedTool() wrap
needed for the send_email-style redact case.
HONEST COVERAGE SCOPE — read this. This governs the regular
Agent.generate()/stream() path only. The following are NOT covered and
require explicit governedTool() / a wrapped createTool:
- The durable agent's direct execution sites.
- Direct
makeCoreTool()users (tools built outside the agent'sconvertTools). - Direct
tool.execute()calls (a tool invoked outside the agent loop). - Mastra's internal AI-SDK-compat
executeTools/runToolsTransformationfamily. clientTools— theirexecuteis stripped server-side by design, so there is no local body to govern.
convertTools is internal-marked, so drift risk is HIGH; the fail-closed
canary (tests/corpus/frameworks-ts/mastra/canary_check.sh) is not optional —
see that corpus entry's README.md for the full seam writeup.
Programmatic client
import { RaxitClient } from "@raxitlabs/raxit";
const raxit = new RaxitClient({ agentId: "my-agent" });
const decision = await raxit.govern.decide({ resource: "db.query", args: { sql } });
if (decision.effect !== "permit") throw new Error(decision.reasonCode);Audit — verify the tamper-evident decision log
raxit.audit.verify() checks the hash-chained Decision Provenance log; pass
signatures: true with one or more pinned Ed25519 public keys to also verify
record signatures offline against a trusted key (not the daemon's own
config):
const raxit = new RaxitClient({ operatorToken: "<operator token>" });
const report = await raxit.audit.verify({
signatures: true,
requireSigned: true,
pubkeys: ["<base64 raw-32-byte Ed25519 pubkey>"],
});
if (!report.signatures_verified || report.signed_count === 0) throw new Error("audit chain not verified");Operator-gated (Authorization: Bearer <operatorToken>). For fully offline,
adversarial-grade verification use the CLI:
raxit audit verify --signatures --pubkey <key>.
Budget — read the session/principal budget projection
raxit.budget.get() is a strictly read-only projection of the engine's budget
accountant — never a reservation. It reports used/remaining against each
configured cap (call budget, per-session and per-day token/cost caps):
const raxit = new RaxitClient({ operatorToken: "<operator token>" });
const snap = await raxit.budget.get({ agentId: "payments-bot", sessionId: "sess-1" });
for (const b of snap.budgets) console.log(b.name, b.used, "/", b.limit, b.unit, `(${b.window})`);Operator-gated (Authorization: Bearer <operatorToken>). Fails closed: an
unreachable daemon throws GovernanceError — it is never read as an empty
or infinite budget. An empty snap.budgets means genuinely no cap configured.
Warrants — inspect the loaded standing grants
raxit.warrants.list() / raxit.warrants.get(id) are a read-only view of the
warrants loaded at raxit serve startup. There is no create/revoke — minting
stays the operator-controlled load path.
const raxit = new RaxitClient({ operatorToken: "<operator token>" });
for (const w of await raxit.warrants.list())
console.log(w.id, w.agentId, w.toolPattern, w.status, "expires", w.expiresAt);Operator-gated. Fails closed (throws GovernanceError when unreachable, so
"none" is never confused with "unreachable"). Warrants carry no credentials and
the attested intent-baseline text is never projected.
Environment
RAXIT_DAEMON_URL, RAXIT_AGENT_ID, RAXIT_IDENTITY_KEY,
RAXIT_GOVERNANCE (on/off), RAXIT_DEFER_TIMEOUT. Unreachable daemon
degrades closed (deny) unless failOpen is set (dev only).
RAXIT_GOVERNANCE
Governed by default. RAXIT_GOVERNANCE=off is the kill-switch for
contrast/before-after demos — the client then bypasses the daemon entirely
and every call synthesizes permit. Only two values are accepted (after
trim().toLowerCase()): on and off; unset defaults to on. Any other
value — 1/true/yes/0/false/no, or a typo — throws a
GovernanceError at client construction, before any network call, rather
than silently guessing.
