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:trueif no structural issues foundreasons: Array of human-readable reason codessanitized: Copy of input with null bytes removed (original preserved for audit)
Source:
validateInput() in packages/core/src/security/input-validator.tsInput 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.tsInjection Rate Limiter
Tracks repeated high-score injection detections per user with a sliding window approach. Each user is tracked independently bytenantId: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.tsExternal 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 fromAsyncLocalStorage context (or a fresh 24-hex-character delimiter from
randomBytes(12)). The wrapping format is:
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.tsSkill 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.tsInjection 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.tsExecution 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.tsApproval 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:
security.actionConfirmation— Quick toggle for destructive/sensitive action confirmationapprovals— Full rule-based approval workflow with pattern matching
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.tsSSRF 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:
- Parse URL (reject invalid)
- Check protocol (reject non-HTTP/HTTPS)
- DNS resolution (reject unresolvable hostnames)
- Cloud metadata IP check (reject explicit metadata IPs)
- IP range classification (reject private/loopback/link-local/reserved)
Source:
validateUrl(), BLOCKED_RANGES, and CLOUD_METADATA_IPS in packages/core/src/security/ssrf-guard.tsExec Security Validator
Pre-sandbox shell-substitution check applied to everysystem.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.tsEmail Sender Filter
Allowlist gating for the email channel.
Independent of the allowlist,
isAutomatedSender() rejects bulk and
automated mail by inspecting:
- RFC 3834
Auto-Submittedheader Precedence: bulk | junk | listList-UnsubscribeheaderX-Auto-Response-Suppressheadernoreply/no-replyaddress patterns
Source:
isAllowedSender() and isAutomatedSender() in packages/channels/src/email/sender-filter.tsSafe 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.tsCredential 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 inbroker-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.ts — broker-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 oforch:* 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(discriminatedkind: "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.ts — stripInternalFields + 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 acanaryToken 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 thesanitized output using the pattern:
[REDACTED:aws_key].
Source:
createOutputGuard(), SECRET_PATTERNS, and PROMPT_EXTRACTION_PATTERNS_LOCAL in packages/core/src/security/output-guard.tsCanary 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_.
- 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.tsLog 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.tsData 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.tsEncrypted Secrets Store
Secrets are encrypted using AES-256-GCM with HKDF-SHA256 key derivation. Each encryption operation generates unique cryptographic material. Algorithm chain:- Generate 32-byte random salt
- Derive encryption key via HKDF-SHA256 from master key + salt
- Generate 12-byte random IV (AES-GCM standard nonce)
- Encrypt with AES-256-GCM
- Output: ciphertext, IV, 16-byte authentication tag, salt
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.tsSecret Reference Resolver (SecretRefResolver)
Multi-source resolver forSecretRef 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.tsConfig Redaction
Before config objects are exposed via RPC (config.read), secret-bearing
fields are replaced with "[REDACTED]".
Secret field pattern (case-insensitive):
- Deep-clones the config via
structuredClone(input is never mutated) - Walks all nested objects recursively
- Replaces string values whose keys match
SECRET_FIELD_PATTERNwith"[REDACTED]"
Source:
redactConfigSecrets() and SECRET_FIELD_PATTERN in packages/core/src/security/config-redaction.tsGateway / Transport Layer
The gateway layer authenticates clients before any request enters the application.Bearer Token Authentication
Maps bearer tokens to client identities and scopes. Usescrypto.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.tsmTLS 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-inX509Certificate class — no third-party crypto.
Source:
validateCertificates(), extractClientCN() in packages/gateway/src/auth/mtls-verifier.tsAudit
The audit subsystem produces structured security events and deduplicates rapid detections.Audit Event Schema
Every significant action produces an audit event validated againstAuditEventSchema.
Source:
AuditEventSchema and createAuditEvent() in packages/core/src/security/audit.tsAudit 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
setTimeouttimer - Subsequent events in the same window increment the count and add patterns
- When the timer fires, a summary
security:injection_detectedevent is emitted with the accumulated count and unique patterns - Timers use
unref()for clean daemon shutdown flush()emits all pending summaries immediatelydestroy()clears all timers without emitting
Source:
createAuditAggregator() in packages/core/src/security/audit-aggregator.tsConfiguration Reference
SecurityConfigSchema
Top-levelsecurity configuration.
PermissionConfig
ActionConfirmationConfig
AgentToAgentConfig
Source:
SecurityConfigSchema, PermissionConfigSchema, ActionConfirmationConfigSchema, AgentToAgentConfigSchema in packages/core/src/config/schema-security.tsApprovalsConfigSchema
Top-levelapprovals configuration.
Source:
ApprovalsConfigSchema and checkApprovalsConfig() in packages/core/src/config/schema-approvals.tsSecrets storage
The credential store backend is selected globally viasecurity.storage.
Source:
SecurityConfigSchema in packages/core/src/config/schema-security.tsAgentSecretsConfigSchema
Per-agent secret access control (configured per agent, not globally).Source:
AgentSecretsConfigSchema in packages/core/src/config/schema-secrets.tsRelated
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
