Skip to content

Security Model

Eshan Roy edited this page Jun 16, 2026 · 2 revisions

Security Model

M31 Autonomous (M31A) executes shell commands on the user's behalf. Security is enforced at multiple layers: the permission system, input validation, sandboxing, and the OS keychain.

Permission System

Risk Levels

Every tool declares its risk level:

Level Description Default Action
safe Read-only operations (FileRead, Glob, Grep) Auto-allow
medium Write operations (FileWrite, Edit, WebFetch) Ask user
dangerous Shell commands (Bash) Ask user
destructive Destructive operations (FileDelete, rm -rf) Always ask

Permission Rules

Configured in [permissions.rules]:

[[permissions.rules]]
tool = "Bash"
pattern = "rm -rf"
risk_level = "destructive"
action = "deny"

[[permissions.rules]]
tool = "Bash"
pattern = "go test"
risk_level = "safe"
action = "allow"

[[permissions.rules]]
tool = "Bash"
pattern = "git *"
action = "ask"

Per-Agent Profiles

Different agent profiles can have different permission defaults:

[permissions.agents.build]
default_action = "allow"
[[permissions.agents.build.rules]]
tool = "Bash"
pattern = "go build"
action = "allow"

Permission Flow

Tool call arrives
    │
    ▼
Rule matching (tool name + pattern)
    ├── Match: allow/deny/ask
    │
    ▼
Agent default check
    ├── Has default: use it
    │
    ▼
Risk level assessment
    ├── safe/medium: allow
    ├── dangerous: ask user (interactive) or block (non-interactive)
    └── destructive: always ask user

Permission Modal

The TUI shows a modal for ask decisions:

Key Action
y Allow this execution
a Allow always (remember for session)
n Deny
e Exit application

The modal has a configurable timeout (permissions.timeout_seconds, default 300s).

Permission Channel Protocol

Request/response routing uses per-request channels with monotonic IDs:

var permissionRequestID atomic.Int64

type PermissionRequest struct {
    ID          int64
    ToolName    string
    Command     string
    RiskLevel   types.RiskLevel
    TimeoutSecs int
}

This prevents cross-caller response mix-ups when multiple permission requests are in flight (important for concurrent subagents).

API Key Security

OS Keychain

Source: pkg/keychain/

API keys are stored in the OS-native keychain, never in plaintext on disk:

Platform Backend
Linux D-Bus Secret Service API + pass CLI fallback
macOS Keychain Access (/usr/bin/security)
Windows Credential Manager

Keys are identified by m31a/<service_name> with account m31a.

Config File Keys

If API keys are in config.toml, they are read at startup and resolved through the keychain when possible. Environment variables (OPENROUTER_API_KEY, ZEN_API_KEY) are also supported as alternatives.

Input Validation

File Size Limits

Limit Value Purpose
MaxFileSize 5 MB Maximum file read size
MaxSessionFileSize 50 MB Maximum session file size (OOM protection)
MaxLLMResponseBytes 1 MB Maximum LLM response size
BashOutputLimit 50,000 chars Maximum bash output
MaxToolOutputChars 10,000 chars Maximum tool output

Tool Input Limits

Limit Value Purpose
Max parameters 1,000 per tool call Prevent abuse
Max tool calls 16 per LLM response Prevent runaway execution
Max glob results 1,000 (configurable) Prevent memory issues
Max grep results 100 (configurable) Prevent memory issues

Bash Tool Security

  • Process groups: Commands run in their own process group for clean signal forwarding
  • Grace period: SIGINT first, SIGKILL after 5-second grace period
  • Stdin closed: Child processes get EOF immediately (prevents interactive hangs)
  • Non-interactive env: CI=true, DEBIAN_FRONTEND=noninteractive, PIP_NO_INPUT=1
  • Binary detection: Null byte check in first 512 bytes
  • Output capping: limitWriter silently drops bytes beyond 50K limit

SSRF Protection

Source: internal/tools/webfetch.go

WebFetch blocks access to:

  • Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Loopback addresses (127.0.0.0/8, ::1)
  • Link-local addresses (169.254.0.0/16, fe80::/10)

Returns ErrPrivateIPBlocked when a private IP is detected.

Rate Limiting

The tool dispatcher uses a token bucket rate limiter:

  • Burst capacity: ToolRateLimitBurst tokens
  • Refill rate: ToolRateLimitPerSec tokens per second
  • Behavior: Blocks tool execution when tokens are exhausted

This prevents resource exhaustion from LLMs generating thousands of tool calls per second.

Timeout Protection

Operation Timeout Configurable
Bash command 30 minutes Per-call (max 1800s)
Permission modal 300 seconds permissions.timeout_seconds
Health check 10 seconds features.health_check_timeout_secs
Model fetch 15 seconds Fixed
Verify task 5 minutes Fixed
HTTP dial 30 seconds Fixed

Atomic File Operations

Source: internal/fileutil/atomic.go

All file writes use atomic semantics:

  1. Write to temp file
  2. fsync() to flush
  3. Rename temp to target
  4. Prevents corruption on crash

Signal Safety

SIGTERM and SIGINT are handled through Bubble Tea's Send() channel, ensuring all state mutations happen inside the single-threaded Update() loop. A 5-second hard fallback timer (os.Exit(1)) prevents the TUI from hanging on shutdown, with a sentinel file written for unclean shutdown detection.

Budget Limiting

When features.budget_limit_usd is set, the workflow engine checks cumulative cost before each phase using atomic operations on a uint64 (stored as float64 bits):

cost := math.Float64frombits(atomic.LoadUint64(&e.totalCostBits))
if cost >= cfg.Features.BudgetLimitUSD {
    return error("budget limit exceeded")
}

Telemetry

Zero telemetry. M31A does not phone home, collect usage data, or send analytics. The only network requests are to the configured LLM provider API.

Clone this wiki locally