Self-contained spec for the next agent to land the editable→ non-editable install switch and the root-ownership flip on /opt/left4me/src. Prereq for the deployment-responsibility brainstorm: target-side symlinks from /etc/... into the checkout's deploy/files/ only become safe once the checkout is unwritable by the left4me user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Handoff — non-editable install + root-owned /opt/left4me/src
Status
Queued. Must land before the deployment-responsibility brainstorm
resumes (docs/superpowers/specs/2026-05-15-handoff-deployment-responsibility.md).
This is the prereq that makes target-side symlinks of deployment
artifacts safe.
The task
Change ckn-bw's bundles/left4me/ so that:
- The production install uses non-editable pip installs
(
pip install /opt/left4me/src/l4d2host /opt/left4me/src/l4d2web), notpip install -e …. /opt/left4me/src/is owned by root:root, not left4me:left4me.- The
left4me_chown_srcaction and the/opt/left4me/srcdirectory item'sowner/groupflip accordingly. - The pip-install action moves from "runs every apply" to "triggered
by
git_deploy:/opt/left4me/src" — non-editable installs always rebuild a wheel, so running unconditionally is wasteful.
Local-development install flows (direnv + pip install -e ./l4d2host -e ./l4d2web) are unchanged. Editable installs remain correct on
developer machines; only the production install model on the host
changes.
Why
Two reasons, listed in priority order.
Security. The deployment-responsibility brainstorm wants to make
left4me/deploy/files/ the live source of truth for systemd units,
drop-ins, sudoers, sysctl, and helpers, delivered by ckn-bw via
target-side symlinks (/etc/foo → /opt/left4me/src/deploy/files/...).
If the symlink target sits inside a left4me-writable directory, the
service can rewrite its own hardening drop-in and escape the sandbox
on next restart. Making /opt/left4me/src/ root-owned closes that
hole at the filesystem layer, before symlinks ever come into the
picture. Defense-in-depth that costs us nothing the production
workflow actually used.
Operational honesty. The only reason /opt/left4me/src/ is
user-owned today is that pip install -e writes .egg-info into the
source tree. No production workflow ever edits files under
/opt/left4me/src/ directly — code updates always come through
git_deploy + pip_install. Editable mode buys nothing on the host;
non-editable matches what the deploy actually does (rebuild + reinstall
wheel from new source).
What changes — concretely
All edits are in ~/Projekte/ckn-bw/bundles/left4me/.
items.py
Directory items (items.py:7-42) — flip /opt/left4me/src to root:
directories = {
'/opt/left4me': {
'owner': 'root',
'group': 'root',
},
'/opt/left4me/src': {
'owner': 'root',
'group': 'root',
# Was left4me:left4me before the non-editable install switch;
# production now installs wheels, so the source tree is read-only
# at runtime. Keeps left4me from being able to rewrite its own
# hardening drop-ins / unit files (see deployment-responsibility
# handoff for the full argument).
},
# /var/lib/left4me/* and /opt/left4me/{steam,.venv} stay left4me:left4me.
...
}
left4me_pip_install action (items.py:247-263) — drop -e,
become triggered:
actions['left4me_pip_install'] = {
# Non-editable install: builds wheels from the checkout, installs
# into the venv's site-packages. Source tree is no longer mutated by
# pip, so /opt/left4me/src/ stays root:root with read-only access for
# left4me at runtime.
'command': 'sudo -u left4me /opt/left4me/.venv/bin/pip install /opt/left4me/src/l4d2host /opt/left4me/src/l4d2web',
'triggered': True, # was: ran every apply
'cascade_skip': False,
'needs': [
'git_deploy:/opt/left4me/src',
'action:left4me_create_venv',
# action:left4me_chown_src removed (deleted below).
],
'triggers': [
'action:left4me_alembic_upgrade',
],
}
left4me_chown_src action (items.py:207-219) — delete.
The action exists to repair file ownership after each git_deploy
extracts the tarball as root and we needed it as left4me. With the new
model, root is the target ownership, which is also what git_deploy
already produces. Action becomes a no-op; remove it.
git_deploy triggers (items.py:157-183) — ensure
action:left4me_pip_install is in triggers. Currently triggers
left4me_alembic_upgrade and install_left4me_scripts; add
left4me_pip_install so that a fresh checkout always rebuilds the
wheel and reinstalls.
metadata.py
No changes. The systemd/units reactor's WorkingDirectory and
timer working_dir still point at /opt/left4me/src — that path is
still readable as left4me regardless of ownership (it's
world-readable by default after git_deploy extracts as root).
README.md
Line 48 mentions pip install -e. Update to reflect non-editable
production install and add a one-line note that local dev still uses
-e. Two lines of edits.
l4d2web.egg-info/, l4d2host.egg-info/ on the live host
These directories exist today inside /opt/left4me/src/l4d2{host,web}/
as a side-effect of editable installs. After the switch they become
stale (pip installs a fresh wheel into the venv; the in-source egg-info
is unused). Clean-up options:
- Leave them: harmless, ignored by Python. Eventually removed by whoever next refactors the source layout.
- One-shot remove on the live host:
sudo find /opt/left4me/src -name "*.egg-info" -type d -exec rm -rf {} +. Cosmetic; do whatever.
Either's fine. Document the choice in the commit message.
What does NOT change
l4d2host/andl4d2web/pyproject.toml— both already declare[build-system] requires = ["setuptools>=68", "wheel"]and use the flatpackage-dir = {l4d2host = "."}layout. Non-editable install works out of the box; no packaging edits needed.alembic.ini+ migrations — alembic reads/opt/left4me/src/l4d2web/alembic/versions/*.pyat runtime. Root ownership + world-readable means left4me can still read; no change.examples/script-overlays/— same; read-only access by left4me at seed time./opt/left4me/.venv/— stays left4me:left4me (pip writes here during the install action, run as left4me via sudo)./opt/left4me/steam/— stays left4me:left4me (steamcmd self-updates)./var/lib/left4me/and all subdirs — stays left4me:left4me (application runtime state).- Local-dev install instructions in
README.md,AGENTS.md,l4d2web/README.md— keep-e. Developer machines need editable. install_left4me_scriptsaction — already copies from src as root, target paths under/usr/local/{libexec,sbin}/. Source can be root-owned now (no change in behavior).- Hardening composition + every deployed unit / drop-in / sudoers / sysctl file — out of scope for this change. Those move in the deployment-responsibility brainstorm, after this lands.
Verification
Run on left4.me (the production host) after bw apply:
-
Source ownership:
stat -c '%U:%G %a %n' /opt/left4me/src /opt/left4me/.venv /opt/left4me/steam /var/lib/left4meExpected:
/opt/left4me/src→root:root;.venvandsteamand/var/lib/left4me→left4me:left4me. -
Wheel installed, not editable:
sudo -u left4me /opt/left4me/.venv/bin/pip show l4d2web l4d2hostExpected:
Location:points inside/opt/left4me/.venv/lib/python*/site-packages/, NOT inside/opt/left4me/src/. (Editable installs report the source path asLocation:; non-editable reports site-packages.) -
App runs:
systemctl status left4me-web.serviceActive, recent logs clean.
-
Alembic can still read migrations:
sudo -u left4me sh -c 'cd /opt/left4me/src/l4d2web && /opt/left4me/.venv/bin/alembic current'Returns the current head without errors.
-
A gameserver starts:
sudo /usr/local/libexec/left4me/left4me-systemctl start left4me-server@test journalctl -u left4me-server@test -n 50srcds_run starts cleanly. Stop it after verification.
-
Idempotent
bw apply: Runbw apply left4.mea second time. Should report zero changes — no chown action drifting back, no pip install re-firing.
Out of scope
- The deployment-responsibility reshape itself. That brainstorm
resumes after this prereq lands on left4.me. Do not touch
deploy/files/, hardening drop-ins, sudoers location, etc. — those are the next session's work. - Removing the
bundles/left4me/files/etc/{sudoers.d,sysctl.d}/verbatim mirrors. Same; that's the deployment-responsibility session. - Moving
scripts/{libexec,sbin}/intodeploy/scripts/. Same. - Reviewing whether the editable install pattern should change for developer machines. It should not — local dev wants editable for fast iteration; only the host install model changes.
Pointers
- Deployment-responsibility brainstorm handoff (the parent
context):
docs/superpowers/specs/2026-05-15-handoff-deployment-responsibility.md - ckn-bw left4me bundle:
~/Projekte/ckn-bw/bundles/left4me/—items.py:7-42(directories)items.py:157-183(git_deploy)items.py:207-219(left4me_chown_src — delete)items.py:247-263(left4me_pip_install)README.md:48(docs update)
- pyproject.toml layouts:
l4d2host/pyproject.toml,l4d2web/pyproject.toml. Flatpackage-dir = {<pkg> = "."}layout. Non-editable wheel build works with this layout without further changes. - Hardening test plan (motivates the security argument):
docs/superpowers/specs/2026-05-15-hardening-test-plan.md - Original deployment design (the shape we're working toward):
docs/superpowers/specs/2026-05-06-left4me-deployment-design.md
Commit messages (suggested)
ckn-bw side (the actual change):
refactor(left4me): non-editable install + root-owned /opt/left4me/src
Drop `pip install -e` for the production install; switch to wheel
install (`pip install /opt/left4me/src/l4d2{host,web}`). Source tree no
longer needs to be writable by left4me, so flip /opt/left4me/src to
root:root and delete the left4me_chown_src action.
Prereq for the deployment-responsibility reshape: makes target-side
symlinks from /etc/... into /opt/left4me/src/deploy/files/... safe by
construction (left4me cannot rewrite its own hardening profile).
Verified on left4.me: bw apply idempotent; pip show reports
site-packages location; web + gameserver units run clean.
left4me side (this handoff doc):
spec(noneditable-install): handoff for the install refactor prereq
Self-contained spec for the next agent to land the editable→
non-editable install switch and the root-ownership flip on
/opt/left4me/src. Prereq for the deployment-responsibility brainstorm.