Jail confinement

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

TypeHowWhen to use
Persistentjexec into an existing jailOperator-managed jails with preconfigured environments
Ephemeraljail -c command= auto-destroyedOne-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:

on the operator image where mdo is configured. falls back to sudo). 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