Table of Contents
- Security Architecture
- Overview
- 1. Identity Governance and Zero-Trust Communication
- 2. Least Privilege — Capability-Scoped Access Control
- 3. Static Application Security Testing (SAST) at Role Load Time
- 4. Input Sanitization with Recursive Depth and Volumetric Control
- 5. Rate Limiting — Sliding Window Circuit Breaker
- 6. Human-in-the-Loop (HITL) as a Structural Constraint
- 7. Audit Logging and Full Traceability
- 8. Process Isolation and Privilege Separation (systemd Hardening)
- 9. Hot-Reload with Module Cache Invalidation
- 10. Bounded Event Queue — Denial of Service Resilience
- Control Plane Security — Unix Domain Socket
- Summary — Framework Control Mapping
Security Architecture
Framework Alignment: NIST AI RMF · ISO/IEC 42001 · NIST SP 800-53 · OWASP LLM Top 10
Overview
VOX is a multi-agent orchestration framework designed for real-world deployment in environments where AI systems assist in consequential decision-making. Its security architecture is not a post-hoc addition — it is load-bearing. Every design choice described in this document exists because a less secure alternative was explicitly considered and rejected.
This document maps VOX's security controls to established frameworks. For each control, it shows the implementation, contrasts it with a less secure alternative, and identifies the framework reference that motivates it.
1. Identity Governance and Zero-Trust Communication
Framework refs: ISO 42001 §6.1.2 (risk identification), NIST AI RMF — GOVERN 1.2, NIST SP 800-53 IA-3 (device identification and authentication)
What VOX Does
Every agent in the fleet has a UUIDv4 identity declared in agent.yml. Inter-agent communication is validated against this identity at the point of receipt — not assumed to be trustworthy because it arrived from inside the process.
# vox/agents/base.py — resume() only accepts the declared master
def resume(self, master_id: str):
if master_id != self.master_id:
self.log_agent_fail(
f"Resume REJECTED: sender '{master_id}' is not this agent's master."
)
return
# vox/capabilities/comm/gateway/server.py — webhook verified before parsing
if not adapter.verify_request(request, body):
return web.json_response({"error": "forbidden"}, status=403)
Inbound delivery is wrapped so that a SecurityError (guardrail) is trapped
and audited rather than propagating. Per-agent identity filtering happens at
the role level; the gateway guarantees the payload is authentic, signed, and
from the configured channel before any parsing occurs.
What a Less Secure Design Looks Like
# ❌ Junior approach — trust any message that arrives on the channel
async def _process_update(self, raw_data):
message = raw_data.get("message", {})
text = message.get("text")
if text:
await self.agent.emit("inbound_message", content=text)
Why This Matters
In a multi-agent system where agents can escalate tasks, approve actions, or trigger downstream workflows, an unauthorized sender is not just a nuisance — it is a potential command injection vector. Zero-trust at the communication boundary is the correct architectural response.
2. Least Privilege — Capability-Scoped Access Control
Framework refs: NIST SP 800-53 AC-6 (least privilege), OWASP LLM Top 10 — LLM08 (Excessive Agency), ISO 42001 §8.4 (AI system controls)
What VOX Does
Roles (the behavioral units containing business logic) cannot access the filesystem, spawn processes, or make network calls directly. All external I/O must go through a capability. Capabilities declare exactly which configuration keys they need and are provisioned only the values they requested — nothing more.
# vox/agents/base.py — capability provisioning firewall
PROTECTED = ["AGENT_ID", "MASTER_ID", "TELEGRAM_BOT_TOKEN"]
for cap_id in self.capabilities:
cap_class = orchestrator.get_capability_instance(cap_id)
requested = set(cap_class.get_params())
if requested & PROTECTED:
self.log_agent_fail(
f"Capability '{cap_id}' BLOCKED: Unauthorized data access."
)
continue
bound = cap_class.mount(self, self.config) # extracts only declared params
self.capabilities[cap_id] = bound
The Telegram capability needs a token. It gets a token. It does not get the agent's master ID, the Ollama URL, or any other secret. The firewall is enforced at provisioning time, not at call time — the capability never even sees the keys it did not request.
What a Less Secure Design Looks Like
# ❌ Junior approach — pass the full config dict to every capability
class TelegramCapability:
def __init__(self, agent):
self.config = agent.config
self.token = self.config["TELEGRAM_TOKEN"]
Why This Matters
OWASP LLM08 (Excessive Agency) specifically calls out AI systems that operate with broader permissions than their task requires. If a role can open files, it can exfiltrate data. If a capability can read the full config, a supply-chain compromise in one dependency leaks all secrets.
3. Static Application Security Testing (SAST) at Role Load Time
Framework refs: NIST AI RMF — MAP 2.3 (AI system trustworthiness), NIST SP 800-53 SA-11 (developer testing), ISO 42001 §8.5 (verification and validation)
What VOX Does
Before any role is imported and instantiated, VOX parses its source code into an Abstract Syntax Tree (AST) and scans for forbidden primitives. A role that attempts direct filesystem access, process spawning, or dynamic code execution is rejected before it runs — not after.
def _perform_security_audit(self, source: str, role_name: str) -> bool:
try:
tree = ast.parse(source)
forbidden = {"open", "write", "os.system", "subprocess", "eval", "exec"}
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in forbidden:
return False
if isinstance(node.func, ast.Attribute) and node.func.attr in forbidden:
return False
return True
except Exception as e:
self.log_agent_fail(f"Audit engine failure on {role_name}: {e}")
return False
This runs before spec.loader.exec_module(module) — the module is never executed if the audit fails.
What a Less Secure Design Looks Like
spec.loader.exec_module(module)
role = module.Role(agent)
# __init__ has already executed. Damage is done before any check runs.
Why This Matters
Runtime behavioral checks are necessary but not sufficient. AST analysis at load time is a pre-execution control — it belongs to the category of shift-left security. In a system that dynamically loads code from the filesystem, pre-execution SAST is the correct gate.
4. Input Sanitization with Recursive Depth and Volumetric Control
Framework refs: NIST SP 800-53 SI-10 (information input validation), OWASP LLM Top 10 — LLM01 (Prompt Injection), ISO 42001 §8.4
What VOX Does
All event payloads pass through InputSanitizer.sanitize() before reaching any role handler. The sanitizer is recursive (handles nested dicts and lists), applies pattern-based threat detection, HTML-escapes output, and enforces a hard size limit.
DANGEROUS_PATTERNS = [
r'<script',
r'javascript:',
r'eval\(',
r'exec\(',
r'\.\./\.\.\/',
r'SELECT.*FROM',
r'DROP\s+TABLE',
r'rm\s+-rf',
r'base64\s+--decode',
]
def _sanitize_string(self, text: str) -> str:
for pattern in self.DANGEROUS_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
raise SecurityError(...)
text = html.escape(text)
MAX_STR_LENGTH = 8192
return text[:MAX_STR_LENGTH] if len(text) > MAX_STR_LENGTH else text
The 8 KB limit approximates the token budget for a single LLM interaction and prevents memory exhaustion via oversized payloads — a denial-of-service vector frequently overlooked in agentic systems.
What a Less Secure Design Looks Like
async def emit(self, event_name: str, **kwargs):
for role in self.event_router.get(event_name, []):
await role.handle_event(event_name, **kwargs)
Why This Matters
OWASP LLM01 (Prompt Injection) is the top concern in agentic systems precisely because LLMs accept natural language as their command interface. Sanitization at the event boundary — before the payload reaches any role, including the LLM role — is the architectural defense.
5. Rate Limiting — Sliding Window Circuit Breaker
Framework refs: NIST SP 800-53 SC-5 (denial of service protection), NIST AI RMF — MANAGE 2.4
What VOX Does
Every emit() call passes through a per-agent sliding window rate limiter before any role executes. The limiter uses a deque for O(1) window maintenance.
def allow(self) -> bool:
now = time.time()
while self.calls and self.calls[0] < now - self.window:
self.calls.popleft()
if len(self.calls) >= self.max_calls:
return False
self.calls.append(now)
return True
Rate limiting before sanitization is intentional: if a caller is flooding the agent, rejecting at the first gate avoids the CPU cost of pattern matching against every payload.
Why This Matters
In an event-driven agentic system, a rate-limit failure is not just a performance issue — it is a reliability and safety issue. An unbound event storm can starve the HITL confirmation path, causing autonomous actions that were supposed to wait for human approval to time out.
6. Human-in-the-Loop (HITL) as a Structural Constraint
Framework refs: NIST AI RMF — GOVERN 6.1 (human oversight), ISO 42001 §6.1.4 (human review), EU AI Act Art. 14 (human oversight for high-risk AI)
What VOX Does
The HITL model in VOX is not a feature that can be bypassed by a misconfigured role — it is enforced by the event contract. Roles do not execute actions; they emit events. Actions are confirmed by a human before execution.
async def report_to_master(self, event_name: str, **kwargs):
if not self.master_id or not self.orchestrator:
self.log_agent_warn("Escalation failed: No Master ID or Orchestrator link.")
return
master_agent = self.orchestrator.active_agents.get(self.master_id)
if master_agent:
await master_agent.emit(event_name, sender_id=self.id, **kwargs)
The messaging contract (see Messaging-Contract) enforces that subordinate agents detect and report — they never notify the user directly, except in documented system_failure emergencies.
What a Less Secure Design Looks Like
async def handle_message(self, content, **kwargs):
response = await self.agent.capabilities["ai.llm"].generate(system, content)
if "send email" in response.lower():
await self.agent.capabilities["comm.email"].send_email(...)
Why This Matters
The EU AI Act, NIST AI RMF, and ISO 42001 all identify human oversight as a non-negotiable control for AI systems operating in consequential contexts. In VOX, HITL is not a checkbox — it is a structural property of the architecture.
7. Audit Logging and Full Traceability
Framework refs: NIST SP 800-53 AU-2 (event logging), ISO 42001 §9.1 (monitoring and measurement), NIST AI RMF — MANAGE 4.1
What VOX Does
Every security-relevant event in the fleet — capability provisioning, role audits, rate limit breaches, unauthorized access attempts, state transitions, inter-agent escalations — is logged with millisecond-precision timestamps and structured sender context.
# vox/observability/models.py — VOXForensicLogger
logger.ok("Capability provisioned", source=VOXLogSource("agent", name, agent_id))
logger.info("Inbound message verified", source=VOXLogSource("agent", name, agent_id))
logger.warning("Rate limit breach", source=VOXLogSource("agent", name, agent_id))
VOXLogSource carries structured sender context (source type, name, short
UUID); both the colored console path and the flat forensic file path are
rendered from the same logger — the single output path, no ad-hoc print()
anywhere in the codebase.
Why This Matters
ISO 42001 §9.1 requires that organizations monitor their AI systems and retain evidence of operation. NIST SP 800-53 AU-2 requires that systems log sufficient detail to support after-the-fact investigation of security incidents.
8. Process Isolation and Privilege Separation (systemd Hardening)
Framework refs: NIST SP 800-53 SC-39 (process isolation), NIST SP 800-53 AC-6 (least privilege), ISO 42001 §8.3 (AI system security)
What VOX Does
The vox.service unit file applies layered systemd security directives that reduce the attack surface to its operational minimum.
User=vox
Group=vox
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/vox /run/vox
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
MemoryMax=2G
MemorySwapMax=0
The control socket is created with mode 0o600 (owner read/write only).
Why This Matters
Defense in depth requires each layer of the stack to enforce its own access controls. Running as a non-root user with NoNewPrivileges=true and ProtectSystem=strict means that even if an attacker achieves code execution inside the VOX process, their blast radius is bounded by what the vox service user can do on the filesystem.
9. Hot-Reload with Module Cache Invalidation
Framework refs: NIST AI RMF — MANAGE 3.1 (AI system updates), ISO 42001 §10.1 (continual improvement)
What VOX Does
When an agent is restarted, VOX purges the Python module cache for that agent's roles before reloading them from disk. This guarantees that a restart picks up the current code — not a stale cached version.
spec_name = f"vox_runtime.agents.{self.name.lower()}.roles.{role_name}"
if spec_name in sys.modules:
del sys.modules[spec_name]
spec = importlib.util.spec_from_file_location(spec_name, role_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
The FileWatcher uses content hashing (SHA-256), not modification timestamps, to detect changes.
Why This Matters
In a live agentic system, a restart is often triggered because the operator patched a security vulnerability in a role. If the restart loads stale code, the fix never takes effect — and the operator has no indication that it did not.
10. Bounded Event Queue — Denial of Service Resilience
Framework refs: NIST SP 800-53 SC-5 (DoS protection), NIST AI RMF — MANAGE 2.4 (resilience)
What VOX Does
When a subordinate agent is paused, incoming events are held in a bounded queue rather than discarded silently or accumulated without limit.
class EventQueue:
DEFAULT_CAPACITY = 256
def enqueue(self, event_name: str, kwargs: Dict[str, Any]) -> bool:
if len(self._queue) >= self._cap:
self._dropped += 1
return False
self._queue.append(PendingEvent(event_name, kwargs))
return True
The capacity limit (256 events) is configurable. On resume, the queue is drained in arrival order and re-emitted through the normal emit() path — including sanitization and rate limiting.
Why This Matters
Resource exhaustion is a class of vulnerability that disproportionately affects agentic systems because they are designed to run unattended for long periods. A bounded queue with explicit drop reporting degrades gracefully rather than catastrophically.
Control Plane Security — Unix Domain Socket
Framework refs: NIST SP 800-53 AC-17 (remote access), NIST SP 800-53 AC-3 (access enforcement)
What VOX Does
The runtime control plane (agent start/stop/restart, status queries) is exposed via a Unix Domain Socket with filesystem-level access control, not a network port.
self._commands: Dict[str, tuple] = {
"status": (self._handle_status, 0, "status"),
"list": (self._handle_list, 0, "list"),
"restart": (self._handle_restart, 1, "restart <agent-name>"),
"stop": (self._handle_stop, 1, "stop <agent-name>"),
"start": (self._handle_start, 1, "start <agent-name>"),
"pause": (self._handle_pause, 1, "pause <agent-name>"),
"resume": (self._handle_resume, 1, "resume <agent-name>"),
}
The socket defaults to /tmp/vox.sock (overridable via VOX_UDS_PATH) and
is created with mode 0o600. Only the owning user can reach the socket at the
OS level.
Summary — Framework Control Mapping
| VOX Control | NIST AI RMF | ISO 42001 | NIST SP 800-53 | OWASP LLM |
|---|---|---|---|---|
| UUID-based identity, whitelist comms | GOVERN 1.2 | §6.1.2 | IA-3 | — |
| Capability-scoped least privilege | — | §8.4 | AC-6 | LLM08 |
| Pre-execution SAST (AST) | MAP 2.3 | §8.5 | SA-11 | LLM02 |
| Recursive input sanitization | — | §8.4 | SI-10 | LLM01 |
| Sliding window rate limiter | MANAGE 2.4 | — | SC-5 | — |
| Structural HITL enforcement | GOVERN 6.1 | §6.1.4 | — | LLM06 |
| Structured audit logging | MANAGE 4.1 | §9.1 | AU-2 | — |
| systemd process isolation | — | §8.3 | SC-39, AC-6 | — |
| Module cache invalidation on reload | MANAGE 3.1 | §10.1 | — | — |
| Bounded event queue | MANAGE 2.4 | — | SC-5 | — |
| UDS control plane + command whitelist | — | §8.3 | AC-17, AC-3 | — |
This document reflects VOX as of the v2 architecture.
Overview
- Home
- Versioning
- Architecture Overview
- Building Blocks
Agent Model
- VOXAgent
- Agent Lifecycle
- VOXRole
- Creating an Agent
Capabilities
- VOXCapability
- Capabilities Reference
- Capability Contract
- Comm Gateway
- WhatsApp Setup
Messaging
- VOXMessage
- Messaging Contract
- Hierarchical Messaging Contract
- The War Room
Orchestrator & Control Plane
- VOXOrchestrator
- Orchestrator & Control Plane
- VOXApiServer
Persistence & Forensics
- Segmented Persistence & Forensic Traceability
Security & Governance
- Security Architecture
- Core Ontology (legacy)
- Capability Specification (legacy)
Status: v0.5.4