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.

