Security conventions
Defense-in-depth with 4 independent mechanisms, built on the 3-layer access model. Threat model, secrets lifecycle, and validation.
This security model is experimental. Running an LLM-powered agent on a development machine introduces risks that are not yet fully addressed: prompt injection, data exfiltration via API calls, supply chain attacks through generated code. The layers described here reduce the attack surface but do not eliminate it. Treat this as a foundation to harden, not a finished security posture.
Defense in depth
The agent container is treated as semi-trusted: benign but potentially buggy. Security does not rely on any single mechanism. Four independent layers ensure that a breach at one level is contained by the next.
Secrets in env/ (not mounted). Read-only mounts for engine. Isolated workspace directory.
Non-root user with UID/GID mapping. Network isolation (separate bridge). No Docker socket access.
All capabilities dropped. AppArmor mandatory access control (docker-default). no-new-privileges prevents gaining privileges through execution. Bind mounts do not enforce nosuid, nodev, or noexec here.
Process count limits (pids_limit: 256). File descriptor limits. Memory and CPU constraints.
3-layer access model
Every file, every volume mount, every access decision follows a 3-layer model. Where a file lives determines its access level.
Agent's mutable workspace: source code, documentation, delivery tickets, agent config. Everything the agent creates or modifies lives here.
Engine, Taskfiles, package sources are mounted read-only. The agent can read conventions but cannot modify the infrastructure it runs on. To propose L2 changes, the agent writes to an exchange zone (exchange.dev.tmp/proposed/ for the dev agent, exchange.lab.tmp/ for metalab). The operator reviews the diff and applies manually via cp.
Secrets (env/), Docker socket, host filesystem. Not restricted, simply absent from the container.
Resilience invariant
The dev agent and the metalab agent run on separate networks with separate runtime state. At least one of them must stay functional at all times: if one breaks, the operator uses the other to diagnose and recover. Both share the same image, so an image change is tested on one agent before the other is restarted (staged rebuild).
Container hardening
Non-root execution
The agent runs as a non-root user with UID/GID matching the host operator. This prevents privilege escalation, ensures file ownership consistency, and limits damage from any container compromise.
Capability dropping
cap_drop:
- ALL # Remove all ~15 default capabilities
security_opt:
- no-new-privileges:true # Prevent privilege escalation
- apparmor=docker-default # Kernel-level MAC Docker grants ~15 capabilities by default. The agent container drops all of them: no NET_RAW (packet sniffing), no SYS_ADMIN (mount/kernel), no MKNOD (device nodes). The agent functions correctly with zero capabilities.
Resource limits
pids_limit: 256 # Max processes (fork bomb protection)
mem_limit: 2g # Max 2 GB RAM
cpus: 2 # Max 2 CPU coresNetwork isolation
Each agent runs on its own internal network: no direct route to the internet, to the other agent, or to the app containers. The app containers use the internal gateway network of the proxy.
Only the proxy (pkg.proxy) reaches the internet, through a separate egress network. augr-host-only publishes the Phoenix and app ports to the host, without internet access. augr-host-internet is a temporary direct route that no container uses by default.
The OAuth login (task augr:pkg:agent:connect) is the exception: a short-lived container on the host network, which still reaches the provider through the proxy, on 127.0.0.1:13128. See Providers and models.
Agents reach their LLM provider only through the proxy: every request passes through one logged point, and agents have no internet when the proxy is down. The proxy does not filter destinations yet: through it, an agent can reach any host, including the app containers. Filtering outbound calls per agent is planned, so that each agent only reaches what its work requires.
This design also works behind a corporate proxy, with TLS inspection, and with a private npm registry: see Corporate proxy, Custom CA certificates, and Private npm registry.
Secrets management
Real secrets never live in the repository, and env/ is not mounted in the agents.
workspace/env.template/ : placeholders only, committed to Git. No real values.
task admin:setup copies templates to env/. Operator fills in real values.
task augr:build aggregates per-package env files into env/.env.project. Validates no empty required vars.
Docker Compose injects variables as container environment variables. No .env file is visible to the agent.
- A process can read the credentials it receives: the agent sees its own environment variables.
- OAuth credentials persist in the OpenCode data volume of each agent.
- The seed app containers can read the private npm registry token, mounted for pnpm at runtime. This exception is temporary; see Private npm registry.
Where a provider credential lives depends on the connection method; see Providers and models.
Threat model
The agent is treated as semi-trusted: it acts in good faith but may have bugs, hallucinations, or be running compromised dependencies.
env/ not mounted, only ENV injection
Socket not mounted
Non-root execution and no-new-privileges (not bind-mount nosuid)
Separate internal networks; published ports and proxy policy remain boundaries
pids_limit, mem_limit, cpus
Non-root user, no capabilities, AppArmor
env/ in .gitignore, not in /workspace
AppArmor, no SYS_ADMIN capability
Operator / agent boundaries
Agent CAN
- Read/write files in
/workspace - Read engine and Taskfiles (L2)
- Propose L2 changes via exchange zones
- Analyze code, propose changes
- Access internet for LLM APIs
- Create documentation
Agent CANNOT
- Execute
taskcommands - Run Docker/Podman commands
- Control other containers through the Docker socket
- Modify L2 files (read-only)
- Access
env/, Docker socket
Validation
The seed includes a security audit Taskfile that validates all constraints automatically:
task security-audit:validateThis checks: no secrets in workspace, agent container running as non-root, no Docker socket mounted, network isolation active, AppArmor loaded, capabilities dropped, PID/memory limits set, mount options correct.