Skip to main content

Security Architecture

Digitorn provides enterprise-grade security for AI agent applications. Every action an agent attempts passes through multiple enforcement layers before execution. Nothing executes without explicit authorization.

Design Principles

The security system follows four principles:

  1. Deny by default -- actions without explicit policy require approval
  2. Least privilege -- agents see only what they need
  3. Defense in depth -- 7 independent gates, each can block
  4. Full auditability -- every decision is logged with context

Architecture Overview

YAML Configuration

All security is declared in the capabilities: block:

capabilities:
# Default policy for actions not explicitly mentioned
default_policy: approve # auto | approve | block

# Maximum risk level the app can handle without explicit grants
max_risk_level: medium # low | medium | high

# Actions the agent can execute freely
grant:
- module: filesystem
actions: [read, ls, find, grep, edit, write]
- module: git
actions: [status, diff, log, add, commit]
- module: shell
actions: [run, bash, which]

# Actions that require user confirmation before execution
approve:
- module: filesystem
actions: [rm]
- module: git
actions: [push, reset, merge]
- module: shell
actions: [task_kill]

# Actions that are permanently blocked
deny:
- module: database
actions: [execute_query, batch_execute]
reason: "Read-only mode"

# Modules invisible to the agent (still usable by system)
hidden_modules: [llm_provider, index]

# Specific actions hidden from tool index
hidden_actions:
- module: database
actions: [set_policy]

The Seven Security Gates

Gate 0: App Active

If the application is marked inactive (via API or admin), all actions are denied. This allows instant kill-switch capability without redeployment.

if not profile.is_active:
raise PermissionDeniedError # logged as gate0_inactive

Gate 1: Module Visibility

The agent can only use modules that are visible in its security profile. Hidden modules are completely invisible -- the agent cannot discover them via search_tools, list_categories, or browse_category.

# These modules exist but the agent cannot see them
hidden_modules: [llm_provider, index]

Gate 2: Risk Level Cap

Every action has a risk level declared in its @action decorator:

RiskExamplesDescription
lowread, ls, grep, status, searchRead-only, no side effects
mediumwrite, edit, commit, postModifies state but recoverable
highrm, push, reset, execute_queryDestructive or affects shared state

The max_risk_level in capabilities sets the ceiling. Actions above this level are denied unless they have an explicit grant or approve override.

max_risk_level: medium

grant:
- module: shell
actions: [run] # run is high-risk, but this explicit grant overrides the cap

Gate 3: Symbolic Permissions

Actions can declare required permissions in their @action decorator:

@action(
description="Edit a file",
permissions=["fs.read", "fs.write"], # required
risk_level="medium",
)

The security gate checks these against granted_permissions. However, explicit grants override symbolic permissions. If filesystem:edit is in the grant list, the symbolic fs.read and fs.write checks are skipped.

This prevents the common scenario where a developer explicitly grants an action but it fails because of a symbolic permission mismatch.

Gate 4: Action Policy Resolution

The final policy for each action is resolved through a priority chain:

Priority order (first match wins):

  1. Explicit action override -- grant:, approve:, or deny: in YAML
  2. Risk-based rule -- per-risk-level policy mapping
  3. Module default -- default_action_policy on the module grant
  4. App default -- default_policy in capabilities

The golden rule: deny always wins. Even if an action is in both grant and deny, the deny takes effect.

Gate 5: Data Classification

Actions can declare a data classification level:

@action(
description="Read sensitive config",
data_classification="confidential",
)

Classification levels, from least to most sensitive:

LevelDescriptionExamples
publicNo sensitivityhelp text, tool lists
internalInternal usesource code, configs
confidentialBusiness sensitivecustomer data, credentials
restrictedHighest sensitivityencryption keys, PII

Configure the maximum allowed level:

capabilities:
max_data_classification: internal # blocks confidential and restricted

Gate 6: Per-Action Rate Limiting

Prevent abuse by limiting how often specific actions can be called:

capabilities:
rate_limits:
"shell.run": 30 # max 30 calls per minute
"filesystem.write": 60 # max 60 writes per minute
"*": 120 # default for all actions

The rate limiter uses a sliding window (60-second window). When the limit is reached, the action is denied with a message telling the agent how long to wait before retrying.

Approval Workflow

When an action has the approve policy, execution is paused and the user is prompted for confirmation.

Approval in CLI Mode

In standalone mode (digitorn run --standalone), the CLI prompts the user directly in the terminal with a Rich prompt.

Approval in Daemon Mode

In daemon mode, the approval request is emitted as an SSE event:

