raxit

Limitations & honesty

What raxit does not do yet, stated plainly.

raxit's culture treats silently softening a caveat as a form of overclaiming. This page states what's limited today, why, what raxit does instead of pretending otherwise, and what's planned. Nothing here is spin.

macOS egress containment is unenforceable

Limited: on macOS, raxit can confine an agent's filesystem access via sandbox-exec (Seatbelt/SBPL), but it cannot confine network egress to a specific set of hosts.

Why: SBPL's (remote tcp ...) address filter only accepts the literal * (allow all) or localhost — not a hostname — so "allow calls to api.example.com, deny everything else" cannot be expressed in SBPL at all. A profile that tries fails the sandbox compiler outright.

What we do instead: enforce: true with a populated egress host list refuses to spawn on macOS rather than running unconfined or shipping a profile that looks scoped but isn't. Linux Landlock v1 has the identical gap — no network primitive — so the same refusal applies there too; it would need seccomp/netns, which is out of the current scope. This is enforced as a self-attack test on both platforms, not just documented.

What's planned: egress enforcement via seccomp/netns on Linux is a named follow-up. No equivalent primitive exists for macOS.

A single daemon caps out around ~340 decisions/sec

Limited: a single raxit daemon sustains roughly ~340 decisions/sec (measured 2026-07-13: ~332 dec/s sequential, ~349 dec/s at concurrency 50×20 — added concurrency does not raise the ceiling).

Why: this is architectural, not a bug. The audit write path holds a single lock across both writing the decision record and fsync'ing it to disk, because that is the audit-before-execute invariant — every decision is durably recorded before it returns, and a failed audit write flips the decision to deny (fail-closed). That serialization is what makes the audit-before-execute guarantee real; it also caps single-daemon throughput.

What we do instead: state the number plainly rather than let a customer discover it under load. Size deployments around a per-daemon ceiling: for higher aggregate throughput, shard by agent or by daemon rather than expecting one daemon to scale past this.

What's planned: batching/pipelining the audit write is a known future optimization, not yet implemented.

Proxy subcommands are experimental

Limited: the engine binary ships four additional enforcement subcommands beyond the SDK shim — raxit mcp-proxy, raxit http-proxy, raxit a2a-gateway, and raxit ext-authz. They are present and runnable, but experimental and unannounced: no stability contract, no dedicated docs yet, and flags/behavior may change between releases without a deprecation notice.

Why: these tiers exist to cover interception points the in-process SDK shim can't reach (an MCP proxy, an HTTP-level proxy, an agent-to-agent gateway, and an Envoy/Kong ext_authz sidecar), but they haven't gone through the same hardening and documentation pass as the shim.

What we do instead: one concrete gap worth knowing before relying on ext-authz: it maps any non-permit decision — including defer — straight to HTTP 403. There is no inline human-approval loop at that tier; a deferred call is simply rejected, not queued for review.

What's planned: stabilizing and documenting each proxy tier individually, starting with mcp-proxy, is on the roadmap but not yet scheduled.

MODIFY isn't supported at every framework's autopatch seam

Limited: the per-tool governed_tool (Python) / governedTool() (TypeScript) wrapper supports every decision type — permit, defer, deny, modify — on every framework. The zero-code raxit run autopatch now proves MODIFY live on ~20 framework surfaces, but 3 surfaces remain genuinely disclosed-unsupported, not silently missing.

Why: MODIFY needs the engine's rewritten arguments threaded back into the call before the tool body runs, which requires a per-chokepoint adapter that knows that framework's own input shape (a bare positional scalar, a JSON-string payload, a keyword-only context object, a nested envelope, …). That adapter work is now done for almost every zero-code chokepoint. It genuinely can't be done for three: claude-agent-sdk's chokepoint is a closed subprocess boundary; openai-agents' built-in <X>Action/handoff dispatch (as opposed to its FunctionTool custom-tool path, which is proven) carries no rewritable args at all; and the Vercel AI SDK's telemetry-integration seam fails closed on a MODIFY verdict rather than silently running unmodified args, because input is captured by the wrapper before the engine's rewrite could apply — a MODIFY policy against a Vercel AI tool requires the explicit governedTool() wrapper, full stop.

