Sysctl drop-in lives in left4me/deploy/files/etc/sysctl.d/99-left4me.conf
(absorbed kernel.yama.ptrace_scope from the metadata entry). Deliver
via target-side symlink instead of a verbatim copy.
Canary for the deployment-responsibility reshape (left4me design doc
2026-05-15-deployment-responsibility-design.md, step 1). Validated
end-to-end on ovh.left4me: symlink resolves to the checkout,
sysctl --system fires on apply, kernel target value matches, idempotent.
One-shot cleanup of stale /etc/sysctl.d/99-left4me-ptrace.conf
(orphan from earlier apply; bundles/sysctl does not auto-purge unmanaged
files).
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Adds the metadata key default (None — node must override) and pipes it
into web.env.mako so the live-state poller can resolve Steam IDs to
persona names + avatars via GetPlayerSummaries.
ovh.left4me gets the actual key as an encrypted vault secret.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two changes from the same debug session, both prerequisites for
`l4d2ctl install` to work end-to-end on a fresh node:
1) Install steamcmd via tarball under /opt/left4me/steam.
- dpkg --add-architecture i386 + libc6:i386 + lib32z1 (32-bit deps;
bw pkg_apt translates _ to : at install time, hence libc6_i386)
- curl|tar one-shot, guarded by `test -x steamcmd.sh`
- LEFT4ME_STEAMCMD in host.env so l4d2host invokes by absolute path
(mirrors the old bundles/left4dead2/files/setup approach; avoids
the dirname-$0 trap that bites when steamcmd is reached via a
PATH symlink)
2) Drop the `unless` on left4me_pip_install. The gate checked
importability of l4d2host/l4d2web, which is too weak a proxy for
install state: adding [project.scripts] to pyproject.toml later
wouldn't be picked up if the package was already importable from a
prior `pip install -e`. Cost is ~2s/apply for a no-op pip
resolution — not enough to keep the gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle metadata declares port_range_start/end in defaults, but the
running app (l4d2web/config.py:34-35) reads them from
LEFT4ME_PORT_RANGE_START/END env vars. Without these in web.env, the
bundle's metadata values were dead code and the app fell back to its
own hardcoded defaults. Wiring them through closes the loop.
SECRET_KEY pulled from node metadata (set via !32_random_bytes_as_base64_for:
in the node file). SESSION_COOKIE_SECURE flips to true since nginx fronts
gunicorn with TLS.
Copied verbatim from left4me/deploy/files/. Helpers are the trust unit
the sudoers rules grant access to; left as static files (not generated)
so the audit trail stays grep-able. Modes/owners are set via items.py
in the next commit.