4.4 KiB
Jail confinement
← index
What this is
Colibri can confine spawned agents and external MCP servers inside FreeBSD
jails. The spawner wraps the subprocess command through jexec (persistent
jails) or jail -c (ephemeral jails), so the agent's entire filesystem view
and network are isolated. stdio passes through unchanged — the agent's JSONL
still reaches Glasspane, and the MCP host's stdin/stdout transport still works.
Decisions
Reuse the spawner's confinement primitive (don't build a parallel one)
The agent spawner and the external-MCP host both need to confine untrusted
subprocesses. Instead of building a second confinement layer, the MCP host
reuses the agent spawner's jail_wrap() function directly — the same
JailConfig struct, the same PrivMode policy, the same prepare_spawn_command
pipeline.
Why reuse: two confinement paths → one can drift. The spawner is tested
(20+ unit tests in spawner.rs covering named, ephemeral, staged, priv-mode
variants). The MCP host gets a battle-tested implementation for free.
→ COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md,
crates/colibri-daemon/src/spawner.rs
(jail_wrap, JailConfig),
crates/colibri-mcp/src/external.rs
Persistent vs ephemeral jails
| Type | How | When to use |
|---|---|---|
| Persistent | jexec <name> into an existing jail |
Operator-managed jails with preconfigured environments |
| Ephemeral | jail -c command=<binary> auto-destroyed |
One-shot confinement, no state between runs |
The JailConfig struct uses an enum: if name is set, jexec; if path is set,
ephemeral. They're mutually exclusive; name takes precedence.
Why both: persistent jails are operator-managed infrastructure (a build jail, a worker jail that persists between agent runs). Ephemeral jails are for untrusted one-shot work — like an external MCP server from a third-party registry. The caller picks the lifecycle.
→ COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md
Priv-mode policy (mdo on live USB, helper on deployed)
The daemon is unprivileged but jail creation requires root. The priv-mode policy resolves this without granting the daemon blanket sudo:
mdo— the live USB's operator tool (mdo -u root jail -c ...). Used on the operator image wheremdois configured.helper— a setuid helper binary on deployed hosts (not yet shipped; falls back tosudo). The daemon never runs as root.
The policy is configurable via COLIBRI_JAIL_PRIV_MODE and is resolved once
at daemon startup. The same policy applies to agents and MCP servers.
Why not the daemon as root: the daemon spawns arbitrary subprocesses
(potentially attacker-controlled, via the MCP registry or task intake).
Running as unprivileged colibri limits the blast radius; the priv-mode
helper grants only the specific operations needed (jail creation).
→ COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md,
crates/colibri-daemon/src/spawner.rs
(PrivMode)
MCP servers are jailed by default (same threat model as agents)
External MCP servers registered in the external MCP registry accept an optional
jail field with the same shape as agent spawn configs. The MCP host applies
the jail wrapper before spawning the server. Servers without a jail field
run on the host (backward compatible).
The MCP host's registry entry supports per-server jail configuration — different servers can run in different jails. This is a property of the registry, not a global daemon setting.
Why jailed by default: external MCP servers are arbitrary third-party binaries — at least as untrusted as the agents Colibri already jails. The threat model is identical.
→ COLIBRI-EXTERNAL-MCP-PROTOTYPE.md,
crates/colibri-mcp/src/external.rs
See also
- mother-hive — the SSH forced-command boundary (a different confinement model)
- agent-harness — the spawner that jails agents