Page
Security
Choir’s security model follows the principle: even a malicious, root-level container cannot cause host-level side effects without explicit human approval.
Container Security Profile
Applied Constraints
| Constraint | Setting |
|---|---|
| Capabilities | --cap-drop=ALL |
| Privilege escalation | --security-opt no-new-privileges |
| Root filesystem | --read-only |
| Temporary storage | --tmpfs /tmp, --tmpfs /run |
| Resource limits | cgroup CPU, memory, and pids limits |
| Seccomp | Default Docker seccomp profile |
Root and shell access inside the container are allowed by design. This is a single-tenant disposable cognitive appliance – the container boundary is the sandbox, not internal access controls.
Prohibited Configurations
These are hard prohibitions that must never be applied to agent containers:
| Configuration | Reason |
|---|---|
--privileged |
Grants full host capabilities |
| Docker socket mount | Would allow container to control other containers |
--pid=host |
Exposes host process namespace |
--network=host |
Bypasses network isolation |
| Device passthrough | Direct hardware access |
Host mounts (except /workspace and UDS socket) |
Prevents arbitrary host filesystem access |
Trust Boundaries
1. Host <-> Container (Strong)
The container runs with namespaced root, no --privileged, no Docker socket, no host PID namespace. Only two host paths are bind-mounted:
/workspace– writable, the agent’s working directory~/.choir.d/socks/choird.sock– the UDS control socket (when using UDS transport)
2. Choird <-> Agent RPC (Policy)
Every RPC call carries a lease-scoped authentication token:
- Generated by choird at container creation
- Passed as an environment variable at startup
- Required in every RPC call
- Scoped to a single session
- Valid while the agent is active; revoked when the session ends
- No time-based expiry – validity is tied to session liveness
Rate limits and payload size caps are enforced on all RPC verbs. No RPC verb can directly perform host-destructive operations.
3. Tool Lock System (Serialization)
Resource-level mutual exclusion ensures only one lane owns side-effect tools at a time. See Tools for lock details.
Secret Handling
Secret Handshake
- choir-agent boots and connects to choird over the configured transport.
- Sends
INIT_HELLOwith agent ID, session ID, and image version. - choird validates identity and resolves resource bindings.
- Agent sends
GET_SECRETSwith a list of named secret references (derived from"secret"fields of bound resources). - choird replies with secret values.
- Agent stores secrets in-memory only.
In-Memory Only
- Secrets exist only in process memory. Atomic swap on update; never mutated in place.
- Never persisted to
/workspace, logs, or any file. - Never passed as environment variables (root + bash means
envor/proc/<pid>/environwould reveal them). - On crash, secrets are lost. On restart, the handshake repeats.
Per-Tool Scoping
Secret access is scoped per tool via the secret_resources field in the tool’s runtime manifest:
| Tool | Accessible Secrets |
|---|---|
choir.notion.query |
Agent’s bound Notion integration secret |
choir.email.send |
Agent’s bound email account secret |
choir.tts.speak |
Agent’s bound TTS provider secret |
choir.exec |
None (secret_resources: []) |
choir.fs.read |
None |
A tool cannot access secrets that are not listed in its secret_resources. This prevents arbitrary shell commands (choir.exec) from accessing API keys.
Secret Refresh
Secrets can be updated without restarting agents:
echo 'new-key-value' | choirctl secret set openrouter-key
choirctl secret apply
secret set/secret delete updates ~/.choir.d/secrets.json. secret apply tells choird to reload that file and push a refresh signal to running agents, which atomically swap their in-memory secret store.
Git Credential Helper
Each agent leases a named git identity with its own credentials:
user.nameanduser.emailare set in the container’s git config at startup (not via env vars).- Git auth uses a custom credential helper:
choir-agentdetectsargv[0] == "git-cred-helper"and acts as a credential helper. - The helper validates the remote host before returning credentials parsed from the mounted git credential secret file. HTTPS secrets may be either
username:passwordor token-only; token-only falls back to the agent ID as the username. - SSH private keys are loaded into
ssh-agentviassh-add -, using the mountedssh_keysecret and optionalssh_secretpassphrase helper. Allowed hosts are enforced via SSH config. - No persistent key files exist in the workspace, env vars, or
.git/config. - Arbitrary git commands are allowed – authentication is handled transparently.
Git identities are exclusive resources: at most one agent may lease a given identity at a time, ensuring commits are unambiguously attributable.
Workspace Non-Authoritative Model
/workspace is explicitly non-authoritative and may be wiped at any time (choirctl workspace reset). Durable truth lives in:
- Remote git origins
- Databases
- Host control-plane state (Postgres,
.choir.d/config.json,.choir.d/secrets.json)
This means:
- Compromised workspace contents cannot corrupt authoritative state.
- Workspace reset is a valid recovery operation.
- The agent treats workspace as a scratch area, not a source of truth.
Approval Pipeline
Capability-changing actions always require human approval:
| Action | Meta-Tool |
|---|---|
| Add or modify a tool | choir.propose.tool |
| Add or modify a skill | choir.propose.skill |
| Change configuration | choir.propose.config_change |
Meta-tools never execute immediately, never hold locks, and always go through the approval pipeline. The human reviews via choirctl or Telegram gateway commands (/approve, /reject).
Unanswered approvals timeout after 30 minutes (configurable) and are automatically rejected.
Network Policy
Arbitrary outbound web requests from containers are allowed in v1. This is a design tradeoff for single-user, single-host deployment where the user manages their own risk.
Browser operations are routed through the host-side Playwright worker via EXECUTE_HOST_TOOL, giving choird visibility into browsing activity. Each agent gets an isolated browser context; no agent can access another’s tabs.
Observability
All security-relevant operations are logged:
- Session lifecycle events (start, stop, crash)
- Tool call traces (proposals, lock acquisition/release, execution results)
- Approval requests and outcomes (who requested, what, approved/rejected, by whom)
- Secret version changes (metadata only, never values)
- Config version changes
Logs are structured JSON lines written to .choir.d/logs/ and include session ID and lane ID for correlation.