Policy Engine¶
The policy engine is Missy's central authorization layer. It composes three domain-specific engines behind a single PolicyEngine facade, each enforcing a different class of access control.
Every check is audited
Every policy evaluation -- allow or deny -- emits a structured AuditEvent to the event bus. This provides a complete, tamper-evident record of all access attempts.
Architecture¶
graph LR
A[AgentRuntime] --> B[PolicyEngine]
B --> C[NetworkPolicyEngine]
B --> D[FilesystemPolicyEngine]
B --> E[ShellPolicyEngine]
C --> F[AuditEvent]
D --> F
E --> F
F --> G["audit.jsonl"] The PolicyEngine is initialized once from MissyConfig and installed as a thread-safe process-level singleton:
from missy.policy.engine import init_policy_engine, get_policy_engine
init_policy_engine(config)
engine = get_policy_engine()
engine.check_network("api.github.com", category="provider")
NetworkPolicyEngine¶
Controls all outbound network access. Operates in default-deny mode: every connection is blocked unless explicitly allowed.
Evaluation Order¶
The engine evaluates requests in this order, short-circuiting on the first match:
- Default-allow check -- If
default_deny: false, allow everything (not recommended). - Bare IP check -- If the host is an IP address, check against
allowed_cidrsonly. - Exact host match -- Check
allowed_hostsand per-category host lists. - Domain suffix match -- Check
allowed_domainswith wildcard support. - DNS resolution + CIDR re-check -- Resolve the hostname, then check resolved IPs against
allowed_cidrs. - Deny -- Raise
PolicyViolationError.
Configuration¶
network:
default_deny: true
# CIDR blocks (IPv4 and IPv6)
allowed_cidrs:
- "10.0.0.0/8"
- "fd00::/8"
# Domain names (exact or wildcard suffix)
allowed_domains:
- "api.anthropic.com" # Exact match
- "*.github.com" # Matches api.github.com, github.com, etc.
# Explicit host:port pairs
allowed_hosts:
- "api.anthropic.com:443"
# Per-category overrides (merged with global lists)
provider_allowed_hosts:
- "api.anthropic.com:443"
- "api.openai.com:443"
tool_allowed_hosts:
- "api.github.com:443"
discord_allowed_hosts:
- "discord.com:443"
- "gateway.discord.gg:443"
Domain Matching¶
Domain patterns support two forms:
| Pattern | Matches | Does Not Match |
|---|---|---|
github.com | github.com | api.github.com |
*.github.com | github.com, api.github.com, raw.github.com | evil-github.com |
Wildcard scope
*.github.com matches github.com itself and any subdomain. It does not match domains that merely end with the string (e.g., not-github.com is not matched).
Per-Category Host Lists¶
Network requests carry a category tag identifying the subsystem making the request:
| Category | Host List | Typical Use |
|---|---|---|
provider | provider_allowed_hosts | AI provider API calls |
tool | tool_allowed_hosts | Tool HTTP requests |
discord | discord_allowed_hosts | Discord API and Gateway |
Per-category lists are checked in addition to the global allowed_hosts and allowed_domains. A host allowed in the global list is accessible to all categories.
DNS Rebinding Protection¶
DNS rebinding attacks
An attacker can point evil.example.com at 169.254.169.254 (cloud metadata) or 10.0.0.1 (internal infrastructure). Without DNS rebinding protection, a domain-allowlisted hostname could reach private networks.
When a hostname passes domain/host checks and proceeds to DNS resolution, the engine applies strict rebinding protection:
- Resolve all DNS records for the hostname.
- Check every resolved IP -- if any IP is private, loopback, or link-local, verify it is explicitly covered by
allowed_cidrs. - If any resolved IP is private and not allowed, deny the entire request. This prevents mixed-record attacks where a hostname resolves to both public and private IPs.
evil.example.com → 93.184.216.34, 169.254.169.254
↑ private, not in allowed_cidrs
→ DENIED (entire request blocked)
CIDR Matching¶
CIDR blocks are parsed once at engine construction time for performance. Both IPv4 and IPv6 are supported:
allowed_cidrs:
- "10.0.0.0/8" # Private IPv4
- "172.16.0.0/12" # Private IPv4
- "192.168.0.0/16" # Private IPv4
- "fd00::/8" # Private IPv6
- "169.254.169.254/32" # AWS metadata (explicit opt-in only)
Cloud metadata endpoints
Never add 169.254.169.254/32 to allowed_cidrs unless you explicitly need cloud metadata access and understand the SSRF implications.
FilesystemPolicyEngine¶
Controls read and write access to the local filesystem. By default, no paths are accessible.
Symlink Resolution¶
Symlink traversal prevention
All paths are resolved via Path.resolve() before comparison. A symlink inside an allowed directory that points outside it will be denied. This prevents an attacker from creating ~/workspace/escape -> /etc/shadow and reading protected files through an allowed path.
Configuration¶
filesystem:
allowed_read_paths:
- "/home/user/workspace"
- "/home/user/documents"
allowed_write_paths:
- "/home/user/workspace/output"
Path Matching¶
A path is allowed if it is equal to or nested inside any entry in the relevant list. The comparison uses resolved absolute paths:
| Request | allowed_write_paths: ["/home/user/workspace"] | Result |
|---|---|---|
/home/user/workspace/file.txt | Nested inside allowed path | Allow |
/home/user/workspace | Exact match | Allow |
/home/user/other/file.txt | Not under any allowed path | Deny |
/home/user/workspace/../../etc/passwd | Resolves to /etc/passwd | Deny |
Separate Read/Write Controls¶
Read and write permissions are independent. Granting write access to a directory does not imply read access, and vice versa. Configure both explicitly:
filesystem:
allowed_read_paths:
- "/home/user/workspace" # Can read everything
allowed_write_paths:
- "/home/user/workspace/output" # Can only write to output/
ShellPolicyEngine¶
Controls shell command execution. The shell is disabled by default -- the enabled flag must be set to true before any command is evaluated against the whitelist.
Evaluation Order¶
- Global disable check -- If
enabled: false, deny all commands immediately. - Command parsing -- Extract all program names from potentially compound commands.
- Subshell rejection -- Commands containing
$(...), backticks,<(...), or brace groups are rejected outright. - Whitelist check -- Every program in the command must match an
allowed_commandsentry.
Configuration¶
Empty whitelist = unrestricted
When enabled: true and allowed_commands is an empty list, all commands are permitted. Always specify an explicit whitelist.
Compound Command Handling¶
The engine parses compound commands and validates every program in the chain:
# Both 'git' and 'grep' must be in allowed_commands
git log --oneline | grep "fix" # ✓ if both allowed
# Subshell commands are always rejected
echo $(cat /etc/passwd) # ✗ rejected (subshell marker)
# Brace groups are always rejected
{ rm -rf /; } # ✗ rejected (brace group)
Chain operators that trigger compound parsing: &&, ||, ;, |, &, newline.
Launcher Command Warnings¶
The engine warns when whitelisted commands can execute arbitrary subcommands:
Dangerous whitelisted commands
These commands can execute arbitrary child processes, effectively bypassing the whitelist: env, xargs, find, sudo, bash, sh, python, python3, perl, ruby, node, eval, exec, nice, nohup, strace, time, watch.
If you whitelist bash, an attacker can run bash -c "rm -rf /". Consider whether you truly need these commands.
Basename Matching¶
Commands are matched by basename, so both bare names and fully-qualified paths work:
allowed_commands: ["git"] | Command | Result |
|---|---|---|
git status | Allow | |
/usr/bin/git status | Allow (basename is git) | |
gitk | Deny (basename gitk != git) |
Audit Events¶
Every policy check emits an AuditEvent with these fields:
| Field | Description |
|---|---|
event_type | network_check, filesystem_read, filesystem_write, shell_check |
category | network, filesystem, shell |
result | allow or deny |
policy_rule | The matching rule (e.g., domain:*.github.com) or null for denials |
detail | Context-specific data (host, path, command) |
session_id | Session that triggered the check |
task_id | Task that triggered the check |
Query audit events with the CLI: