Skip to main content
This page is the exhaustive technical reference for every security mechanism in Comis, organized by processing stage. Each section documents the exact rules, thresholds, pattern weights, and default values as implemented in the source code. For a user-friendly overview of how these layers work together, see Defense in Depth.

Threat Model

Comis operates under a single root assumption: the LLM is the attack surface. Every other design decision flows from that.

Trust Boundaries

Every external input is wrapped, scanned, or filtered before it reaches the prompt. Every model output is scanned before it reaches the user. Every tool call is classified before it executes. The architecture never assumes any single check will catch everything.

STRIDE Coverage


Input Layer

The input layer validates and scans incoming messages before they reach the agent. It runs four mechanisms in sequence: structural validation, jailbreak detection, injection rate limiting, and external content wrapping.

Input Validation

Structural validation catches malformed and adversarial payloads before any semantic analysis. Pure function with no side effects. Checks performed: The validation result contains:
  • valid: true if no structural issues found
  • reasons: Array of human-readable reason codes
  • sanitized: Copy of input with null bytes removed (original preserved for audit)
Source: validateInput() in packages/core/src/security/input-validator.ts

Input Guard (Jailbreak Detection)

The Input Guard performs semantic jailbreak detection using weighted compound phrase patterns, typoglycemia detection, and code block exclusion. It scores input text on a 0.0-1.0 scale.

Pattern Categories and Weights

Each category groups related regex patterns. If any pattern in a category matches, the category weight is added once (boolean per category — multiple matches within the same category do not multiply the weight). The 13 categories in the table above are evaluated each scan. Pattern constants are imported from injection-patterns.ts.

Risk Levels and Thresholds

Typoglycemia Detection

The guard detects scrambled-middle variants of 8 key jailbreak terms. Each match adds 0.3 to the score. A word is a typoglycemia variant if it has the same length, same first and last characters, same sorted middle characters, but is not an exact match.

Code Block Exclusion

Content inside fenced code blocks (triple backticks) and inline code (single backticks) is stripped before pattern matching. This minimizes false positives on technical content that legitimately discusses prompt injection or system commands.

InputSecurityGuardConfig Interface

Source: createInputSecurityGuard() and PATTERN_WEIGHTS in packages/core/src/security/input-security-guard.ts

Injection Rate Limiter

Tracks repeated high-score injection detections per user with a sliding window approach. Each user is tracked independently by tenantId:userId key. Threshold behavior: When maxEntries is reached, the oldest entry (by most recent timestamp) is evicted. Timers use unref() for clean daemon shutdown.
Source: createInjectionRateLimiter() in packages/core/src/security/injection-rate-limiter.ts

External Content Wrapping

External content from untrusted sources (emails, webhooks, APIs, web tools) is wrapped with random delimiters and security warnings before passing to the LLM. Delimiter generation: Each session gets a deterministic random delimiter from AsyncLocalStorage context (or a fresh 24-hex-character delimiter from randomBytes(12)). The wrapping format is:
Source types: email, webhook, api, channel_metadata, web_search, web_fetch, document, unknown. Marker sanitization: If the content itself contains delimiter patterns (static <<<EXTERNAL_UNTRUSTED_CONTENT>>> or dynamic <<<UNTRUSTED_hex>>>), they are replaced with [[MARKER_SANITIZED]] before wrapping. This also handles fullwidth Unicode equivalents to prevent bypass via character substitution. Suspicious pattern detection: Content is scanned against 17 injection patterns from injection-patterns.ts. When patterns are found, an onSuspiciousContent callback fires with the source, matched patterns, content length, and sender.
Source: wrapExternalContent() in packages/core/src/security/external-content.ts

Skill Layer

The skill layer scans and sanitizes prompt skill content at load time, before it can influence agent behavior.

Content Scanner

Scans skill body content for dangerous patterns across 6 categories. Pure function with no side effects — callers handle audit events and blocking decisions. Total: 18 scan rules across 6 categories in the skill scanner. (The broader injection-pattern library aggregates 65 patterns across 8 categories — see the table below.)
Source: scanSkillContent() and CONTENT_SCAN_RULES in packages/skills/src/prompt/content-scanner.ts

Injection Pattern Library (cross-cutting)

The injection-pattern library powers multiple layers (input guard, external content wrapper, memory write validator, output guard, log sanitizer). It totals 65 distinct patterns across 8 categories:

Sanitization Pipeline

Skill body content passes through a 4-step pipeline before reaching the system prompt: Size enforcement applies to the final sanitized output, not the raw input. This prevents unnecessary truncation when HTML comments inflate the raw size.
Source: sanitizeSkillBody() in packages/skills/src/prompt/sanitizer.ts

Execution Layer

The execution layer classifies actions, enforces approval gates, prevents SSRF, and validates file paths before any side effects occur.

Action Classifier

Every action in the system is classified by risk level. Unknown actions default to "destructive" (fail-closed principle). The registry contains 178 registered actions across 21 categories. After bootstrap, the registry is locked via lockRegistry() to prevent runtime classification downgrades by malicious plugins. For the complete action registry, see Action Classifier.
Source: classifyAction() and ACTION_REGISTRY in packages/core/src/security/action-classifier.ts

Approval Gate

The approval system controls whether agent-initiated actions proceed automatically, require human confirmation, or are denied. Three modes: Configuration layers: Comis has two distinct configuration sections that interact:
  1. security.actionConfirmation — Quick toggle for destructive/sensitive action confirmation
  2. approvals — Full rule-based approval workflow with pattern matching
The approvals system evaluates rules in order (first match wins). Each rule matches action types by pattern and specifies a mode, timeout, and minimum trust level. Approval rule fields:
Source: ApprovalRuleSchema and ApprovalsConfigSchema in packages/core/src/config/schema-approvals.ts

SSRF Guard

Validates URLs before any outbound HTTP request to prevent Server-Side Request Forgery. Uses DNS-pinned validation: the URL is resolved to an IP address and checked against blocked ranges before the actual fetch. Blocked IP ranges: Cloud metadata IPs (explicitly blocked): Protocol check: Only http: and https: protocols are allowed. Validation sequence:
  1. Parse URL (reject invalid)
  2. Check protocol (reject non-HTTP/HTTPS)
  3. DNS resolution (reject unresolvable hostnames)
  4. Cloud metadata IP check (reject explicit metadata IPs)
  5. IP range classification (reject private/loopback/link-local/reserved)
Source: validateUrl(), BLOCKED_RANGES, and CLOUD_METADATA_IPS in packages/core/src/security/ssrf-guard.ts

Exec Security Validator

Pre-sandbox shell-substitution check applied to every system.exec command string. Implemented as a quote-aware state machine (ShellQuoteTracker) that tracks normal / single / double / backtick context. Returns null if safe, or an error message describing the dangerous pattern. The validator runs before the OS sandbox so it never even reaches bubblewrap/sandbox-exec. Quote-awareness avoids false positives on legitimate strings like printf '$(echo)' where the shell does not interpret the substitution.
Source: validateExecCommand() and ShellQuoteTracker in packages/skills/src/builtin/exec-security.ts

Email Sender Filter

Allowlist gating for the email channel. Independent of the allowlist, isAutomatedSender() rejects bulk and automated mail by inspecting:
  • RFC 3834 Auto-Submitted header
  • Precedence: bulk | junk | list
  • List-Unsubscribe header
  • X-Auto-Response-Suppress header
  • noreply / no-reply address patterns
Source: isAllowedSender() and isAutomatedSender() in packages/channels/src/email/sender-filter.ts

Safe Path

Validates file paths to prevent directory traversal attacks. Returns a resolved, validated absolute path that is guaranteed to stay within the base directory. Attack vectors defended: For the complete safe path API, see Safe Path.
Source: safePath() in packages/core/src/security/safe-path.ts

Credential Broker (Network Layer)

What it does. For API-key CLIs driven from the exec sandbox, the credential broker acts as an in-process HTTPS MITM proxy. The real key never enters the sandbox — the broker resolves it per-request from SecretManager and injects it at the TLS boundary. On Linux, the credentialed sandbox runs in broker-only network mode, where --unshare-net is applied and the broker unix socket is the only bind-mounted network path. This kernel-enforcement is validated on the Linux production host class: direct egress from inside the namespace fails (network unreachable), while the bound broker socket stays reachable. The broker is fail-closed: a missing binding returns 403, a missing secret returns 502, and a forged proxy token returns 407. No path forwards the request without a valid credential. All broker activity is audited via broker:* events carrying agentId and traceId. Network modes (SandboxOptions.network):
Source: packages/infra/src/credential-broker/mitm-broker.ts — fail-closed gates; packages/skills/src/tools/builtin/sandbox/bwrap-provider.tsbroker-only branch. See Credential Broker for the full deep-dive.

Authorization Layer

Authorization governs what an in-process agent may do. It is an axis orthogonal to the gateway’s network scopes, enforced by a single capability gate on every privileged action, with a stricter deny-by-origin rule guarding the control plane. See Capability Model for the full treatment.

