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-pyThe 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:
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.
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}"How it works
The whole integration is two decorators sharing one client and one session:
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:
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:
db.query→ permit, redacted, tainted. The policy allows the read, so it runs. After execution,governed_toolfeeds the result to the engine's/v1/recordendpoint; the output scanner classifies the email and SSN asPII, redacts them in the returned value (the wrapper hands back[REDACTED:pii_email]/[REDACTED:pii_ssn], not the raw row), and recordsPIItaint on the session. See Taint & the lethal trifecta for the label vocabulary and lattice.send.external→ deny. On its own,send.externalmatches a plainallow. But taint is session state: by the time this call is evaluated, the session's taint set containsPII, so thedenyWhenTainted: [PII]rule — which compiles to a Cedarforbid— wins over the allow (forbid-over-permit). The call never executes;EXFILTRATEDnever prints.- 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 784aca75d98b4c76bbd231d164a1430dBlocked 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.
Related
- Taint & the lethal trifecta — the concept: labels, the lattice, and
classify:/denyWhenTainted. - Permit / defer / deny / modify / step-up — the decision vocabulary.
- Audit & the DPR chain — the signed hash-chain verified in step 3.
- Rules & effects — the
security.yamlgrammar used above.
Quickstart tour
A real 60-second run: one allowed call permitted, one forbidden call denied with a named reason code, and the Ed25519-signed audit chain verified green — three SDK calls, no LLM in the decision path.
Human approval loop
A real defer → approve → permit run: a policy-deferred refund waits for a human, an operator approves it, and the agent resumes to permit — with the signed audit chain verified.

