Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Policy Enforcer

The policy enforcer provides declarative, TOML-based authorization rules that are evaluated before any tool call executes. It is the outermost layer of the tool execution stack, sitting above TrustGateExecutor.

Feature flag: policy-enforcer (optional, included in full). The feature is off by default and adds no overhead when disabled.

Security Model

  • Deny-wins semantics: deny rules are evaluated first across all rules. If any deny rule matches, the call is blocked regardless of allow rules.
  • Insertion-order independent: the order of rules in the config does not affect the deny-wins outcome.
  • Path normalization (CRIT-01): path parameters are lexically normalized before matching — /tmp/../etc/passwd becomes /etc/passwd. This prevents traversal bypasses. No filesystem I/O occurs during normalization.
  • Tool name normalization (CRIT-02): tool names are lowercased and trimmed before glob matching, preventing aliasing via mixed case.
  • Generic LLM error (MED-03): when a call is blocked, the LLM receives only "Tool call denied by policy". The rule trace goes to the audit log only.
  • Compile-time limits: max 256 rules, max 1024 bytes per regex pattern. Prevents OOM from malformed policy files.
  • User confirmation bypass prevention (MED-04): execute_tool_call_confirmed also enforces policy. User confirmation does not bypass declarative authorization.

Configuration

[tools.policy]
enabled = true
default_effect = "deny"     # Fallback when no rule matches: "allow" or "deny"
# policy_file = "policy.toml"  # Optional external rules file (overrides inline rules)

Inline Rules

[[tools.policy.rules]]
effect = "deny"             # "allow" or "deny"
tool = "shell"              # Glob pattern for tool name (case-insensitive)
paths = ["/etc/*", "/root/*"]  # Path globs; matched after lexical normalization
# trust_level = "verified"  # Optional: rule only applies when trust <= this level
# args_match = ".*sudo.*"   # Optional: regex matched against individual string param values

[[tools.policy.rules]]
effect = "allow"
tool = "shell"
paths = ["/tmp/*"]

External Policy File

When policy_file is set, rules are loaded from that TOML file instead of inline [[tools.policy.rules]]. The file is read once at startup. Format:

[[rules]]
effect = "deny"
tool = "shell"
paths = ["/etc/*"]

[[rules]]
effect = "allow"
tool = "shell"
paths = ["/tmp/*"]

File size is capped at 256 KiB.

CLI Flag

zeph --policy-file /path/to/policy.toml

This overrides tools.policy.policy_file from the config file and enables the policy enforcer (enabled = true).

Slash Commands

CommandDescription
/policy statusShow whether policy is enabled, rule count, default effect, and optional file path.
/policy check <tool> [args_json]Dry-run evaluation. Returns Allow or Deny with the matching rule trace.

Examples:

/policy status
/policy check shell {"file_path":"/etc/passwd"}
/policy check bash {"command":"sudo rm -rf /"}

Rule Fields

FieldTypeDescription
effect"allow" or "deny"Action when this rule matches.
toolglob stringTool name pattern (case-insensitive). * matches any tool.
paths[string]Optional path globs. Extracted from file_path, path, directory, dest, source, and absolute paths in command.
trust_leveltrust level stringOptional maximum trust level for this rule to apply ("trusted", "verified", "quarantined", "blocked").
args_matchregex stringOptional regex matched against each individual string param value.
env[string]Optional list of environment variable names that must be present.

Examples

Allow-list: only /tmp is writable

[tools.policy]
enabled = true
default_effect = "deny"

[[tools.policy.rules]]
effect = "allow"
tool = "shell"
paths = ["/tmp/*"]

[[tools.policy.rules]]
effect = "allow"
tool = "file_*"
paths = ["/tmp/*"]

Block sudo commands

[[tools.policy.rules]]
effect = "deny"
tool = "shell"
args_match = ".*sudo.*"

Restrict quarantined callers to read-only

[[tools.policy.rules]]
effect = "deny"
tool = "shell"
trust_level = "quarantined"

[[tools.policy.rules]]
effect = "allow"
tool = "file_read"
trust_level = "quarantined"
paths = ["/tmp/*", "/home/*"]

Wiring Order

PolicyGateExecutor       ← outermost (policy check)
  └─ TrustGateExecutor   ← trust level enforcement
       └─ CompositeExecutor
            └─ ShellExecutor / FileExecutor / ...

Policy is checked before trust level gating. A deny decision short-circuits the entire chain.

Audit Logging

When an [tools.audit] logger is attached, every policy decision (allow and deny) is recorded with timestamp, tool name, truncated params, and result. Deny entries include the full rule trace in the reason field — this trace is never sent to the LLM.

[tools.audit]
enabled = true
destination = ".zeph/audit.jsonl"

OAP Authorization Config

A separate [tools.authorization] section provides a supplementary authorization layer that sits alongside the policy enforcer. Unlike the inline [[tools.policy.rules]], authorization rules are merged into PolicyEnforcer at startup after policy rules (policy takes precedence). This lets you split operational rules (in [tools.policy]) from access-control rules (in [tools.authorization]) across different config files or config management systems.

[tools.authorization]
enabled = true

[[tools.authorization.rules]]
effect    = "deny"
tool      = "bash"
args_match = ".*sudo.*"

[[tools.authorization.rules]]
effect = "allow"
tool   = "read"
paths  = ["/home/user/*"]

Rule fields are identical to [[tools.policy.rules]]. The capabilities field on PolicyRuleConfig is reserved for future use when tools expose structured capability metadata (M4).

Note

Authorization rules do not replace policy rules — they extend them. The wiring order is: [tools.policy.rules] first, then [tools.authorization.rules]. First-match-wins semantics apply across the merged set.

Migrate Config

When upgrading from a config that predates policy enforcer support, run:

zeph --migrate-config --in-place

This adds [tools.policy] with enabled = false as a commented-out block so you can discover and enable it without manual editing.