Skip to main content
Tool security encompasses four subsystems that protect against malicious input from external sources: SSRF prevention for outbound HTTP, tool policy enforcement for access control, content scanning for skill body inspection, and sanitization for hidden content defense.

SSRF Guard

The SSRF guard prevents server-side request forgery by validating every outbound URL before any HTTP request is made. The validateUrl() 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: The function returns Result<ValidatedUrl, Error> where ValidatedUrl contains hostname, ip (resolved address), and url (parsed URL object).

Allowed Protocols

Only two protocols are permitted:
  • http:
  • https:
All other protocols (e.g., file:, ftp:, gopher:) are rejected.

Blocked IP Ranges

Six IP range categories are blocked using ipaddr.js range classification:

Cloud Metadata IPs

Three explicit cloud metadata service IPs are blocked regardless of their range classification:

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:
The caller uses the resolved 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: The *-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 using group:xxx syntax in allow/deny arrays:
There are no group:context / group:context_expand groups: the DAG context engine’s on-demand ctx_* recall tools are not yet shipped, so no tool group bundles them.

Resolution Precedence

Tool policy resolution follows a strict order:
  1. Profile baseline — start with the profile’s tool set
  2. Allow additions — add explicitly allowed tools and expand group references
  3. Deny removals — remove denied tools (deny always takes precedence)
Per-skill tool restrictions from skill manifests provide an additional layer of filtering applied after the agent-level policy. For user-facing configuration guide, see Tool Policy.

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 over POST /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_explain does not invoke the admin-gated obs.explain RPC. 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 without obs_explain allowlisted 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.
Enumeration implication. An allowlisted client can request an IncidentReport for any sessionKey or traceId it can name — there are no per-session ACLs on top of the allowlist grant (out of scope; the report is digest-only/bounded regardless). The operator decides which clients receive obs_explain by editing mcpClient.allowlist; it must not be granted by default. Before flipping /mcp/v1 live on a publicly reachable instance, a security reviewer should confirm the grant list and the digest-only bounding meet the deployment’s threat model.

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

Targets injection syntax operators combined with dangerous binaries, not mere mentions of command names.
Targets mass-dump patterns, not individual $VAR references which are common in documentation.
Very low false-positive risk — these terms rarely appear in legitimate skill instructions.
Focuses on piped execution rather than standalone URL references.
Only flags long encoded blocks or decode-and-execute chains, not short base64 examples.
Detects attempts to escape skill XML structure and inject system-level instructions.

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

The scanSkillContent() function returns a ContentScanResult:
For user-facing guide, see Security Scanning.

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

1

Strip HTML Comments

Removes all &lt;!-- ... --&gt; sequences using non-greedy regex to handle multiple separate comments correctly. Returns the count of comments removed for audit logging.
2

NFKC Normalization

Applies Unicode NFKC normalization (compatibility decomposition + canonical composition). This decomposes fullwidth characters and ligatures into their standard equivalents, preventing homoglyph-based obfuscation.
3

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)
4

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 the TypedEventBus. 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:
Every tool execution — whether built-in, prompt skill, or MCP — generates an audit event. The 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