fix: arm cron on agent tenant boxes

Agent tenants exist to run the cron-driven duty engine, and the engine's
installer runs unprivileged — it can detect a missing cron but never
apt-get it. Install cron with the shared toolbelt, then assert the
effective state, not the package: crontab on PATH AND cron.service
enabled and active, converging (unmask/enable/start) best-effort first.
A masked daemon with the binary present is exactly the silent-inert box
the issue reports, so the service asserts are the authority and either
failing dies naming cron.

staging-box stays exempt with the rest of the agent-assert block: no
agent, no duty engine. Machine roles (bootstrap.sh) are deliberately
not widened — the issue names tenants as the demonstrated gap.

Closes #162

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-25 18:54:27 +00:00
parent 34ff1c8917
commit 29c4a0840b
3 changed files with 37 additions and 1 deletions

3
changelog.d/162.md Normal file
View file

@ -0,0 +1,3 @@
### Fixed
- Agent tenant boxes ship cron — binary asserted, service enabled and active — so the duty engine can arm its timer (#162)

View file

@ -246,8 +246,11 @@ else
# The shared agent toolbelt the templates carried, plus the definition's
# APT_EXTRAS (claude-box's zsh rides there). Unquoted on purpose — it is a
# word list, every word already vetted by the parser's package-name gate.
# cron is toolbelt, not a template flavour: every agent tenant exists to
# run the cron-driven duty engine, whose unprivileged installer can detect
# a missing cron but never apt-get it (#162).
# shellcheck disable=SC2086
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential $TPL_APT_EXTRAS
apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential cron $TPL_APT_EXTRAS
fi
# Assert the effective toolbelt, not apt's exit code — tmux is the box#65
# contract ('box tmux' runs tmux new-session inside every box) and gh is how
@ -256,6 +259,24 @@ command -v tmux >/dev/null 2>&1 || die "tmux missing after package install — '
if [ "$ROLE" != "staging-box" ]; then
command -v gh >/dev/null 2>&1 || die "gh missing after package install"
command -v git >/dev/null 2>&1 || die "git missing after package install"
command -v crontab >/dev/null 2>&1 || die "crontab missing after package install — the duty engine arms itself with cron (#162)"
# The binary on PATH is not the effective state — an image can ship crontab
# with cron.service masked or stopped, and an unarmed timer is exactly the
# silent-inert box #162 is about. Converge best-effort, then assert what
# systemd actually reports; the assert is the authority. Enabling an
# already-enabled unit is a no-op and no path here touches any crontab.
# staging-box is exempt with the rest of this block: no agent, no engine.
if ! systemctl is-enabled cron >/dev/null 2>&1; then
systemctl unmask cron >/dev/null 2>&1 || true
systemctl enable cron >/dev/null 2>&1 || true
log "enabled cron.service"
fi
if ! systemctl is-active cron >/dev/null 2>&1; then
systemctl start cron >/dev/null 2>&1 || true
log "started cron.service"
fi
systemctl is-enabled cron >/dev/null 2>&1 || die "cron.service is not enabled after converge — the duty engine's timer never fires without it (#162)"
systemctl is-active cron >/dev/null 2>&1 || die "cron.service is not active after converge — the duty engine's timer never fires without it (#162)"
fi
# --- docker ------------------------------------------------------------------

View file

@ -655,6 +655,18 @@ check "tenant: a crafted role name dies at the charset gate" 2 "invalid tenant r
"$ROOT/commands/bootstrap-tenant.sh" 'UPPER-box'
check "tenant: dockerd effective-state assert is present" 0 "" \
grep -qF "docker info" "$ROOT/commands/bootstrap-tenant.sh"
# The #162 contract, both halves: cron installs with the agent toolbelt (the
# duty engine's unprivileged installer can never apt-get it), and PATH is not
# the effective state — the service must be asserted enabled AND active, or a
# masked daemon leaves every tenant crontab silently inert.
check "tenant: cron rides the agent toolbelt install" 0 "" \
grep -qE '^ *apt-get install .* cron ' "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: crontab toolbelt assert is present" 0 "" \
grep -qF "command -v crontab" "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: cron.service enabled assert is present" 0 "" \
grep -qF "systemctl is-enabled cron" "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: cron.service active assert is present" 0 "" \
grep -qF "systemctl is-active cron" "$ROOT/commands/bootstrap-tenant.sh"
# The machine-role traits die with the tenant story, never "unknown flag" — an
# operator coming from the machine families needs the boundary, not a shrug.
check "tenant: trait flags die with the tenant story" 2 "have no traits" \