SSRF Guard
The SSRF guard prevents server-side request forgery by validating every outbound URL before any HTTP request is made. ThevalidateUrl() function performs DNS-pinned validation: it resolves the hostname, then checks the resolved IP against blocked ranges.
Source:
packages/core/src/security/ssrf-guard.ts — requirement: every web-facing tool must pass through validateUrl() before fetch.Validation Steps
validateUrl() performs 5 checks in order:
| Step | Check | Behavior on Failure |
|---|---|---|
| 1 | URL parsing | Returns err("Invalid URL: ...") |
| 2 | Protocol check | Returns err("Blocked protocol: ...") |
| 3 | DNS resolution | Returns err() with DNS error |
| 4 | Cloud metadata IP blocklist | Returns err("Blocked: resolved IP ... is a cloud metadata service address") |
| 5 | IP range classification | Returns err("Blocked: resolved IP ... is in {range} range") |
Result<ValidatedUrl, Error> where ValidatedUrl contains hostname, ip (resolved address), and url (parsed URL object).
Allowed Protocols
Only two protocols are permitted:http:https:
file:, ftp:, gopher:) are rejected.
Blocked IP Ranges
Six IP range categories are blocked usingipaddr.js range classification:
| Range Name | IPv4 | IPv6 | Purpose |
|---|---|---|---|
loopback | 127.0.0.0/8 | ::1 | Localhost access |
linkLocal | 169.254.0.0/16 | fe80::/10 | Link-local addresses |
private | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | — | RFC 1918 private networks |
uniqueLocal | — | fc00::/7 | IPv6 private addresses |
unspecified | 0.0.0.0 | :: | Unspecified address |
reserved | IANA reserved ranges | IANA reserved ranges | Future-allocated blocks |
Cloud Metadata IPs
Three explicit cloud metadata service IPs are blocked regardless of their range classification:| IP | Cloud Provider |
|---|---|
169.254.169.254 | AWS, GCP, Azure instance metadata |
169.254.170.2 | AWS ECS task metadata |
100.100.100.200 | Alibaba Cloud metadata |
DNS Resolution
The SSRF guard resolves hostnames before checking IPs. This prevents DNS rebinding attacks where a hostname initially resolves to a safe IP but later resolves to a blocked one. IPv6 literal hostnames (bracketed like[::1]) have brackets stripped before DNS lookup.
ValidatedUrl Interface
On successful validation,validateUrl() returns a ValidatedUrl object:
ip for the actual HTTP request, ensuring the fetched IP matches the validated one (DNS pinning).
Usage Example
Tool Policy
Tool policies control which tools are available to an agent. Policies use named profiles as a baseline, with allow/deny overrides for fine-grained control.Source:
packages/skills/src/policy/tool-policy.ts — config-driven tool filtering with group expansion.Built-in Profiles
Built-in profiles define baseline tool sets. The five general-purpose profiles plus two narrow opt-in presets for non-interactive workloads:| Profile | Tools Included |
|---|---|
minimal | read, write |
coding | read, edit, write, grep, find, ls, apply_patch, exec, process |
messaging | message, session_status |
supervisor | agents_manage, obs_query, sessions_manage, memory_manage, channels_manage, tokens_manage, models_manage, skills_manage, mcp_manage, heartbeat_manage |
full | All tools (empty array = unrestricted) |
cron-minimal | web_search, message, read_file, write_file, list_dir, memory_store, memory_search, cron, discover |
heartbeat-minimal | message, memory_store, memory_search, discover |
*-minimal presets are opt-in via toolPolicy.profile on a CronJob or
heartbeat config respectively. They are never applied as a silent default —
operators are expected to widen them per-job via allow.
Tool Groups
Groups provide convenient bulk operations usinggroup:xxx syntax in allow/deny arrays:
| Group | Tools |
|---|---|
group:coding | read, edit, write, grep, find, ls, apply_patch, exec, process |
group:web | web_fetch, web_search, browser |
group:browser | browser |
group:memory | memory_search, memory_get, memory_store |
group:scheduling | cron |
group:messaging | message |
group:sessions | sessions_list, sessions_history, sessions_send, sessions_spawn, session_status, session_search, subagents, pipeline |
group:platform_actions | discord_action, telegram_action, slack_action, whatsapp_action |
group:supervisor | agents_manage, obs_query, sessions_manage, memory_manage, channels_manage, tokens_manage, models_manage, skills_manage, mcp_manage, heartbeat_manage |
The
group:context / group:context_expand groups bundled the DAG context
engine’s ctx_* recall tools. Those tools and groups were removed pending the
v2.12 “Lossless Context DAG” reimplementation and return with the LCD engine.Resolution Precedence
Tool policy resolution follows a strict order:- Profile baseline — start with the profile’s tool set
- Allow additions — add explicitly allowed tools and expand group references
- Deny removals — remove denied tools (deny always takes precedence)
MCP Export Policy
Tool policy (above) governs which tools an agent may call. A separate, stricter layer governs which tools an external MCP client may reach overPOST /mcp/v1: every platform tool carries an mcpExportPolicy of safe,
permission-gated, or never-export (a missing annotation is treated as
never-export — default-deny). See the
MCP server bucket reference for the full
classification.
Source:
packages/skills/src/skills/bridge/tool-metadata-registry.ts
(the mcpExportPolicy annotations) + packages/daemon/src/api/mcp-server-handlers.ts
(the registration filter + per-tool dispatch).obs_explain — read-only incident report, allowlist-gated
obs_explain is a permission-gated, isReadOnly MCP tool that surfaces
the obs.explain IncidentReport (root-cause post-mortem for a session or
trace) to an external coding agent. It is reachable only when the operator
adds "obs_explain" to that client’s gateway.tokens[].mcpClient.allowlist.
Three properties make this safe to expose with no new privilege:
- The allowlist IS the granted permission — not admin trust.
obs_explaindoes not invoke the admin-gatedobs.explainRPC. Its dispatch branch runs the report assembler directly under daemon authority, bypassing the trust-flag indirection entirely. No_trustLevel:"admin"is ever injected, and the admin RPC’s own["admin"]gate is unchanged for non-MCP callers. A client withoutobs_explainallowlisted is rejected (default-deny). - Digest-only and bounded. The report is summary-bounded (no raw tool bodies,
no raw message content cross the boundary) and flows through
wrapExternalContent— the calling agent receives it wrapped in a SECURITY NOTICE block + untrusted-content markers, like any other external text. - Read-only + rate-limited. The tool performs no mutation, and the standard per-client per-tool minute-bucket rate limit applies.
Content Scanner
The content scanner inspects skill body content at load time for dangerous patterns across six categories. It is a pure function with no side effects — callers handle audit emission and blocking decisions.Source:
packages/skills/src/prompt/content-scanner.ts — iterates CONTENT_SCAN_RULES array against content, producing ContentScanResult.Scan Categories
exec_injection (CRITICAL)
exec_injection (CRITICAL)
Targets injection syntax operators combined with dangerous binaries, not mere mentions of command names.
| Pattern ID | Description |
|---|---|
EXEC_SUBSHELL | Subshell command injection: $(command) with dangerous binary |
EXEC_BACKTICK | Backtick command injection with dangerous binary |
EXEC_EVAL | eval() with string argument |
EXEC_PIPE_BASH | Pipe to shell interpreter |
env_harvesting (WARN)
env_harvesting (WARN)
Targets mass-dump patterns, not individual
$VAR references which are common in documentation.| Pattern ID | Description |
|---|---|
ENV_PRINTENV | printenv command dumps all environment variables |
ENV_PROC_ENVIRON | Direct read of process environment via /proc |
ENV_MASS_DUMP | Environment dump piped to exfiltration or encoding |
crypto_mining (CRITICAL/WARN)
crypto_mining (CRITICAL/WARN)
Very low false-positive risk — these terms rarely appear in legitimate skill instructions.
| Pattern ID | Severity | Description |
|---|---|---|
CRYPTO_STRATUM | CRITICAL | Mining pool protocol (stratum://) |
CRYPTO_MINER_BINARY | CRITICAL | Known cryptocurrency miner binary |
CRYPTO_POOL_DOMAIN | WARN | Mining pool domain pattern |
network_exfiltration (WARN/CRITICAL)
network_exfiltration (WARN/CRITICAL)
Focuses on piped execution rather than standalone URL references.
| Pattern ID | Severity | Description |
|---|---|---|
NET_CURL_PIPE | WARN | curl output piped to interpreter |
NET_WGET_EXEC | WARN | wget output to stdout piped elsewhere |
NET_REVERSE_SHELL | CRITICAL | Reverse shell pattern |
obfuscated_encoding (WARN/CRITICAL)
obfuscated_encoding (WARN/CRITICAL)
Only flags long encoded blocks or decode-and-execute chains, not short base64 examples.
| Pattern ID | Severity | Description |
|---|---|---|
OBF_BASE64_LONG | WARN | Long base64-encoded string (80+ chars) |
OBF_HEX_LONG | WARN | Long hex-escaped string (20+ sequences) |
OBF_BASE64_DECODE_PIPE | CRITICAL | base64 decode piped to another command |
xml_breakout (CRITICAL)
xml_breakout (CRITICAL)
Detects attempts to escape skill XML structure and inject system-level instructions.
| Pattern ID | Description |
|---|---|
XML_SKILL_CLOSE | Closing tag for skill XML structure (breakout attempt) |
XML_SYSTEM_TAG | System-level message tag (breakout attempt) |
Severity Levels
- CRITICAL: Patterns that indicate active exploitation attempts (exec injection, reverse shells, crypto mining, base64 decode piping, XML breakout)
- WARN: Patterns that are suspicious but may have legitimate uses (environment harvesting, curl piping, long encoded strings, mining pool domains)
Scan Result
ThescanSkillContent() function returns a ContentScanResult:
Sanitization Pipeline
The sanitization pipeline processes skill body content through four ordered steps before it reaches the system prompt.Source:
packages/skills/src/prompt/sanitizer.ts — pure functions, no side effects. Audit metadata returned for caller to emit events.Pipeline Steps
Strip HTML Comments
Removes all
<!-- ... --> sequences using non-greedy regex to handle multiple separate comments correctly. Returns the count of comments removed for audit logging.NFKC Normalization
Applies Unicode NFKC normalization (compatibility decomposition + canonical composition). This decomposes fullwidth characters and ligatures into their standard equivalents, preventing homoglyph-based obfuscation.
Strip Invisible Characters
Removes zero-width and invisible characters that could hide malicious content:
- Zero-width joiner (U+200D)
- Zero-width non-joiner (U+200C)
- Zero-width space (U+200B)
- Soft hyphen (U+00AD)
- Left-to-right mark (U+200E)
- Right-to-left mark (U+200F)
- Unicode tag block characters (U+E0000-U+E007F)
Size Limit Enforcement
Truncates the final sanitized output at
maxBodyLength characters (default: 20,000). Size enforcement applies to the output after all other sanitization steps, preventing unnecessary truncation when HTML comments inflate raw size. Appends [TRUNCATED] marker when truncation occurs.Sanitization Result
Audit Wrapper
Tool executions generate audit events through theTypedEventBus. Each tool call emits an audit:event with the action type, classification (from the Action Classifier), outcome, and timing metadata. This provides a complete audit trail of all agent actions.
The audit event structure:
classification field comes from the Action Classifier registry. Failed executions still emit audit events with outcome: "failure" and the error message in metadata.
Action Classifier
Complete registry of action classifications
Security Model
Defense-in-depth security architecture
Sandbox
Skill sandboxing implementation
Tool Policy Guide
User-facing tool policy configuration
