Edictum
Guides

Tutorial: Writing Rules

Turn a plain-English requirement into a ruleset, validate it, test it, and roll it out safely.

AI Assistance

Right page if: you are turning a requirement into a working `rules.yaml` file for the first time. Wrong page if: you already have a ruleset and just need tests -- see https://docs.edictum.ai/docs/guides/testing-rules. For ordered process enforcement, see https://docs.edictum.ai/docs/guides/workflow-gates. Gotcha: YAML double-quoted regex strings treat \b as backspace. Use single quotes for regex. Missing principal fields evaluate to false, so role-based rules silently stop matching.

This is the shortest reliable workflow for authoring rules: write the requirement, translate it to YAML, validate it, dry-run it, ship it in observe mode, then enforce it.

Step 1: Start With a Requirement

Suppose your team has this requirement:

Analysts should not be able to read secret files like .env, .pem, or credential files.

This is a check rule. You want to block the tool call before it executes.

Step 2: Translate It To YAML

Create a file called rules.yaml with a complete Ruleset:

apiVersion: edictum/v1
kind: Ruleset
metadata:
  name: analyst-file-policy
  description: "Block analysts from reading secret files."
defaults:
  mode: observe
rules:
  - id: block-secret-reads
    type: pre
    tool: read_file
    when:
      all:
        - args.path:
            contains_any: [".env", ".secret", "credentials", ".pem", "id_rsa"]
        - principal.role:
            equals: analyst
    then:
      action: block
      message: "Analysts cannot read '{args.path}'. Ask an admin for help."
      tags: [secrets, dlp]

Why this works:

  • type: pre means check before execution.
  • tool: read_file scopes the rule to one tool.
  • all means both conditions must match.
  • action: block blocks the call. Check rules also support action: ask.
  • mode: observe lets you verify the rule before enforcing it.

Step 3: Validate The Ruleset

Run the validator before you ship anything:

edictum validate rules.yaml

Validation catches schema problems like duplicate IDs, bad action names, invalid selectors, and malformed regex.

Step 4: Dry-Run It

Spot-check the rule without executing the tool:

edictum check rules.yaml \
  --tool read_file \
  --args '{"path": ".env"}' \
  --principal-role analyst

Or do the same in code:

from edictum import Edictum, Principal

guard = Edictum.from_yaml("rules.yaml")
result = guard.evaluate(
    "read_file",
    {"path": ".env"},
    principal=Principal(user_id="alice", role="analyst"),
)

assert result.decision == "block"
assert result.block_reasons[0] == "Analysts cannot read '.env'. Ask an admin for help."

Make sure the allowed case still passes:

allowed = guard.evaluate(
    "read_file",
    {"path": "readme.txt"},
    principal=Principal(user_id="alice", role="analyst"),
)

assert allowed.decision == "allow"

Step 5: Ship In Observe Mode

Notice that defaults.mode is set to observe. In this mode, Edictum logs what would be blocked without actually blocking anything. This is the safe rollout path.

from edictum import Edictum, Principal
from edictum.adapters.langchain import LangChainAdapter

guard = Edictum.from_yaml("rules.yaml")

adapter = LangChainAdapter(
    guard=guard,
    principal=Principal(user_id="alice", role="analyst"),
)

middleware = adapter.as_middleware()

Step 6: Review The Decision Log

In observe mode, calls that would have been blocked still run, but Edictum records the event. Review those events before switching to enforce:

{
  "action": "call_would_block",
  "tool_name": "read_file",
  "decision_name": "block-secret-reads",
  "tool_args": {"path": ".env"},
  "principal": {"user_id": "alice", "role": "analyst"},
  "reason": "Analysts cannot read '.env'. Ask an admin for help."
}

Check for:

  • False positives: legitimate calls that would be blocked.
  • False negatives: calls that should be blocked but are not.
  • Missing principal fields: if principal.role is null, the rule never matches.

Step 7: Flip To Enforce

Once the observed traffic looks right, switch to enforce:

defaults:
  mode: enforce

Now the tool call is blocked for real.

Common Mistakes

Wrong operator for the shape of the data

Using equals when you need contains:

# Wrong -- only matches if the entire path is literally ".env"
args.path:
  equals: ".env"

# Right -- matches any path containing ".env"
args.path:
  contains: ".env"

Missing principal field

If the principal does not have a role field set, selectors like principal.role resolve to null. That makes the rule stop matching.

Fix: ensure the principal is populated when creating the adapter:

principal = Principal(user_id="alice", role="analyst")
adapter = LangChainAdapter(guard=guard, principal=principal)

Regex escaping in YAML

YAML double-quoted strings interpret escape sequences. "\b" is a backspace character, not a word boundary. Always use single quotes for regex:

# Wrong -- "\b" is backspace
args.command:
  matches: "\brm\b"

# Right -- '\b' is literal backslash-b (word boundary)
args.command:
  matches: '\brm\b'

Using output.text in a check rule

output.text only exists in check_output rules. Using it in type: pre is a load-time validation error:

# Wrong -- output.text does not exist before the tool runs
- id: bad-pre
  type: pre
  tool: read_file
  when:
    output.text:
      contains: "SECRET"
  then:
    action: block
    message: "..."

Picking the wrong output action

Check output rules support three actions:

ActionWhat happens
warnEmit a violation. Output passes through unchanged.
redactReplace regex-matched patterns in the output with [REDACTED].
blockReplace the entire tool output with [OUTPUT SUPPRESSED] ....
- id: redact-ssn
  type: post
  tool: "*"
  when:
    output.text:
      matches: '\b\d{3}-\d{2}-\d{4}\b'
  then:
    action: redact
    message: "SSN pattern detected in output."

Use warn when you want a violation only, redact when cleanup is safe, and block when any matching output should be suppressed.

When To Reach For Sandbox Rules

If the deny-list keeps growing, stop adding more check rules and move the boundary into a sandbox rule.

Example: File Path Sandbox

Requirement: the agent should only read/write files in /workspace and /tmp, never in /workspace/.git.

- id: file-sandbox
  type: sandbox
  tools: [read_file, write_file, edit_file]
  within:
    - /workspace
    - /tmp
  not_within:
    - /workspace/.git
  outside: block
  message: "File access outside workspace: {args.path}"

Use sandbox rules for files, shell commands, and domains. See sandbox rules for the full model.

Next Steps

Last updated on

On this page