Capability Axis

An agent holds a set of orch:* capabilities (for example orch:spawn, orch:cron, orch:browse) granted by its autonomy profile. Every privileged handler checks the one capability it requires before acting:
  • One gate, no bypass. The agent’s tool calls reach the RPC dispatcher directly, in process — they never pass through the gateway scope check. The capability gate lives at the handler boundary every call reaches, closing that in-process bypass.
  • No wildcard, disjoint from scope. The capability predicate is a plain membership test with no asterisk-implies-all rule, so no capability can ever imply admin, rpc, or “all”. The capability vocabulary (orch:*) is disjoint from the gateway scope vocabulary (rpc | admin | mcp-client) — an architecture test asserts the intersection is empty.
  • Denied content-free. A missing capability raises CapabilityDeniedError (discriminated kind: "capability_denied"), recorded as a security signal in the audit trail without bodies or secrets.

Deny-by-Origin (control plane)

Administrative handlers — secrets, tokens, config, channel management — reject any agent-origin call outright, before capabilities are consulted and independent of the call’s trust level. This is sound because inbound internal fields (the whole underscore-prefixed registry, including _agentId and _capabilities) are stripped from external WebSocket and REST callers before dispatch, while the legitimate in-process injector runs inside the daemon. The presence of _agentId is therefore an unforgeable agent-origin signal, and the control plane stays unreachable to agents by construction.
Source: packages/core/src/security/capability.ts — the closed AgentCapability union + checkCapability / requireCapability / CapabilityDeniedError; packages/core/src/api-contracts/internals.tsstripInternalFields + the INTERNAL_FIELD_NAMES registry; packages/daemon/src/wiring/setup-gateway-api.ts — the external-boundary strip that makes deny-by-origin sound.

Output Layer

The output layer scans agent responses before they reach the user, catching leaked secrets, canary tokens, and prompt extraction attempts.

Output Guard

Scans LLM responses for secret patterns, canary token leakage, and prompt extraction attempts. Critical findings are blocked and redacted; warning-level findings are reported but not redacted.

Secret Patterns

Critical patterns (blocked and redacted): Warning patterns (detected but not redacted):

Prompt Extraction Patterns

Prompt extraction patterns (warning severity, detect-only):

Canary Token Check

If a canaryToken is provided in the scan context and found in the response, it is redacted as [REDACTED:canary] with critical severity. This indicates the agent leaked its canary token, suggesting prompt extraction succeeded.

Redaction Format

Critical findings are redacted in the sanitized output using the pattern:
For example, an AWS key becomes [REDACTED:aws_key].
Source: createOutputGuard(), SECRET_PATTERNS, and PROMPT_EXTRACTION_PATTERNS_LOCAL in packages/core/src/security/output-guard.ts

Canary Tokens

Canary tokens are injected into system prompts to detect prompt extraction attacks. If the token appears in the agent’s output, it means the system prompt was leaked. Generation: HMAC-SHA256 of "canary:{sessionKey}" using a configured secret. The first 16 hex characters are used, prefixed with CTKN_.
Properties:
  • Deterministic per session: Same session key and secret always produce the same canary
  • Format: CTKN_ prefix followed by 16 hex characters
  • Detection: The output guard checks if the canary token appears anywhere in the response
Source: generateCanaryToken() and detectCanaryLeakage() in packages/core/src/security/canary-token.ts

Log Sanitizer

Defense-in-depth regex-based credential scrubbing for log strings. This is a second line of defense after Pino’s structured field redaction — it catches credentials embedded in free-text log messages. Pino’s structured-field redactor (the first line of defense) auto-redacts the canonical credential field names: apiKey, token, password, secret, authorization, botToken, privateKey, cookie, webhookSecret. The Log Sanitizer below covers credentials that escape into free-text log messages. 18 credential patterns processed in order (more specific patterns first): Size limit: Inputs exceeding 1 MB are returned as-is to prevent ReDoS on oversized strings.
Source: sanitizeLogString() and CREDENTIAL_PATTERNS in packages/core/src/security/log-sanitizer.ts

Data Layer

The data layer protects stored data through memory write validation, encrypted secrets storage, and configuration redaction.

Memory Write Validator

Pre-storage security scan for memory content. Prevents memory poisoning attacks where adversaries store prompt injection payloads for later retrieval via RAG. The validator uses the same detectSuspiciousPatterns() function from external-content.ts for pattern detection. Critical patterns are a subset: execution-oriented patterns that are dangerous when stored for later RAG retrieval.
Source: validateMemoryWrite() in packages/core/src/security/memory-write-validator.ts