What we do instead: MODIFY zero-code, demoed live, is confirmed exactly where the Frameworks coverage matrix says it is — proven on all 16 Python frameworks except claude-agent-sdk, and on LangChain.js, LangGraph.js, Mastra, and MCP servers (TypeScript) — each verified by an automated release check that asserts the tool body itself observed the redacted value. The 3 disclosed-unsupported surfaces are named above, not folded quietly into the coverage count.

What's planned: no further framework-by-framework MODIFY expansion is currently scheduled; use governed_tool/governedTool() at any chokepoint where the autopatch seam can't carry it.

modify: truncate doesn't load

Limited: truncate isn't reachable via security.yaml: the loader rejects a modify: { truncate: [...] } rule outright. (modify: drop's prior silent fold gap — the tool body receiving the original, undropped value while the audit record showed it dropped — is fixed as of 0.0.4; release notes ship with the package — see the changelog included in the release.)

Why: truncate was never wired into security.yaml at all — the loader explicitly rejects it, because the current schema has no field for the max length a truncate needs, and a no-max truncate would itself silently forward the original arguments.

What we do instead: truncate isn't offered as an option at all today, so there's no silent-failure surface for it; use redact, hash, or drop instead.

What's planned: a schema extension for truncate's max-length field is a candidate follow-up, not yet scheduled.

R7 intent-drift is a per-call tripwire, not cumulative-creep detection

Limited: raxit's intent-drift check (R7) denies a call whose arguments land far from the session's reference-intent centroid — a per-call distance score, thresholded per call.

Why: the session's running cumulative distance (intent_drift.cumulative) is computed and recorded on every decision record for audit visibility, but it is never compared against a threshold or consulted in a deny decision. If you're picturing "no single step trips it, only the accumulated pattern does" — that's not what's enforced.

What we do instead: ship it honestly as a tripwire: an abrupt single-call jump denies, and a later, larger departure denies too — both via the same per-call score, not a cumulative one. The reference intent (r0) is set once at session start and is immutable afterward, so a prompt injection that sets a broad r0 early makes the gate decorative for that session — a known, documented gap, not a hidden one.

What's planned: an attested intent-warrant (tracked as a design doc) is the real fix for the r0 weakness; cumulative-sum enforcement is not yet scheduled.

Warrants are in-memory only

Limited: a warrant — a standing, scope-bounded pre-approval that turns a matching defer into an automatic permit — lives in daemon memory. Restarting the daemon loses it.

Why: warrant persistence behind a store wrapper is designed to come later; the current implementation is intentionally the simplest thing that works for a single running daemon.

What we do instead: nothing is silently assumed to survive a restart. A restarted daemon requires warrants to be re-issued, which is safer than a stale warrant surviving invisibly. (Note: the separate file-backed session-store option, used for taint/budget/rate state, does not cover warrants — the two are not the same mechanism.)

What's planned: warrant persistence is a documented, not-yet-built item.

Credential brokers are verified against recorded fixtures, not live cloud tenants

Limited: the AWS, GCP, Azure, and Vault credential brokers are tested against net/http/httptest fixtures that reproduce the real request/response shape of each provider's token-exchange API — not against a live cloud account.

Why: exercising STS/token-exchange calls against real cloud tenants in CI is both costly and a credential-management problem in its own right; the fixture approach captures the real wire format offline. AWS's AssumeRoleWithWebIdentity is unauthenticated at the transport level (the web-identity token is the auth), so the fixture reproduces the real request shape faithfully — but it is still not a live call.

What we do instead: state this plainly rather than imply cloud-tested coverage. STS credentials are also non-revocable by design once minted, which the broker's short (~30s) lease window is meant to bound.

What's planned: live-tenant conformance testing is not yet scheduled.

No cryptographic signing of release artifacts yet

Limited: audit records (Decision Provenance Records) are always hash-chained, but per-record cryptographic signing is opt-in code, off by default, and — when enabled — keyed from a development seed rather than a production KMS or certificate chain. Release artifacts (binaries, packages) are integrity-checked with sha256 and pinned in the launcher, not signed.

