raxit
Live demos

Demo: automatic PII redaction

A send_email carrying a raw card number is rewritten in-flight: the engine returns MODIFY with redacted args, the tool runs on those, and the PII never leaves — a real, executed demo.

When an agent emails a receipt whose body contains a raw credit-card number, refusing the call breaks the workflow but permitting it as-is leaks PII. This demo shows the middle path: a modify: rule makes the engine return Decision.type == "MODIFY" with modified_args, and governed_tool runs the tool body on the engine's redacted arguments, never the raw ones — the email still sends, the card number never leaves. 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 signing every record, 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-modify-redaction-py

The demo lives in examples/capabilities/modify-redaction/ — a single modify_redaction.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 MODIFY path, so this page makes no claim about it.

The walkthrough

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

Automatic PII redaction (MODIFY)

One governed tool, one client. The agent calls send_email with a raw credit-card number in the body — the classic accidental-PII-egress moment.

modify_redaction.py
client = RaxitClient()

@governed_tool("send_email", client)
def send_email(to: str, subject: str, body: str) -> str:
    return f"send_email OK — sent to {to} subject={subject!r} body={body!r}"

args = {"to": "a@b.c", "subject": "receipt", "body": "card 4242424242424242"}
terminal
$ $ make -C examples demo-modify-redaction-py
raxit modify-redaction · agent=modify-bot · daemon=http://127.0.0.1:8301

How it works

The whole integration is one decorator and one client:

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

client = RaxitClient()

@governed_tool("send_email", client)
def send_email(to: str, subject: str, body: str) -> str:
    return f"send_email OK — sent to {to} subject={subject!r} body={body!r}"

args = {"to": "a@b.c", "subject": "receipt", "body": "card 4242424242424242"}
out = send_email(**args)

The policy is a single rule. Note it carries modify: instead of an effect: — the two are mutually exclusive in the security.yaml grammar:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: modify-bot
default: block
rules:
  - resource: send_email        # MODIFY: redact PII in the body before sending
    modify: { redact: [body] }

The sequence, per the transcript:

  1. send_email → MODIFY, enforced. The rule matches, so the engine returns a permit-shaped decision with type=MODIFY, reason=modify.policy, and modified_args in which the body's card number has been rewritten to [REDACTED:pii_credit_card]. governed_tool merges modified_args over the caller's args and runs the tool body on the result — the transform is applied by the wrapper, not merely suggested to it. The exact redaction markup is the engine's own classifier output, not scripted by the example.
  2. The decision, inspected directly. The example repeats the same call through client.govern.decide(...) and prints decision.modified_args["body"] — the literal card [REDACTED:pii_credit_card] string, straight from the engine.
  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.

MODIFY fails closed

modify is permit-with-transformation, not a fourth verdict — the decision vocabulary stays permit | defer | deny. That makes the transform load-bearing: a MODIFY decision that arrives without applicable modified_args is not forwarded with the raw arguments — the call fails closed to deny.modify_error instead. See Permit / defer / deny / modify / step-up for the resolution table.

What got modified — and what would be denied

Nothing was denied in this run, and that is the point of MODIFY: the reason code the CI oracle asserts is modify.policy, a permit that carries a mandatory transform. Verbatim from the run:

  decide()      -> type=MODIFY reason=modify.policy modified_args.body='card [REDACTED:pii_credit_card]'
[PASS] modify.policy: engine returned MODIFY with a redacted body — card [REDACTED:pii_credit_card]

Two denial paths still guard this policy, even though neither fired here:

  • default: block — any tool other than send_email has no matching rule and is denied outright. The agent would see a [raxit BLOCKED] ... string in place of the tool's result.
  • deny.modify_error — if the declared transform cannot be applied, the engine denies rather than silently forwarding the unredacted arguments.

Contrast this with the lethal-trifecta demo, where redaction happens on a tool's output (a db.query result) and feeds session taint. Here the redaction happens on the arguments — the data about to leave through send_email — before the call ever executes.

On this page