Encrypted Secrets Store

Secrets are encrypted using AES-256-GCM with HKDF-SHA256 key derivation. Each encryption operation generates unique cryptographic material. Algorithm chain:
  1. Generate 32-byte random salt
  2. Derive encryption key via HKDF-SHA256 from master key + salt
  3. Generate 12-byte random IV (AES-GCM standard nonce)
  4. Encrypt with AES-256-GCM
  5. Output: ciphertext, IV, 16-byte authentication tag, salt
EncryptedSecret fields: Master key requirements: At least 32 bytes, provided as hex (64+ chars) or base64 (44+ chars). Only the first 32 bytes are used.
Source: createSecretsCrypto() and EncryptedSecret in packages/core/src/security/secret-crypto.ts

Secret Reference Resolver (SecretRefResolver)

Multi-source resolver for SecretRef values in YAML config. Three providers, all routed through SecretManager for the env case so every credential read is auditable. resolveConfigSecretRefs() walks a config recursively over a structuredClone (never mutates the input) and fails fast on the first resolution error.
Source: resolveSecretRef(), resolveConfigSecretRefs() in packages/core/src/security/secret-ref-resolver.ts

Config Redaction

Before config objects are exposed via RPC (config.read), secret-bearing fields are replaced with "[REDACTED]". Secret field pattern (case-insensitive):
The redaction function:
  1. Deep-clones the config via structuredClone (input is never mutated)
  2. Walks all nested objects recursively
  3. Replaces string values whose keys match SECRET_FIELD_PATTERN with "[REDACTED]"
Source: redactConfigSecrets() and SECRET_FIELD_PATTERN in packages/core/src/security/config-redaction.ts

Gateway / Transport Layer

The gateway layer authenticates clients before any request enters the application.

Bearer Token Authentication

Maps bearer tokens to client identities and scopes. Uses crypto.timingSafeEqual() for constant-time comparison and compares every entry in the token store even after a match, to prevent timing-based enumeration of valid tokens.
Source: createTokenStore(), checkScope(), extractBearerToken() in packages/gateway/src/auth/token-auth.ts

mTLS Verifier

Validates server certificate, key, and CA at startup; verifies clients against the configured CA at connection time. All validation happens via Node’s built-in X509Certificate class — no third-party crypto.
Source: validateCertificates(), extractClientCN() in packages/gateway/src/auth/mtls-verifier.ts

Audit

The audit subsystem produces structured security events and deduplicates rapid detections.

Audit Event Schema

Every significant action produces an audit event validated against AuditEventSchema.
Source: AuditEventSchema and createAuditEvent() in packages/core/src/security/audit.ts

Audit Aggregator

Deduplicates rapid security events within configurable time windows, emitting summary events instead of flooding the event bus. Behavior:
  • Events are bucketed by source type (user_input, tool_output, external_content, memory_write)
  • First event in a window creates a new bucket with a setTimeout timer
  • Subsequent events in the same window increment the count and add patterns
  • When the timer fires, a summary security:injection_detected event is emitted with the accumulated count and unique patterns
  • Timers use unref() for clean daemon shutdown
  • flush() emits all pending summaries immediately
  • destroy() clears all timers without emitting
Source: createAuditAggregator() in packages/core/src/security/audit-aggregator.ts

Configuration Reference

SecurityConfigSchema

Top-level security configuration.

PermissionConfig

ActionConfirmationConfig

AgentToAgentConfig

Source: SecurityConfigSchema, PermissionConfigSchema, ActionConfirmationConfigSchema, AgentToAgentConfigSchema in packages/core/src/config/schema-security.ts

ApprovalsConfigSchema

Top-level approvals configuration.
If approvals.enabled is false but approvals.rules has entries, the rules are not evaluated. Comis logs a configuration warning in this case.
Source: ApprovalsConfigSchema and checkApprovalsConfig() in packages/core/src/config/schema-approvals.ts

Secrets storage

The credential store backend is selected globally via security.storage.
Source: SecurityConfigSchema in packages/core/src/config/schema-security.ts

AgentSecretsConfigSchema

Per-agent secret access control (configured per agent, not globally).
Source: AgentSecretsConfigSchema in packages/core/src/config/schema-secrets.ts

Action Classifier

Complete action registry with all 100+ classifications

Tool Security

Tool policy profiles and content scanning

Safe Path

Path traversal prevention API

Sandbox

Execution environment isolation

Secret Manager

Encrypted secrets store operations

Node Permissions

Node.js permission model integration

Defense in Depth

User-friendly security overview

Hardening

Production hardening checklist