Compliance and Audit Patterns
Compliance patterns address regulatory and organizational requirements: classifying rulesets with tags, tracking ruleset versions, rolling out new c...
Right page if: you need to tag rulesets for regulatory classification, track ruleset versions for decision logs, or deploy new rulesets safely with observe mode. Wrong page if: you need the full EU AI Act / SOC 2 compliance mapping -- see https://docs.edictum.ai/docs/security/compliance. Gotcha: tags on rule actions appear in every audit event and can be filtered downstream. Use `observe_alongside` to run a new rule version in observe mode alongside your enforced version -- both produce audit events, but only the enforced version blocks.
Compliance patterns address regulatory and organizational requirements: classifying rulesets with tags, tracking ruleset versions, rolling out new rulesets safely with observe mode testing, and filtering audit events downstream.
Regulatory Tags
Use the tags field on rule actions to classify rulesets by regulatory or organizational concern. Tags appear in every audit event and can be filtered, aggregated, and reported on downstream.
When to use: You need to demonstrate compliance with specific regulations or internal policies, and your audit system needs to categorize events by concern.
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: tagged-compliance
defaults:
mode: enforce
rules:
- id: pii-output-scan
type: post
tool: "*"
when:
output.text:
matches_any:
- '\\b\\d{3}-\\d{2}-\\d{4}\\b'
- '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b'
then:
action: warn
message: "PII detected in output. Redact before downstream use."
tags: [pii, compliance, data-protection]
- id: sensitive-data-access
type: pre
tool: query_database
when:
args.table:
in: [user_profiles, payment_records, access_logs]
then:
action: block
message: "Access to '{args.table}' requires explicit authorization."
tags: [compliance, sensitive-data, audit-required]
metadata:
severity: high
regulation: internal-policyimport re
from edictum import Decision, precondition
from edictum.rulesets import postcondition
@postcondition("*")
def pii_output_scan(envelope, tool_response):
if not isinstance(tool_response, str):
return Decision.pass_()
patterns = [
r"\b\d{3}-\d{2}-\d{4}\b",
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
]
for pat in patterns:
if re.search(pat, tool_response):
return Decision.fail(
"PII detected in output. Redact before downstream use.",
tags=["pii", "compliance", "data-protection"],
)
return Decision.pass_()
@precondition("query_database")
def sensitive_data_access(envelope):
table = envelope.args.get("table", "")
if table in ("user_profiles", "payment_records", "access_logs"):
return Decision.fail(
f"Access to '{table}' requires explicit authorization.",
tags=["compliance", "sensitive-data", "audit-required"],
)
return Decision.pass_()How tags work:
- Tags are arrays of strings attached to the
thenblock. They are stamped into theDecisionand everyAuditEventproduced by the rule. - Tags are free-form. Use a consistent naming convention across your rulesets (e.g.,
pii,compliance,dlp,change-control). - Downstream systems can filter audit events by tag. For example, a compliance dashboard could show all events tagged
piiorcompliance.
Gotchas:
- Tags do not affect rule evaluation. They are metadata only. A rule tagged
[compliance]behaves identically to one with no tags. - There is no validation of tag values. Typos in tags (e.g.,
compliancinstead ofcompliance) will not produce errors but will silently break downstream filtering.
Ruleset Versioning
Every YAML ruleset gets a SHA256 hash computed at load time. This hash is stamped as policy_version on every AuditEvent and OpenTelemetry span, creating an immutable link between any audit record and the exact ruleset that produced it.
When to use: You need to prove which version of a ruleset was active when an event occurred. This is essential for audits, incident investigations, and regulatory compliance.
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: versioned-policy
description: "Production policy v2.3 -- approved 2025-01-15."
defaults:
mode: enforce
rules:
- id: block-sensitive-reads
type: pre
tool: read_file
when:
args.path:
contains_any: [".env", "credentials", ".pem"]
then:
action: block
message: "Sensitive file denied."
tags: [secrets, dlp]
metadata:
severity: highfrom edictum import Edictum
# Load a versioned YAML ruleset — the SHA256 hash is computed
# automatically and stamped on every audit event.
guard = Edictum.from_yaml("policy.yaml")
# The policy_version attribute contains the hash
# print(guard.policy_version) # "a1b2c3d4..."How versioning works:
Edictum.from_yaml("policy.yaml")reads the raw YAML bytes.- A SHA256 hash is computed from the bytes.
- Every audit event produced by this ruleset includes
policy_version: <hash>. - If the YAML file changes by even one byte, the hash changes, creating a new version.
Gotchas:
- The hash is computed from the raw file bytes, not the parsed structure. Whitespace changes, comment additions, and reordering produce different hashes.
- Store your YAML files in version control. The hash tells you which version was active; the VCS history tells you what changed and who changed it.
- The
metadata.descriptionfield is a good place to record human-readable version information, but it is not used in the hash computation -- the hash covers the entire file.
Dual-Mode Deployment
Roll out new rulesets safely by starting in observe mode and switching to enforce after verifying the rule behaves as expected. Observed denials are logged as CALL_WOULD_DENY audit events without denying tool calls.
When to use: You are adding a new rule to an existing production ruleset and want to validate it against real traffic before enforcing it.
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: dual-mode-rollout
defaults:
mode: enforce
rules:
# Existing enforced rule
- id: block-sensitive-reads
type: pre
tool: read_file
when:
args.path:
contains_any: [".env", "credentials", ".pem"]
then:
action: block
message: "Sensitive file denied."
tags: [secrets, dlp]
# New rule in observe mode -- observe mode testing
- id: experimental-cost-gate
type: pre
mode: observe
tool: query_database
when:
args.query: { matches: '\\bJOIN\\b.*\\bJOIN\\b.*\\bJOIN\\b' }
then:
action: block
message: "Query with 3+ JOINs detected (observe mode). Consider optimizing."
tags: [cost, experimental]from edictum import Edictum, Decision, precondition
from edictum.audit import FileAuditSink
@precondition("read_file")
def block_sensitive_reads(envelope):
path = envelope.args.get("path", "")
for s in (".env", "credentials", ".pem"):
if s in path:
return Decision.fail("Sensitive file denied.")
return Decision.pass_()
# Enforced guard (blocks tool calls)
enforced_guard = Edictum(rules=[block_sensitive_reads])
# Observe guard (logs but never blocks)
observe_guard = Edictum(
mode="observe",
rules=[block_sensitive_reads],
audit_sink=FileAuditSink("audit.jsonl"),
)How dual-mode works:
- Set
defaults.mode: enforcefor the ruleset. - On the new rule, add
mode: observeto override the ruleset default. - When the observe-mode rule matches, it emits a
CALL_WOULD_DENYaudit event. The tool call proceeds normally. - Review
CALL_WOULD_DENYevents in your audit logs. If the rule fires correctly with no false positives, change it tomode: enforce(or remove the override to inherit the ruleset default).
Gotchas:
- Observe mode applies to all rule types. For postconditions, when
mode: observeis set and the condition matches, actions (redact/block) are downgraded to a warning and the message is prefixed with[observe]. The tool output is not modified. - A
CALL_WOULD_DENYevent contains the same information as a real block event (rule ID, message, tags, metadata). The only difference is the event type. - Do not leave rulesets in observe mode indefinitely. Unreviewed observe-mode rulesets accumulate audit noise without providing protection.
Tag-Based Filtering Downstream
Tags enable downstream systems to filter, route, and aggregate audit events by concern. This pattern shows how to design tags for common compliance workflows.
Recommended tag taxonomy:
| Tag | Use Case |
|---|---|
pii | Events involving personally identifiable information |
secrets | Events involving credentials, tokens, or keys |
dlp | Data loss prevention events |
compliance | Events relevant to regulatory compliance |
change-control | Events related to production changes |
rate-limit | Session limit events |
cost | Events related to resource cost |
experimental | Observe-mode rulesets |
Example: filtering audit events in Python:
from edictum.audit import FileAuditSink
sink = FileAuditSink("audit.jsonl")
# After loading audit events, filter by tag:
# events = [e for e in all_events if "pii" in e.tags]Gotchas:
- Tags are arrays, so a single event can have multiple tags. An event tagged
[pii, compliance]appears in both filters. - Define your tag taxonomy before writing rulesets. Inconsistent tagging across rulesets makes downstream filtering unreliable.
Last updated on
Rate Limiting Patterns
Rate limiting rulesets use session-level counters to govern cumulative agent behavior across tool calls.
Advanced Patterns
This page covers patterns that combine multiple Edictum features: nested boolean logic, regex composition, principal claims, template composition, wildcards,...