Built-in guardrails
This page describes guardrails shipped with Railtracks, how they are organized, and how to try them in isolation with decide(). For attaching rails to agents and the Guard container, see Overview and Quickstart.
Introduction
Where they live. Core types (Guard, InputGuard, OutputGuard, LLMGuardrailEvent, GuardrailDecision, …) live in the railtracks.guardrails package. Built-in LLM guard implementations (today: PII redaction) live under railtracks.guardrails.llm and are re-exported from that module for a single import path.
from railtracks.guardrails import (
Guard,
GuardrailDecision,
InputGuard,
LLMGuardrailEvent,
OutputGuard,
)
from railtracks.guardrails.llm import (
PIICustomPattern,
PIIEntity,
PIIRedactConfig,
PIIRedactInputGuard,
PIIRedactOutputGuard,
)
decide()
InputGuard and OutputGuard define decide(...) so you can run a guard without building an LLMGuardrailEvent by hand. For input guards, a str is treated as a single user message. For output guards, a str becomes the assistant message under inspection. You can also pass Message, MessageHistory, or a full LLMGuardrailEvent. On a match, inspect GuardrailDecision.messages (input guard) or GuardrailDecision.output_message (output guard) for the rewritten content.
PII guards return TRANSFORM when they rewrite text, with redacted content on decision.messages (input) or decision.output_message (output). They return ALLOW when there is nothing to change.
Contributing a built-in guardrail
- Implement the guard in
packages/railtracks/src/railtracks/guardrails/llm/(e.g.input/andoutput/modules, shared logic in a private subpackage such as_pii/). - Subclass
InputGuardorOutputGuard, implement__call__(self, event: LLMGuardrailEvent) -> GuardrailDecision, and rely ondecide()from the base class for ad hoc testing. - Export public names from
railtracks/guardrails/llm/__init__.py(and submodules’__init__.pyif you split them). - Add unit tests under
packages/railtracks/tests/unit_tests/guardrails/. - Extend
docs/scripts/builtin_guardrails_examples.pywith snippet regions and document the guard in this file under Guardrails.
Keep dependencies optional or zero unless the feature truly needs them; document behavior, limits, and false-positive/false-negative tradeoffs briefly.
Guardrails
PII redaction
PIIRedactInputGuard and PIIRedactOutputGuard scan string message content with regex. Matches are replaced by placeholders such as [EMAIL_ADDRESS]. The input guard scans user and system messages; assistant and tool messages are left unchanged. The output guard scans the model’s output message. Non-string content is passed through unchanged.
Detection uses a fixed priority when patterns overlap (for example, email and URL win over the broader phone pattern). CREDIT_CARD and CA_SIN matches are accepted only when they pass a Luhn checksum check.
Entities
Built-in entity enum PIIEntity includes:
| Entity | Notes |
|---|---|
EMAIL_ADDRESS |
Common email shapes |
PHONE_NUMBER |
+country, parentheses, dots, dashes; 7–10 digit core |
CREDIT_CARD |
13–16 digit groups, Luhn-validated |
US_SSN |
###-##-#### with word boundaries |
CA_SIN |
Canadian SIN ###-###-###, Luhn-validated |
IP_ADDRESS |
IPv4 |
URL |
http:// or https:// only |
IBAN_CODE |
IBAN with optional spaces |
Discover names and short descriptions at runtime:
names_to_help = PIIEntity.available()
# e.g. {"EMAIL_ADDRESS": "Email addresses (e.g. alice@example.com)", ...}
Configuration
PIIRedactConfig is a frozen Pydantic model: default entities is the full list above; custom_patterns defaults to empty. Each PIICustomPattern has a name (used in the placeholder) and a regex string.
config = PIIRedactConfig(
entities=[
PIIEntity.EMAIL_ADDRESS,
PIIEntity.CA_SIN,
]
)
redact_input = PIIRedactInputGuard(config=config, name="RedactEmail")
msg = (
"My name is Alice and my email is alice@example.com "
"and my SIN is 163-180-003"
)
result = redact_input.decide(msg)
# result.messages — redacted user message(s)
Sample result.messages
Custom patterns
You are not limited to PIIEntity values. Add PIICustomPattern(name=..., regex=...) entries to custom_patterns; each name becomes the placeholder label (for example EMPLOYEE_ID produces [EMPLOYEE_ID]). Use them alone or together with any built-in entities in the same PIIRedactConfig.
custom_config = PIIRedactConfig(
entities=[PIIEntity.EMAIL_ADDRESS],
custom_patterns=[
PIICustomPattern(name="EMPLOYEE_ID", regex=r"\bEMP-\d{6}\b"),
],
)
guard_with_custom = PIIRedactInputGuard(config=custom_config)
result = guard_with_custom.decide(
"My ID is EMP-123456; contact hr@company.example internally."
)
# result.messages — redacted user message(s), e.g. [EMPLOYEE_ID] and [EMAIL_ADDRESS]
Illustrative redacted line
For the sample string in the snippet above, the user message content may look like: My ID is [EMPLOYEE_ID]; contact [EMAIL_ADDRESS] internally.
Use the same PIIRedactConfig instance for both input and output guards if you want identical rules.
Agent usage
Attach like any other guard:
import railtracks as rt
Agent = rt.agent_node(
name="pii-redact-demo",
llm=rt.llm.GeminiLLM("gemini-2.5-flash"),
system_message="You are a concise assistant.",
guardrails=Guard(
input=[PIIRedactInputGuard()],
output=[PIIRedactOutputGuard()],
),
)
Scope
This PII layer is regex-only: no NER/Presidio, no MASK mode, no streaming-specific API, and no redaction inside tool calls yet. Those may arrive in later releases.