Why: the signing primitive exists (Ed25519, wired through WithSigner), but the trust-root migration that would make the signing key itself trustworthy (a TUF root, tracked in an ADR) is not built yet.

What we do instead: unsigned records verify as chain-intact via hashing alone (tamper still detectable by hash mismatch); the one place signing is mandatory rather than optional is deploy receipts, which refuse to verify if unsigned — but they use the same dev-seed key today. Don't read "signed" language into anything here beyond hash-chain integrity.

What's planned: a TUF-root key migration is designed but not implemented.

Benchmark numbers are self-authored, small-n

Limited: any precision/recall or efficacy numbers raxit cites are run in-house against a 24-case labeled evaluation set, not audited by a third party.

Why: the set is a deliberately small, class-balanced first batch — quality of labels over a rushed larger one. Cases are sourced from public taxonomies (OWASP LLM Top 10, OWASP Agentic AI, MITRE ATLAS technique IDs), but assembly and labeling is done in-house.

What we do instead: always state n=24 alongside any such number and treat it as an indicative signal, not a certified benchmark.

What's planned: the evaluation set is expected to grow; no fixed size or date is committed.

The tests: gate is client-side, at apply time

Limited: the tests: block in security.yaml is validated by raxit apply's own pipeline, client-side, before it pushes to a daemon. A bare authenticated POST /v1/reload straight to a running daemon loads and validates Cedar syntax, but does not run the policy's declared tests.

Why: the reload endpoint's job is narrowly "swap the compiled policy safely"; the tests gate is a separate concern that today lives entirely in the apply CLI path.

What we do instead: document this precisely rather than imply the daemon itself enforces the gate — always apply through raxit apply, not a raw reload call, if the tests gate matters to your deployment.

What's planned: moving the gate server-side is a candidate follow-up, not yet scheduled.

The zero-config default changed from a TCP daemon to a managed local engine

Behavior change: the zero-config default the SDK connects to has changed. It used to be an implicit http://localhost:8181 TCP daemon; it is now a managed auto-spawn over a per-project Unix domain socket (see Running the daemon). Any workflow that relied on a bare SDK call implicitly finding a raxit serve/raxit dev daemon on :8181 needs a one-line update.

Consequence to know before you hit it: if you run raxit serve yourself (TCP, e.g. :8181) and then make a plain SDK call with no RAXIT_DAEMON_URL set, the SDK will not discover that daemon — it spawns its own second managed engine over the socket instead. You now have two engines, each with an independent taint/budget/audit view for the same project, which silently defeats cross-call taint tracking (the exact lethal-trifecta protection the stateful governance layer exists for) without either engine erroring.

Mitigation: set RAXIT_DAEMON_URL=http://localhost:8181 (or whatever --addr/--listen you started the daemon with) so the SDK attaches to the running daemon instead of spawning a second one. An explicit RAXIT_DAEMON_URL is always honored first, ahead of the managed auto-spawn path.

Why we didn't auto-detect :8181 instead: the SDK deliberately does not try localhost:8181 before spawning a managed engine. Auto-adopting a plain TCP localhost port would reintroduce exactly the adoption path the managed socket's SO_PEERCRED same-uid check exists to close — any local process (including one controlled by a compromised dependency or a prompt- injected tool) can open a TCP port, but only a process running as your own uid can connect to the managed socket. Silently trusting :8181 would be a step backward on that boundary, so the inconvenience of the explicit env var is the intended trade, not an oversight.

Published package vs. engine source tree

Limited: the raxit package published on PyPI/npm (v0.0.4) can lag engine-side fixes and features already merged in the source tree between releases — publishing is a manual step, not tied to every merge.

Why: releases are manual and deliberate rather than an auto-publish-on-merge cadence. Engine binaries are published separately via the public raxit-engine-releases repository.

What we do instead: this documentation describes the current engine (source tree HEAD). Where a described behavior is genuinely new since the last publish, this doc calls it out inline. Run raxit version to check what you actually have.

What's planned: the next release picks these up for published-package users.

On this page