fix(spawner): stage jail spawn files under daemon-owned home, not /var/run
Some checks failed
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled

Closes #135. The daemon stages per-spawn launch.sh/env.sh under the jail root;
the previous location /var/run/colibri-stage is root-owned, so the daemon
(running as clawdie) could not create per-spawn subdirs there — the second
jail-spawn EACCES, worked around in #134 by pre-creating the dir in
agent-jail-bootstrap.sh.

Move the default staging root to the daemon user's home,
/home/clawdie/.cache/colibri/stage, which clawdie owns by construction of the
jail account. create_dir_all now succeeds with no privileged pre-creation step,
and /home is persistent (unlike a tmpfs /var/run). The path is overridable via
COLIBRI_JAIL_STAGE_DIR, matching the daemon's other env-configurable paths.

- spawner.rs: const → staged_jail_run_dir() resolver; updated unit test.
- agent-jail-bootstrap.sh: drop the now-unnecessary install -d staging block
  and DAEMON_USER var (the #134 workaround).
- docs: update jailed-spawn design + truss analysis to the new location.

clippy clean; spawner suite green (21 tests); sh -n clean; touched docs pass
the markdown gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sam & Claude 2026-06-21 17:37:32 +02:00
parent 35f1f3f7b0
commit a7565c49ad
4 changed files with 26 additions and 17 deletions

View file

@ -121,7 +121,20 @@ impl PrivMode {
/// Default path to the setuid jail-spawn helper used in `PrivMode::Helper`.
pub const DEFAULT_JAIL_HELPER: &str = "/usr/local/libexec/colibri-jail-spawn";
const STAGED_JAIL_RUN_DIR: &str = "/var/run/colibri-stage";
/// Jail-relative directory under which the daemon stages per-spawn `launch.sh` /
/// `env.sh` files. It lives under the daemon user's home (`clawdie`) rather than
/// root-owned `/var/run`, so the daemon can create per-spawn subdirs itself with
/// no privileged pre-creation step — and `/home` is persistent, unlike a tmpfs
/// `/var/run`. Overridable via `COLIBRI_JAIL_STAGE_DIR`.
const DEFAULT_JAIL_STAGE_DIR: &str = "/home/clawdie/.cache/colibri/stage";
fn staged_jail_run_dir() -> String {
std::env::var("COLIBRI_JAIL_STAGE_DIR")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_JAIL_STAGE_DIR.to_string())
}
/// Optional FreeBSD jail confinement for a spawned agent.
///
@ -383,7 +396,7 @@ pub async fn prepare_spawn_command(
)
})?;
let jail_stage_dir = format!("{STAGED_JAIL_RUN_DIR}/{stage_id}");
let jail_stage_dir = format!("{}/{stage_id}", staged_jail_run_dir());
let host_stage_dir = Path::new(stage_root).join(jail_stage_dir.trim_start_matches('/'));
tokio::fs::create_dir_all(&host_stage_dir).await?;
@ -1030,12 +1043,12 @@ mod jail_tests {
assert_eq!(prepared.argv[1], "/bin/sh");
assert_eq!(
prepared.argv[2],
"/var/run/colibri-stage/agent-test/launch.sh"
"/home/clawdie/.cache/colibri/stage/agent-test/launch.sh"
);
assert!(prepared.env.is_empty());
let launcher = root.join("var/run/colibri-stage/agent-test/launch.sh");
let env_file = root.join("var/run/colibri-stage/agent-test/env.sh");
let launcher = root.join("home/clawdie/.cache/colibri/stage/agent-test/launch.sh");
let env_file = root.join("home/clawdie/.cache/colibri/stage/agent-test/env.sh");
assert!(launcher.exists());
assert!(env_file.exists());
let env_raw = std::fs::read_to_string(env_file).unwrap();

View file

@ -65,7 +65,9 @@ just `jexec`. We choose the escalation per host via `PrivMode`
When a jailed spawn needs env vars or a working dir, `prepare_spawn_command()`
writes a 0600 `env.sh` (sorted, single-quoted exports) and a `launch.sh` wrapper
into a staged directory under the jail's `root_path` at
`/var/run/colibri-stage/<id>/`. The jail command runs `/bin/sh launch.sh`, which
`/home/clawdie/.cache/colibri/stage/<id>/` (the daemon user's home, so the
daemon creates it with no privileged step; overridable via
`COLIBRI_JAIL_STAGE_DIR`). The jail command runs `/bin/sh launch.sh`, which
sources the env file and `cd`s to the working dir before `exec`-ing the agent
binary. This bypasses the env-passthrough problem entirely — no reliance on
`jexec`/`mdo` inheriting env vars.

View file

@ -30,9 +30,11 @@ For jailed spawns with environment variables, the daemon's
created by a previous run (as root) and was mode 755 root:wheel.
The daemon runs as `clawdie` and could not write staging files there.
**Fix:** `chmod 777 <jail_root>/var/run/colibri-stage` (or, better:
`agent-jail-bootstrap.sh` should pre-create this directory with appropriate
ownership).
**Fix (history):** initially `chmod 777 <jail_root>/var/run/colibri-stage`, then
`agent-jail-bootstrap.sh` pre-created it clawdie-owned `0700` (#134). **Final
(#135):** staging moved out of root-owned `/var/run` to the daemon user's home
at `/home/clawdie/.cache/colibri/stage/<id>/`, so the daemon creates it itself
with no privileged pre-creation step (overridable via `COLIBRI_JAIL_STAGE_DIR`).
## The Winning Spawn

View file

@ -11,7 +11,6 @@ set -eu
JAIL_NAME="${1:-}"
PKG_CACHE_DIR="${PKG_CACHE_DIR:-/var/cache/pkg}"
DAEMON_USER="${DAEMON_USER:-clawdie}"
# The jail name becomes a path component, so reject anything that could escape
# /usr/local/bastille/jails/<name>/root (empty, traversal, odd characters).
@ -98,11 +97,4 @@ if ! grep -q '/etc/profile.d/clawdie-npm.sh' "${JAIL_ROOT}/etc/profile" 2>/dev/n
>> "${JAIL_ROOT}/etc/profile"
fi
# Pre-create the daemon's per-spawn staging directory. The daemon runs as
# ${DAEMON_USER} and stages launch.sh/env.sh under <stage_id> subdirs here, so
# it must own this directory. Created clawdie-owned 0700 rather than left for a
# root-owned /var/run to block (the spawn EACCES) or patched world-writable.
install -d -o "${DAEMON_USER}" -g "${DAEMON_USER}" -m 0700 \
"${JAIL_ROOT}/var/run/colibri-stage"
echo "Done — ${JAIL_NAME} ready for vault provision."