colibri/packaging/freebsd/agent-jail-bootstrap.sh
Sam & Claude 4623f8c209
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled
fix(bootstrap): pre-create daemon staging dir in agent jails
Second root cause of the jail-spawn EACCES (found via truss, docs PR #132):
for staged spawns the daemon writes launch.sh/env.sh under
<jail_root>/var/run/colibri-stage/<stage_id>/, but nothing created
/var/run/colibri-stage. The daemon runs as clawdie and cannot mkdir under
root-owned /var/run, so staging failed with Permission denied.

agent-jail-bootstrap.sh now pre-creates the dir owned by the daemon user
(0700), replacing the runtime `chmod 777` workaround — durable across jail
rebuilds and not world-writable (staged files are sourced as shell, so a
world-writable staging dir would be a privilege footgun). DAEMON_USER is
overridable, defaulting to clawdie.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:28:20 +02:00

108 lines
4.3 KiB
Bash
Executable file

#!/bin/sh
# Agent jail bootstrap — install the minimum runtime into a fresh Bastille jail,
# pinned to the EXACT package versions the host already has. The jail reaches the
# host's pkg cache (no internet needed), so installing the host's exact versions
# guarantees host/jail parity and works offline.
#
# Usage: sudo agent-jail-bootstrap.sh <jail_name>
#
# Run order: bastille create <jail> -> this script -> vault provision -> register.
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).
case "${JAIL_NAME}" in
'')
echo "usage: $0 <jail_name>" >&2
exit 2
;;
*[!A-Za-z0-9_-]*)
echo "error: invalid jail name '${JAIL_NAME}' (allowed: A-Z a-z 0-9 _ -)" >&2
exit 2
;;
esac
JAIL_ROOT="/usr/local/bastille/jails/${JAIL_NAME}/root"
if [ ! -d "${JAIL_ROOT}" ]; then
echo "error: jail root not found: ${JAIL_ROOT} — create the jail first" >&2
exit 1
fi
echo "=== Bootstrap ${JAIL_NAME} ==="
# Runtime packages. Each is pinned to the host's installed version (the host's
# cache supplies it), so the jail matches the host exactly. If the host is
# missing one, fail loudly rather than pulling a different version into the jail.
PKGS="python312 node24 npm-node24 bash curl"
for p in ${PKGS}; do
ver="$(pkg query '%v' "${p}" 2>/dev/null || true)"
if [ -z "${ver}" ]; then
echo "error: host has no '${p}' installed — install it on the host first" >&2
echo " (versions are pinned to the host; the cache has nothing to serve otherwise)" >&2
exit 1
fi
if ! ls "${PKG_CACHE_DIR}/${p}-${ver}"*.pkg >/dev/null 2>&1; then
echo "error: host pkg cache is missing ${p}-${ver}" >&2
echo " prime it first: pkg fetch -y ${p}-${ver}" >&2
echo " (offline/exact-version bootstrap depends on ${PKG_CACHE_DIR})" >&2
exit 1
fi
echo " ${p}-${ver}"
pkg -c "${JAIL_ROOT}" install -y "${p}-${ver}"
done
# Copy colibri binaries from the host (same FreeBSD base, so shared libs match).
for bin in colibri colibri-daemon colibri-probe colibri-mcp colibri-test-agent colibri-host-status colibri-runtime-inventory; do
src="/usr/local/bin/${bin}"
if [ ! -x "${src}" ]; then
echo "error: missing host binary ${src} — build/stage it before bootstrap" >&2
exit 1
fi
cp "${src}" "${JAIL_ROOT}/usr/local/bin/${bin}"
chmod 755 "${JAIL_ROOT}/usr/local/bin/${bin}"
done
# Copy npm global agents from the host (jails have no internet).
NPM_PREFIX="/home/clawdie/.npm-global"
mkdir -p "${JAIL_ROOT}${NPM_PREFIX}/bin" "${JAIL_ROOT}${NPM_PREFIX}/lib/node_modules"
if [ ! -d "${NPM_PREFIX}/lib/node_modules/@earendil-works" ]; then
echo "error: missing ${NPM_PREFIX}/lib/node_modules/@earendil-works on host" >&2
exit 1
fi
cp -a "${NPM_PREFIX}/lib/node_modules/@earendil-works" "${JAIL_ROOT}${NPM_PREFIX}/lib/node_modules/"
if [ ! -e "${NPM_PREFIX}/bin/pi" ]; then
echo "error: missing ${NPM_PREFIX}/bin/pi on host" >&2
exit 1
fi
cp -a "${NPM_PREFIX}/bin/pi" "${JAIL_ROOT}${NPM_PREFIX}/bin/pi"
# Put the npm-global bin on PATH for every login shell. Install the canonical
# snippet from colibri (same file the ISO image uses) with NPM_PREFIX baked in.
install -d -m 0755 "${JAIL_ROOT}/etc/profile.d"
{
printf 'NPM_PREFIX="%s"\n' "${NPM_PREFIX}"
cat "$(dirname "$0")/clawdie-npm-profile.sh"
} > "${JAIL_ROOT}/etc/profile.d/clawdie-npm.sh"
chmod 0644 "${JAIL_ROOT}/etc/profile.d/clawdie-npm.sh"
if ! grep -q '/etc/profile.d/clawdie-npm.sh' "${JAIL_ROOT}/etc/profile" 2>/dev/null; then
printf '\n[ -r /etc/profile.d/clawdie-npm.sh ] && . /etc/profile.d/clawdie-npm.sh\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."