Rule Reference¶
A Rule is a single security constraint within a policy that defines: - A condition (when to trigger the rule) - An action (what to do when the condition is met) - A message (optional explanation for blocks)
Rules are immutable and use a fluent builder pattern for construction.
Basic Rule Structure¶
from tramlines.guardrail.dsl.rules import rule
from tramlines.guardrail.dsl.context import call
# Basic rule structure
my_rule = (rule("Descriptive rule name")
.when(call.name == "dangerous_tool")
.block("This tool is not allowed"))
Rule Builder Pattern¶
Rules use a fluent builder pattern that enforces proper construction:
rule("Rule name") # 1. Start with a descriptive name
.when(condition) # 2. Define the triggering condition
.block("message") # 3. Specify the action (block or allow)
# Alternative action
rule("Rule name")
.when(condition)
.allow() # Allow action (no message needed)
Building Conditions with Predicates¶
Basic Call Properties¶
Access properties of the current tool call using the call
context:
from tramlines.guardrail.dsl.context import call
# Match tool names
call.name == "get_user_data"
# Check arguments
call.arg("user_id") == "admin"
call.arg("query").contains("DROP", "DELETE")
String Operations¶
The DSL provides rich string operations for building conditions:
# Equality and comparison
call.name == "delete_user"
call.arg("priority") != "low"
# Pattern matching with regex
call.name.matches(r"^(create|update)_.*")
call.arg("email").matches(r".*@company\.com$")
# String content checks
call.arg("query").contains("DROP", "DELETE", "TRUNCATE")
call.name.startswith("admin_")
call.arg("filename").endswith(".sql", ".exe")
Logical Operators¶
Combine conditions using Python's logical operators:
# AND operations
(call.name == "transfer_money") & (call.arg("amount") > 10000)
# OR operations
(call.name == "delete_user") | (call.name == "deactivate_user")
History-Based Conditions¶
Query session history for complex patterns:
from tramlines.guardrail.dsl.context import history
# Check if any calls occurred
history.select(r"github_.*").exists()
# Count tool calls within time window
history.select("send_email").count(within="1h") > 5
# Access historical call properties
history.select("create_.*").last().arg("user_id") == call.arg("user_id")
# Complex history queries
history.select("create_user").where(call.arg("role") == "admin").count() > 0
Time Windows¶
History queries support time-based filtering:
# Count calls in different time windows
history.select("api_call").count(within="1m") # Last minute
history.select("api_call").count(within="1h") # Last hour
history.select("api_call").count(within="1d") # Last day
history.select("api_call").count(within="30s") # Last 30 seconds
# Time window formats
"10s" # 10 seconds
"5m" # 5 minutes
"2h" # 2 hours
"1d" # 1 day
Rule Actions¶
Rules can take two types of actions:
Block Actions¶
Block actions prevent the tool call from executing and return an error message:
rule("Block dangerous operations")
.when(call.name.contains("delete", "destroy"))
.block("Destructive operations are not permitted")
rule("Rate limit API calls")
.when(history.select("api_call").count(within="1h") > 100)
.block("API rate limit exceeded - maximum 100 calls per hour")
Allow Actions¶
Allow actions immediately permit the tool call and skip evaluation of remaining rules:
rule("Allow read-only operations")
.when(call.name.startswith("get_") | call.name.startswith("list_"))
.allow() # No message needed, skips remaining rules
rule("Allow low-risk operations")
.when(call.name.startswith("get_") & call.arg("sensitive") != "true")
.allow()
Custom Predicates¶
For complex logic that can't be expressed with built-in predicates, use custom functions. Custom predicates must return boolean values and should handle edge cases gracefully:
from tramlines.guardrail.dsl.predicates import custom
from tramlines.session import CallHistory, ToolCall
def single_user_predicate(current_call: ToolCall, session_history: CallHistory) -> bool:
"""Ensure operations are limited to a single user."""
current_user = current_call.arguments.get("user_id")
if not current_user:
return False
# Check if a different user was accessed previously
for call in session_history.calls:
previous_user = call.arguments.get("user_id")
if previous_user and previous_user != current_user:
return True # Different user detected
return False
# Use in a rule
rule("Enforce single user per session")
.when(custom(single_user_predicate))
.block("You may only operate on one user account per session")