Sync deployment references for the runtime state relocation
shipped via ckn-bw (commit 6fae2fd). /opt/left4me/ is now a
root-owned deploy-artifact root (just src/); .venv and steamcmd
live at /var/lib/left4me/{.venv,steam}.
Touches:
- deploy/files/.../left4me-web.service: PATH + ExecStart
- deploy/files/.../left4me-workshop-refresh.service: WorkingDirectory
(was /opt/left4me, now /opt/left4me/src to match the web unit),
PATH, ExecStart
- scripts/sbin/left4me wrapper: flask path
- deploy/tests/test_example_units.py: PATH + ExecStart assertions
for the web unit; also fix a pre-existing broken assertion that
read "Environment=PATH=..." (the unit has Environment=HOME=...
PATH=... on one line, so "Environment=PATH=" was never present)
- now reads just "PATH=..."
- deploy/README.md: paths
- l4d2host/tests/test_cli.py: LEFT4ME_STEAMCMD fixture path
Design + as-shipped record:
docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md.
The original (narrower) prereq spec at
docs/superpowers/specs/2026-05-15-handoff-noneditable-install.md
is marked superseded with a pointer to what shipped + why the
scope grew (setuptools writes egg-info to source during PEP 517
build prep).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
285 lines
11 KiB
Markdown
285 lines
11 KiB
Markdown
# Handoff — non-editable install + root-owned `/opt/left4me/src`
|
|
|
|
## Status
|
|
|
|
**Superseded 2026-05-15** by what actually shipped — see
|
|
`docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md`.
|
|
The narrow approach proposed here (just flip `/opt/left4me/src` to
|
|
root, switch `pip install -e` → `pip install`) doesn't work as
|
|
described: `setuptools.build_meta` writes `<pkg>.egg-info/` into the
|
|
source dir during `get_requires_for_build_wheel`, which fails against
|
|
a root-owned source. The shipped fix copies source to a writable
|
|
tempdir before building, and (since that one-shot copy was needed
|
|
anyway) also relocates `.venv` + `steam` to `/var/lib/left4me/`.
|
|
|
|
The original prereq goal — making target-side symlinks of deployment
|
|
artifacts safe — is still met; the realized shape is just bigger than
|
|
this doc sketched.
|
|
|
|
This doc is kept as the historical record of the originally-proposed
|
|
approach and why it didn't work.
|
|
|
|
## 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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/src` → `root:root`; `.venv` and `steam` and
|
|
`/var/lib/left4me` → `left4me: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.
|
|
```
|