{
"event": "approval_request",
"data": {
"request_id": "apr_abc123",
"tool_name": "filesystem.rm",
"tool_params": {"path": "/tmp/data"},
"risk_level": "high",
"description": "Delete a file or directory permanently."
}
}

The client (web app, CLI, extension) resolves it via the API:

POST /api/apps/{app_id}/approve
{
"request_id": "apr_abc123",
"approved": true
}

Approval Timeout

Approvals timeout after 5 minutes by default. If the user does not respond, the action is automatically denied.

Temporal Scopes

Grants can be time-limited or session-scoped:

capabilities:
temporal_grants:
- module: shell
action: run
scope: session # valid only for current session
- module: git
action: push
scope: timed
duration: 3600 # valid for 1 hour after session start
ScopeBehavior
sessionGrant is valid only for the current session. New session = new approval needed.
timedGrant expires after the specified duration (seconds).

Temporal grants are managed by the TemporalGrantStore and cleaned up automatically when they expire.

Security Audit Log

Every security decision is logged to a persistent audit trail. The audit log records what was attempted, what decision was made, which gate made the decision, and why.

Audit Event Structure

{
"timestamp": 1710547200.0,
"app_id": "claude-code",
"agent_id": "main",
"session_id": "sess_abc123",
"module_id": "filesystem",
"action": "rm",
"risk_level": "high",
"params_summary": {"path": "/tmp/data"},
"decision": "approval_required",
"gate": "gate4_policy",
"reason": "Action requires user approval",
"policy_resolved": "approve",
"approved_by": "",
"approval_duration_ms": 0
}

Decision Types

DecisionMeaning
allowedAction passed all gates and executed
deniedAction was blocked by a security gate
approval_requiredAction is waiting for user confirmation
approvedUser approved the action, it executed
denied_by_userUser denied the approval request

Querying the Audit Log

Via the API:

# Get recent security events
GET /api/apps/{app_id}/audit?limit=50

# Filter by decision
GET /api/apps/{app_id}/audit?decision=denied&limit=20

# Filter by module
GET /api/apps/{app_id}/audit?module_id=shell

# Get statistics
GET /api/apps/{app_id}/audit/stats

Parameter Sanitization

Audit log entries never contain sensitive data. Parameters are automatically sanitized before logging:

  • Keys containing password, secret, token, api_key, credential, auth, private_key, access_key are replaced with ***REDACTED***
  • Strings longer than 200 characters are truncated
  • Large collections are summarized as <list len=100>
  • Internal keys (starting with _) are excluded

Complete YAML Reference

capabilities:
# Default policy for unlisted actions
default_policy: approve # auto | approve | block

# Risk ceiling (actions above this need explicit grant/approve)
max_risk_level: medium # low | medium | high

# Data sensitivity ceiling
max_data_classification: internal # public | internal | confidential | restricted

# Per-action rate limits (calls per minute)
rate_limits:
"shell.run": 30
"filesystem.write": 60
"*": 120 # default for all

# Free-pass actions (execute without confirmation)
grant:
- module: filesystem
actions: [read, ls, find, grep, edit, write, insert, mkdir]
- module: git
actions: [status, diff, log, blame, show, add, commit]
- module: shell
actions: [run, bash, which, env]

# Actions requiring user confirmation
approve:
- module: filesystem
actions: [rm]
- module: git
actions: [push, reset, merge]
- module: shell
actions: [task_kill]

# Permanently blocked actions
deny:
- module: database
actions: [execute_query, batch_execute, set_policy]
reason: "Read-only access only"

# Invisible modules (system use only)
hidden_modules: [llm_provider, index]

# Invisible actions (still executable by system)
hidden_actions:
- module: database
actions: [set_policy]

# Time-limited grants
temporal_grants:
- module: git
action: push
scope: timed
duration: 3600

Module-Level Security

Beyond the security gate, individual modules enforce their own security controls. All are configurable via YAML and enabled by default.

Filesystem: Path Sandboxing

The filesystem module restricts all operations to declared paths. Every action (read, write, edit, grep, find, ls, rm, undo, diff_checkpoint) checks against the constraint before execution.

modules:
filesystem:
constraints:
paths:
- "{{workspace}}"
- "/tmp/digitorn"
max_file_size: "50MB"
allowed_extensions: [".py", ".js", ".ts", ".yaml", ".md", ".json"]

Any access outside these paths returns a permission error. The paths constraint is resolved at compile time, not at runtime, preventing the agent from modifying it.

Shell: Output Sanitization

The shell module automatically redacts values of sensitive environment variables from command output. This prevents secret exfiltration through commands like env, printenv, or echo $API_KEY.

