Reopen Setup for local web credential changes

Merged
YE
4d ago
+1588-190

A configured web server answers /setup with the lobby, so changing its LiveKit or Google keys means finding and editing the config file by hand. A standalone loopback launch without trusted proxies now links to Change keys from the lobby. The page reuses the Setup form: it shows the LiveKit endpoint and whether a Google key is set, never a key. Blank fields keep their values, a new Google key replaces the whole key list (GOOGLE_API_KEYS is cleared), and comments and other settings, SESSION_SECRET and database paths included, stay as written. The merged config goes through the same probe_credentials as a first-time submission before the file is replaced.

Threat model: the loopback bind keeps the network out and the Host check keeps out DNS rebinding, but any account on the machine can reach the port and choose every header. A save therefore needs a code generated at each launch and printed only to the terminal that started the server. The desktop app's child server (CODETRIAL_EXIT_WITH_STDIN), a launch behind trusted proxies and a non-loopback bind get no Change keys, and neither does a config that is a symlink.

The file is replaced by renaming a new file over it. On Unix the new file is 0o600; on Windows it is opened without sharing and given a protected DACL that grants only its owner before any key is written to it. A save is refused while a local interview is running or still writing its report, and once the file has changed since the launch read it, which also covers a second save before the restart. The file is compared again right before the write, because the probes can take seconds, so a hand edit saved meanwhile is kept.

Following the scope agreed on #525, the running server keeps its old keys until someone restarts it by hand: tokens and new interviews use them until then, and the Setup page says so after a save and on every visit until the restart. First-time Setup is unchanged apart from answering GET /api/setup with {"configured": false}, which is how the shared page tells the two apart.

This is the web-server half of #513, where the restart approach and the split were accepted. The desktop app offers no way back to Setup here: restarting its child server after Setup writes the file, without showing the failure page, is the desktop sub-issue of #513.

The first commit only moves code, because src/main.rs was one line under the default 1500-line ceiling and tests/cli.rs was at its recorded one: exit_when_stdin_closes moves unchanged beside exe_dir in the library, and the two tests of what a first Setup submission writes move to tests/cli/setup_save.rs, a module of the same test binary, which the Change keys end-to-end test then joins. The tests/cli.rs ceiling drops to its new size.

The lobby before this change, which a configured server also answered /setup with, light and dark

The lobby with the Change keys link at the end of the account row, light and dark

Change keys at /setup on a configured server, with the LiveKit endpoint and whether a Google key is set, light and dark

The end of Change keys, where a save needs the code the server printed in its terminal, light and dark

Change keys after a save, with the restart notice every visit shows until the restart, light and dark

Validation: rebased onto 3bbbcf5. A copy of check.yml run on a push in my fork with the base pinned to 3bbbcf5 passed every job, and none of the 57 mutants of this diff survived: https://github.com/YEH-YU-YANG/codetrial/actions/runs/41080109227. The setup_edit tests, including the Windows DACL test, pass on a windows-2025 runner: https://github.com/YEH-YU-YANG/codetrial/actions/runs/41080109195. Locally, cargo clippy --workspace --all-targets, cargo doc, scripts/check-rust-style.py and cargo test --workspace pass, the last including an end-to-end save through the binary, which replaces a LiveKit key and adds a Google key against a Gemini mock that sees the model the config sets, and so do the lobby and Setup browser suites, check-test-timing.mjs and ESLint. The screenshots were taken before the rebase, from Chromium at a 1280x860 window, light and dark from the system scheme, on standalone loopback launches of 38d53a7 (before) and f4fafa8 (after), with LiveKit pointed at a stub at ws://localhost:7880 and made-up keys; the lobby's random problem is seeded so the panels match. Nothing merged since changes the lobby's header or the Setup page. First-time Setup renders byte-identical before and after in both schemes, with and without a config offered by --import-config, so it is not shown. A real Google key, a live interview, GitHub OAuth and the desktop app were not exercised.

Close #525