Rewrite "What this bundle does" to reflect the post-migration model:
- Target-side symlinks table for the 6 static artifacts (sudoers, sysctl,
2 hardening drop-ins, 4 libexec helpers, sbin wrapper)
- Reactor-emitted units section (per-host shape: web/server@ units, slices,
cpuset drop-ins)
- bw files{} for the templated env files
- Action chains section covering the full deploy lifecycle
- Reference to the design doc for rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two related changes landed together:
1. /opt/left4me/ becomes a root-owned deploy-artifact root. Only
/opt/left4me/src lives there. Source tree is no longer mutated
by pip at runtime; left4me only needs read access.
2. Runtime mutable state moved to /var/lib/left4me/: the venv (was
/opt/left4me/.venv) and steamcmd (was /opt/left4me/steam).
The non-editable install copies the (root-owned) source to a
left4me-owned tempdir before `pip install --force-reinstall`.
setuptools.build_meta writes <pkg>.egg-info/ into the source dir
during get_requires_for_build_wheel, so a direct `pip install
/opt/left4me/src/l4d2*` fails on a root-owned source. The
temp-copy is what `python -m build` ought to do but doesn't (its
build isolation only sandboxes deps).
Prereq for the deployment-responsibility reshape: target-side
symlinks from /etc/... into /opt/left4me/src/deploy/files/... are
now safe by construction (left4me cannot rewrite its own hardening
profile).
Design + verification record: left4me/docs/superpowers/specs/
2026-05-15-runtime-state-relocation-design.md
Verified on ovh.left4me: bw apply idempotent on second pass (0
fixed, 0 failed); pip show reports site-packages location, no
Editable project location; web + gameserver units run clean;
alembic current returns head.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the uid-collapse refactor on the left4me side
(docs/superpowers/plans/2026-05-15-uid-collapse.md). The script-
sandbox now runs as left4me too, defended by the hardening profile
that landed earlier today rather than a kernel uid boundary.
users + groups dicts: remove the l4d2-sandbox entry (uid/gid 981).
/var/lib/left4me mode: 0711 → 0755. The 0711 was specifically a
traverse-only loosening for the sandbox uid; with one user, the
natural mode is back.
Replaces bundle-default system_core_count int with a per-node set of
CPU ids; reactor takes set complement for game cores. ovh.left4me sets
{0, 4} to keep both HT siblings of physical core 0 in system.slice
so games don't share L1/L2 with system work. systemd_units reactor
return inlined.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First N cores pin system/user/build (inline on owned slices, drop-ins
on upstream system.slice and user.slice via the systemd/units
'<parent>.d/<basename>.conf' convention). Remainder pins
l4d2-game.slice. Reactor raises on hosts with <2 threads or
system_core_count that leaves no cores for games.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-liner instead of "ssh + heredoc + sudo + sh -c + double quotes":
sudo left4me create-user alice --admin
sudo left4me seed-script-overlays /opt/left4me/src/examples/script-overlays
sudo left4me routes
The wrapper sources host.env + web.env, drops to the left4me user,
sets JOB_WORKER_ENABLED=false (admin-side ops shouldn't race the
worker) and PYTHONPATH=/opt/left4me/src, then exec's the flask CLI
with whatever args followed `left4me`. No env-var enumeration: the
sh -c trailing 'sh "$@"' forwards positional args without quoting
hell. README updated to drop the verbose recipe.
README:
Updated metadata example to show domain as the only required key.
Documented the bundle's derived_from_domain reactor as the source of
nginx/letsencrypt/monitoring/nftables-input wiring, and the
bundle-defaults source of backup/paths.
nodes/ovh.left4me.py:
- groups: + backup, + left4me, + webserver
- bundles: dropped 'left4me' and 'nftables' (come via groups now;
nftables ships with debian-13).
- metadata: pinned vm/cores=4, vm/threads=8 (4-core HT box) so the
nginx bundle's worker_processes resolves; left4me block reduced to
{'domain': 'left4.me'} — git_url, git_branch, secret_key, and the
nginx/letsencrypt/monitoring/nftables/backup blocks now come from
bundle defaults / the derived_from_domain reactor.