modules:
shell:
config:
security:
sanitize_output: true # default: true
sensitive_patterns: # extra patterns (in addition to built-in)
- "database_url"
- "internal_token"

Built-in patterns: key, secret, password, token, auth, credential, private, cert, jwt, signing, encryption, ssh, pgp, gpg.

Any environment variable whose name matches one of these patterns has its value replaced with ***REDACTED*** in stdout and stderr.

Web: Egress Filtering

The web module supports domain allowlists and blocklists. This prevents the agent from fetching content from internal services, cloud metadata endpoints, or attacker-controlled domains.

modules:
web:
config:
egress:
allowed_domains: # null = all allowed (default)
- "docs.python.org"
- "github.com"
- "stackoverflow.com"
blocked_domains: # always blocked, even if allowed_domains is null
- "localhost"
- "127.0.0.1"
- "169.254.169.254" # AWS/GCP metadata endpoint
- "metadata.google.internal"

When allowed_domains is set, only those domains can be fetched. blocked_domains applies regardless of the allowlist.

Web: Prompt Injection Detection

Fetched web content is scanned for common prompt injection patterns. When detected, the result includes a security_warning field and a log entry is emitted. The agent still receives the content, but the warning signals that the data may be adversarial.

modules:
web:
config:
security:
detect_injection: true # default: true
injection_patterns: # extra patterns (in addition to built-in)
- "you are a helpful assistant"

Built-in detection patterns include: ignore previous instructions, disregard your instructions, you are now, system prompt:, forget everything, and common LLM prompt delimiters.

HTTP: Egress Protection

The HTTP module blocks outbound write requests (POST, PUT, PATCH, DELETE) to external hosts by default. Only GET, HEAD, and OPTIONS are allowed to external hosts without explicit authorization. This prevents data exfiltration via HTTP.

To allow write requests to specific hosts, declare them in the YAML:

modules:
http:
constraints:
allowed_hosts:
- "api.github.com"
- "httpbin.org"
- "hooks.slack.com"

When allowed_hosts is set, all HTTP methods are allowed to those hosts. Requests to localhost and 127.0.0.1 are always allowed regardless of the list.

Database: Host Restriction

Database connections to remote hosts are blocked by default. Only localhost, 127.0.0.1, and ::1 are allowed. SQLite connections to :memory: and local files are always allowed.

To connect to a remote database server, declare the host explicitly:

modules:
database:
constraints:
allowed_hosts:
- "db.company.com"
- "analytics.internal"

MCP: Server Trust Model

MCP servers are external processes with their own system access. Digitorn applies several layers of protection, but cannot fully sandbox the server process itself.

What Digitorn controls:

  • Tool risk classification (auto-inferred from tool name)
  • Result normalization (structured output, no raw system data)
  • Approval workflows (high-risk MCP tools require human approval)
  • Audit logging (all MCP tool calls are logged)
  • Credential isolation (each server sees only its own env vars)

What Digitorn cannot control:

  • What the server process does internally (file access, network calls)
  • Whether the server has vulnerabilities or backdoors

Recommendations:

  1. Only install MCP servers from trusted sources
  2. Review the server source code before installing
  3. Use capabilities.approve for all MCP tool calls in production
  4. Run the daemon in a container for full isolation

Memory: Secret Redaction

The memory module redacts sensitive environment variable values before storing facts. This prevents secrets from persisting in the memory store and surviving across sessions.

modules:
memory:
config:
security:
redact_secrets: true # default: true
sensitive_patterns: # extra patterns (in addition to built-in)
- "internal_key"

When an agent calls add_fact(content="Found API key: sk-abc123..."), the value is replaced with [REDACTED] before storage.

Defense in Depth Summary

Every action passes through the security gate first, then through the module's own controls. Both layers are independently configurable. A failure at either level blocks execution.

Security Model Comparison

How Digitorn security compares to other agent frameworks:

FeatureDigitornClaude CodeLangChainCrewAI
Per-action policygrant/approve/blockallow/denyNoneNone
Risk-level enforcement3 levels with capBasicNoneNone
Approval workflowAsync queue + SSEInteractive promptNoneNone
Audit logPersistent, queryableNoneNoneNone
Data classification4 levelsNoneNoneNone
Rate limiting per actionSliding windowNoneNoneNone
Temporal scopesSession + timedNoneNoneNone
Module visibilityHidden + visibleN/AN/AN/A
Output sanitizationAuto-redact secrets in stdoutNoneNoneNone
Path sandboxingAllowlist per moduleWorkspace onlyNoneNone
Egress filteringDomain allow/blocklistNoneNoneNone
Prompt injection detectionPattern scan on fetchBuilt-inNoneNone
Memory secret redactionAuto-redact before storageN/ANoneNone

