Fixed a NameError that crashed every install of v3.1.0 and v3.1.1 at startup, plus two defensive checks so this class of bug can't ship again. v3.1.0 introduced the /api/setup/switch-to-real endpoint for the demo-mode switch-to-real wizard, with a type annotation reading async def switch_to_real_execute(request: Request): — but Request was never imported from fastapi. The release was AST-parse-clean (syntax was valid) but every install crashed at startup with NameError: name 'Request' is not defined the moment FastAPI evaluated the annotation. v3.1.1 carried the same bug forward because the fix only touched templates/updates.html and a single line of server.py response shaping — the bad annotation was untouched. v3.1.2 adds the proper from fastapi import Request to the top-level import line in server.py, allowing the endpoint to keep its standard FastAPI parameter-type annotation. No behavior change for users — the endpoint already worked correctly when it could be reached; the bug just prevented the service from starting up at all.
Operational note for users still on v3.0.x trying to upgrade: v3.1.2 is the first release in the v3.1.x line that starts cleanly. If you haven't successfully upgraded past v3.0.x yet, deploy v3.1.2 directly (skip v3.1.0 and v3.1.1 entirely — there's no value in installing a release that crashes on startup). The recommended path remains the local-zip update flow: download v3.1.2 from GitHub Releases, drag it onto the /updates page's upload zone (or use update-from-zip.sh if you have it), then SSH to the server and run ./install.sh from the install root once to refresh the sudoers rule to v4. After v3.1.2, every subsequent release will apply cleanly through the in-app GitHub channel with the fixed UX from v3.1.1.
Defensive check #1 — install.sh refuses to run from inside an existing install's update/ subdirectory. Today's separate prod-deploy mishap was triggered by exactly this: install.sh was run from inside ~/aerodrome/update/ (where the v3.1.1 zip had just been unpacked), INSTALL_DIR resolved to the staging directory rather than the install root, and the systemd unit was written pointing at /home/<user>/aerodrome/update/venv/bin/python3 — which doesn't exist, because the venv lives in the install root, not staging. The service then exited 203/EXEC on every restart attempt. The new guard checks at install.sh startup: if the script's directory is named update and its parent has a VERSION file plus a main.py, it's sitting inside an existing install's staging area; refuse to run with a clear error pointing at the real install root (cd <parent> && ./install.sh). The check is silent in the normal install path — only fires when the footgun is actually being approached.
Defensive check #2 — static name-resolution check on every release bump, REQUIRED (not advisory). Added to bump-version.sh. Walks every function annotation in server.py, collects all module-scope names (imports, top-level definitions, nested class names that might be Pydantic models), and flags any uppercase name referenced in an annotation that isn't in the set of defined names. Today's Request-without-import bug fails this check loudly with server.py:8816 'Request' and exits non-zero, blocking the bump. The check is intentionally REQUIRED rather than advisory (unlike scripts/check_docs.py) because a release that crashes every install at startup is broken, not "drifted." A --skip-name-check escape hatch exists for documented false positives — Pydantic BaseModel subclasses defined inside get_app() for endpoint payloads, for example — but it requires explicit opt-in, paired with a comment about why the skip is safe. The check correctly passes on the v3.1.2 codebase and was verified by re-introducing the exact bug from v3.1.0/3.1.1 in a test pass, where it fired with the expected output.
Why AST parse alone wasn't enough: Python's ast module checks that source text can be parsed, not that it resolves at runtime. def f(x: Foo) parses fine even when Foo is undefined — the annotation is just an AST node referencing a Name. The error only fires when Python tries to evaluate the annotation, which happens at function-definition time (i.e. module import). For a fast-feedback dev loop where the server actually starts after every change, this catches itself — but for a release pipeline where the bump packages the zip without doing an end-to-end import test, the bug can slip through unless something specifically checks name resolution. v3.1.2's bump-version.sh check is that something.
Scope of changes:
server.py— one-character addition to thefrom fastapi import ...line on line 30 (, Requestappended). Endpoint signature on line 8816 keeps its standardrequest: Requestannotation.install.sh— new guard block at the top, immediately afterINSTALL_DIRis computed. Refuses to run from insideupdate/of an existing install with a clear error and pointer.bump-version.sh— new static name-resolution check between the argument-parse step and the documentation-drift check. Runs by default on every bump; can be skipped with--skip-name-checkfor documented false positives. New--skip-name-checkflag added to the arg parser and the usage docstring.
No data-plane changes, no template changes (beyond the version-header on templates/switch-to-real.html which gets a stale-version drift flag automatically), no config-schema changes, no sudoers changes (SUDOERS_VERSION stays at 4). v3.1.2 is purely a fix release for the startup-crash bug + two release-pipeline guardrails to make sure the same class of bug can't ship again.