raxit
Live demos

Demo: lethal trifecta

A permitted PII read taints the session; a later egress-allowlisted send is denied with deny.taint_trifecta — a real, executed demo.

The lethal trifecta: private-data access, untrusted content, and an external channel, together in one session — no single call looks dangerous alone (a db.query is a normal read; send.external is a normal integration). This demo shows raxit refusing the composition: a permitted db.query returns PII, the engine redacts it in-flight and taints the session, and a later send.external — otherwise allow-listed — is denied with deny.taint_trifecta. Every terminal line below is from a real execution, not a mock.

Run it yourself

From a clone of the repo, one make target builds the local engine, mints a throwaway Ed25519 signing key, starts raxit serve, runs the agent, asserts the reason code, and tears the daemon down:

git clone https://github.com/raxitlabs/runtime-security-core
cd runtime-security-core
make -C examples demo-lethal-trifecta-py

The demo lives in examples/capabilities/lethal-trifecta/ — a single trifecta.py plus its .raxit/security.yaml. It needs a Go toolchain (to build the engine) and uv; no API keys, no model credentials.

To use the same pattern in your own agent, install the SDK (pip install raxit) and wrap your tools with governed_tool as shown below.

Python only

This demo has an executed Python transcript. There is no equivalent captured TypeScript transcript for the taint path, so this page makes no claim about it.

The walkthrough

Step through the real code and the real output from the captured run:

Lethal-trifecta containment

Two tools share a single RaxitClient session. Taint accumulated by the first call must be visible to the second for the trifecta break to fire.

trifecta.py
client = RaxitClient(session_id="trifecta-session")

@governed_tool("db.query", client)
def db_query(q: str) -> str:
    return "customer row: alice@example.com ssn 123-45-6789"

@governed_tool("send.external", client)
def send_external(payload: str) -> str:
    return f"EXFILTRATED: {payload}"
terminal
$ $ make -C examples demo-lethal-trifecta-py
raxit lethal-trifecta · agent=trifecta-bot · session=trifecta-session

How it works

The whole integration is two decorators sharing one client and one session:

trifecta.py
from raxit import RaxitClient
from raxit.agent import governed_tool

client = RaxitClient(session_id="trifecta-session")

@governed_tool("db.query", client)
def db_query(q: str) -> str:
    return "customer row: alice@example.com ssn 123-45-6789"

@governed_tool("send.external", client)
def send_external(payload: str) -> str:
    return f"EXFILTRATED: {payload}"

The policy is three rules. Both tools are allowed; the third rule blocks send.external only when the session carries PII taint. Hand-writing that third rule is no longer the only way to get it: raxit init --taint-denial-posture strict now generates one denyWhenTainted rule like it per discovered egress-capable tool — the deny becomes one init answer, not a policy line you write by hand. That's still opt-in, not the default — raxit init on its own writes redact-and-permit (redaction at the boundary, no deny) unless you ask for strict; see Taint-denial posture and raxit init for the prompt and flag that turn it on. This demo's security.yaml is hand-authored and predates that feature — here's the resulting shape either way:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: trifecta-bot
default: block

rules:
  - effect: allow              # the read that taints the session (PII in its result)
    resource: db.query
  - effect: allow              # the baseline egress permit
    resource: send.external
  - effect: block              # wins over the allow above once PII-tainted
    resource: send.external
    denyWhenTainted: [PII]

The sequence, per the transcript:

  1. db.query → permit, redacted, tainted. The policy allows the read, so it runs. After execution, governed_tool feeds the result to the engine's /v1/record endpoint; the output scanner classifies the email and SSN as PII, redacts them in the returned value (the wrapper hands back [REDACTED:pii_email] / [REDACTED:pii_ssn], not the raw row), and records PII taint on the session. See Taint & the lethal trifecta for the label vocabulary and lattice.
  2. send.external → deny. On its own, send.external matches a plain allow. But taint is session state: by the time this call is evaluated, the session's taint set contains PII, so the denyWhenTainted: [PII] rule — which compiles to a Cedar forbid — wins over the allow (forbid-over-permit). The call never executes; EXFILTRATED never prints.
  3. Audit verifies green. Both decisions were fsync'd to a hash-chained, Ed25519-signed Decision Provenance log before each verdict returned. client.audit.verify(signatures=True, pubkeys=[…]) checks the chain and signatures against the pinned public key: ok=True signatures_verified=True signed_count=2 count=2. See Audit & the DPR chain.

The auto-record is load-bearing

The taint only advances because governed_tool calls /v1/record on every permitted call — that record step is where the output scanner sees the PII. A daemon lacking /v1/record fails closed rather than silently skipping it. If you bypass the wrapper and execute tools directly, the engine never sees the db.query result and the trifecta break cannot fire. This demo is specific to the governed_tool (per-tool SDK) path.

What got denied and why

Exactly one call was denied, with reason code deny.taint_trifecta. This is what the agent saw, verbatim from the run:

  send.external -> [raxit BLOCKED] 'send.external' denied (reason: deny.taint_trifecta). The operation was not executed. Details: raxit explain 784aca75d98b4c76bbd231d164a1430d

Blocked calls surface to the agent as a [raxit BLOCKED] string instead of the tool's result, so an LLM loop can read the refusal and re-plan rather than crash. The raxit explain <call_id> handle at the end resolves to the full decision record — which rule fired, what the session's taint set was, and the hash-chain position. The decision vocabulary is permit | defer | deny; see Permit / defer / deny.

Note what was not denied: db.query was permitted, and send.external would also have been permitted in a fresh, untainted session. The deny exists only because of what an earlier call returned — that cross-call statefulness is the point.

On this page