Implementation Details

File Structure

packages/digitorn/core/
security.py # SecurityProfile, ModuleGrant, security_gate (7 gates)
security_audit.py # SecurityAuditLog, SecurityEvent, param sanitization
security_enforcer.py # ActionRateLimiter, DataClassification, TemporalGrantStore
runtime/
approval.py # ApprovalQueue, ApprovalRequest (async Future-based)

Test Coverage

The security system has 60 dedicated tests across 3 test files:

Test fileTestsCoverage
test_security_gate.py28All 7 gates, policy resolution, Claude Code pattern
test_security_advanced.py32Audit log, rate limiting, classification, temporal scopes
Total60All security features

Complete Security Reference

Every security feature is configurable in the application YAML.

Full Example

modules:
filesystem:
config:
checkpoint: true
max_checkpoints: 20
constraints:
paths: ["{{workspace}}"]
max_file_size: "50MB"
allowed_extensions: [".py", ".js", ".ts", ".yaml", ".md", ".json", ".txt"]

shell:
config:
security:
sanitize_output: true
sensitive_patterns:
- "COMPANY_INTERNAL"
- "PROD_"
constraints:
allowed_actions: [run, bash, which, env, background_run, task_status, task_output, task_list, task_wait]

web:
config:
egress:
allowed_domains:
- "docs.python.org"
- "github.com"
- "stackoverflow.com"
blocked_domains:
- "169.254.169.254"
- "metadata.google.internal"
security:
detect_injection: true
injection_patterns:
- "you are a helpful assistant"

http:
constraints:
allowed_hosts:
- "api.github.com"
- "httpbin.org"
- "hooks.slack.com"

database:
constraints:
allowed_hosts:
- "db.company.com"
- "analytics.internal"

memory:
config:
security:
redact_secrets: true
sensitive_patterns:
- "COMPANY_"
- "INTERNAL_"

mcp:
servers:
github:
env:
GITHUB_TOKEN: "{{secret.GITHUB_TOKEN}}"
config:
cache:
scope: auto
ttl: 300

capabilities:
default_policy: auto
max_risk_level: medium
grant:
- module: filesystem
actions: [read, ls, find, grep, edit, write, insert, mkdir]
- module: git
actions: [status, diff, log, blame, show, add, commit]
- module: shell
actions: [run, which, env]
approve:
- module: git
actions: [push, reset, merge]
- module: filesystem
actions: [rm, mv, cp]
- module: shell
actions: [bash]
deny:
- module: database
actions: [execute_query, batch_execute]
reason: "Read-only database access"

Configuration Summary

FeatureLocation in YAMLDefaultEffect
Path confinementmodules.filesystem.constraints.pathsNone (open)Agent can only access listed paths
File size limitmodules.filesystem.constraints.max_file_sizeNone (open)Blocks read/write of files above limit
Shell actionsmodules.shell.constraints.allowed_actionsAllOnly listed actions are available
Shell output sanitizationmodules.shell.config.security.sanitize_outputtrueRedacts secrets from command output
Shell sensitive patternsmodules.shell.config.security.sensitive_patternsBuilt-inAdditional env var name patterns to redact
Web allowed domainsmodules.web.config.egress.allowed_domainsNone (open)Only listed domains can be fetched
Web blocked domainsmodules.web.config.egress.blocked_domainsNoneAlways blocked, even if in allowed list
Web injection detectionmodules.web.config.security.detect_injectiontrueWarns when fetched content has injection patterns
HTTP allowed hostsmodules.http.constraints.allowed_hostsNone (localhost only for writes)POST/PUT/DELETE allowed to listed hosts
DB allowed hostsmodules.database.constraints.allowed_hostsNone (localhost only)Remote DB connections allowed to listed hosts
Memory secret redactionmodules.memory.config.security.redact_secretstrueRedacts env var values before storing as facts
Memory sensitive patternsmodules.memory.config.security.sensitive_patternsBuilt-inAdditional patterns to redact in memory
MCP env filteringAutomaticAlways onMCP servers only see safe env vars + declared vars
Grant actionscapabilities.grantNoneAllowed without approval
Approve actionscapabilities.approveNoneRequires human approval
Deny actionscapabilities.denyNoneAlways blocked
Max risk levelcapabilities.max_risk_levelmediumActions above this risk require approval
Default policycapabilities.default_policyautoPolicy for actions not in grant/approve/deny