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

  1. choir-agent boots and connects to choird over the configured transport.
  2. Sends INIT_HELLO with agent ID, session ID, and image version.
  3. choird validates identity and resolves resource bindings.
  4. Agent sends GET_SECRETS with a list of named secret references (derived from "secret" fields of bound resources).
  5. choird replies with secret values.
  6. 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 env or /proc/<pid>/environ would 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:

  1. user.name and user.email are set in the container’s git config at startup (not via env vars).
  2. Git auth uses a custom credential helper: choir-agent detects argv[0] == "git-cred-helper" and acts as a credential helper.
  3. The helper validates the remote host before returning credentials parsed from the mounted git credential secret file. HTTPS secrets may be either username:password or token-only; token-only falls back to the agent ID as the username.
  4. SSH private keys are loaded into ssh-agent via ssh-add -, using the mounted ssh_key secret and optional ssh_secret passphrase helper. Allowed hosts are enforced via SSH config.
  5. No persistent key files exist in the workspace, env vars, or .git/config.
  6. 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.