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:
StepCheckBehavior on Failure
1URL parsingReturns err("Invalid URL: ...")
2Protocol checkReturns err("Blocked protocol: ...")
3DNS resolutionReturns err() with DNS error
4Cloud metadata IP blocklistReturns err("Blocked: resolved IP ... is a cloud metadata service address")
5IP range classificationReturns err("Blocked: resolved IP ... is in {range} range")
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:
Range NameIPv4IPv6Purpose
loopback127.0.0.0/8::1Localhost access
linkLocal169.254.0.0/16fe80::/10Link-local addresses
private10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16RFC 1918 private networks
uniqueLocalfc00::/7IPv6 private addresses
unspecified0.0.0.0::Unspecified address
reservedIANA reserved rangesIANA reserved rangesFuture-allocated blocks

Cloud Metadata IPs

Three explicit cloud metadata service IPs are blocked regardless of their range classification:
IPCloud Provider
169.254.169.254AWS, GCP, Azure instance metadata
169.254.170.2AWS ECS task metadata
100.100.100.200Alibaba 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:
interface ValidatedUrl {
  hostname: string;  // Original hostname from the URL
  ip: string;        // DNS-resolved IP address
  url: URL;          // Parsed URL object
}
The caller uses the resolved ip for the actual HTTP request, ensuring the fetched IP matches the validated one (DNS pinning).

Usage Example

import { validateUrl } from "@comis/core";

const result = await validateUrl("https://api.example.com/data");
if (!result.ok) {
  // URL is blocked -- do not fetch
  logger.warn({ err: result.error }, "SSRF validation failed");
  return;
}

// Safe to fetch -- use result.value.url
const response = await fetch(result.value.url.toString());

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:
ProfileTools Included
minimalread, write
codingread, edit, write, grep, find, ls, apply_patch, exec, process
messagingmessage, session_status
supervisoragents_manage, obs_query, sessions_manage, memory_manage, channels_manage, tokens_manage, models_manage, skills_manage, mcp_manage, heartbeat_manage
fullAll tools (empty array = unrestricted)
cron-minimalweb_search, message, read_file, write_file, list_dir, memory_store, memory_search, cron, discover
heartbeat-minimalmessage, memory_store, memory_search, discover
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:
GroupTools
group:codingread, edit, write, grep, find, ls, apply_patch, exec, process
group:webweb_fetch, web_search, browser
group:browserbrowser
group:memorymemory_search, memory_get, memory_store
group:schedulingcron
group:messagingmessage
group:sessionssessions_list, sessions_history, sessions_send, sessions_spawn, session_status, session_search, subagents, pipeline
group:platform_actionsdiscord_action, telegram_action, slack_action, whatsapp_action
group:supervisoragents_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:
  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.
Pattern IDDescription
EXEC_SUBSHELLSubshell command injection: $(command) with dangerous binary
EXEC_BACKTICKBacktick command injection with dangerous binary
EXEC_EVALeval() with string argument
EXEC_PIPE_BASHPipe to shell interpreter
Targets mass-dump patterns, not individual $VAR references which are common in documentation.
Pattern IDDescription
ENV_PRINTENVprintenv command dumps all environment variables
ENV_PROC_ENVIRONDirect read of process environment via /proc
ENV_MASS_DUMPEnvironment dump piped to exfiltration or encoding
Very low false-positive risk — these terms rarely appear in legitimate skill instructions.
Pattern IDSeverityDescription
CRYPTO_STRATUMCRITICALMining pool protocol (stratum://)
CRYPTO_MINER_BINARYCRITICALKnown cryptocurrency miner binary
CRYPTO_POOL_DOMAINWARNMining pool domain pattern
Focuses on piped execution rather than standalone URL references.
Pattern IDSeverityDescription
NET_CURL_PIPEWARNcurl output piped to interpreter
NET_WGET_EXECWARNwget output to stdout piped elsewhere
NET_REVERSE_SHELLCRITICALReverse shell pattern
Only flags long encoded blocks or decode-and-execute chains, not short base64 examples.
Pattern IDSeverityDescription
OBF_BASE64_LONGWARNLong base64-encoded string (80+ chars)
OBF_HEX_LONGWARNLong hex-escaped string (20+ sequences)
OBF_BASE64_DECODE_PIPECRITICALbase64 decode piped to another command
Detects attempts to escape skill XML structure and inject system-level instructions.
Pattern IDDescription
XML_SKILL_CLOSEClosing tag for skill XML structure (breakout attempt)
XML_SYSTEM_TAGSystem-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

The scanSkillContent() function returns a ContentScanResult:
interface ContentScanResult {
  clean: boolean;       // true if no patterns matched
  findings: ContentScanFinding[];
}

interface ContentScanFinding {
  ruleId: string;       // e.g., "EXEC_SUBSHELL"
  category: ScanCategory;
  severity: "CRITICAL" | "WARN";
  description: string;
  matchedText: string;  // truncated to 100 chars
  position: number;     // character offset in content
  lineNumber: number;   // 1-based line number in the original content
}
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

interface SanitizeResult {
  body: string;                  // Final sanitized content
  htmlCommentsStripped: number;  // Count for audit
  truncated: boolean;            // Whether size limit was hit
  tagBlockDetected: boolean;     // Unicode tag block bypass detected
}

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:
{
  timestamp: number;
  agentId: string;
  tenantId: string;
  actionType: string;        // e.g., "file.write", "web.fetch"
  classification: string;    // "read" | "mutate" | "destructive"
  outcome: "success" | "failure";
  metadata: {
    durationMs: number;
    toolName?: string;
    // Additional context per tool
  };
}
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