left4me/docs/superpowers/specs/2026-05-15-handoff-noneditable-install.md
mwiegand ff2b5c4c5a
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:
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>
2026-05-15 16:53:19 +02:00

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:

  1. The production install uses non-editable pip installs (pip install /opt/left4me/src/l4d2host /opt/left4me/src/l4d2web), not pip install -e ….
  2. /opt/left4me/src/ is owned by root:root, not left4me:left4me.
  3. The left4me_chown_src action and the /opt/left4me/src directory item's owner/group flip accordingly.
  4. 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/ and l4d2web/ pyproject.toml — both already declare [build-system] requires = ["setuptools>=68", "wheel"] and use the flat package-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/*.py at 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_scripts action — 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:

  1. Source ownership:

    stat -c '%U:%G %a %n' /opt/left4me/src /opt/left4me/.venv /opt/left4me/steam /var/lib/left4me
    

    Expected: /opt/left4me/srcroot:root; .venv and steam and /var/lib/left4meleft4me:left4me.

  2. Wheel installed, not editable:

    sudo -u left4me /opt/left4me/.venv/bin/pip show l4d2web l4d2host
    

    Expected: Location: points inside /opt/left4me/.venv/lib/python*/site-packages/, NOT inside /opt/left4me/src/. (Editable installs report the source path as Location:; non-editable reports site-packages.)

  3. App runs:

    systemctl status left4me-web.service
    

    Active, recent logs clean.

  4. 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.

  5. A gameserver starts:

    sudo /usr/local/libexec/left4me/left4me-systemctl start left4me-server@test
    journalctl -u left4me-server@test -n 50
    

    srcds_run starts cleanly. Stop it after verification.

  6. Idempotent bw apply: Run bw apply left4.me a 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}/ into deploy/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. Flat package-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.