Review 2026-08-14: CI, task safety, config validation, opt-in auth, History paging, ref-cache TTL - #1
Merged
Merged
Conversation
The repo shipped pytest, Node and ruff guards that only ever ran by hand, and docs/api/ could drift from the adapters unnoticed. scripts/export_openapi.py regenerates every worker document from one entry point; adapters import their model libraries lazily, so it needs no engine venv. --check turns it into a drift gate, and pytest asserts the committed documents still match. CI runs ruff + pytest (with the fake engine venv installed so the worker integration test runs instead of skipping) and the Node WebUI tests on push and pull request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
Engine start/stop/restart, rescan-disable, autostart and idle-unload all ran through a bare asyncio.create_task. The loop holds only a weak reference to a task, so work nobody awaits can be collected mid-flight, and a failure inside it was swallowed entirely. spawn() holds a strong reference until completion, names the task for the logs, and reports exceptions. Tasks already stored on an object keep their own reference and are unchanged; a pytest walks the AST to make sure no new detached create_task creeps back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
PUT /api/config coerced with a bare int(), so a non-numeric port answered 500, and nothing checked ranges — port 0 or a negative retention was accepted and written to config.toml. Worse, fields were applied one by one as they were parsed, so a later failure left the hub half-configured. Everything is now validated up front and rejected with a 422 the WebUI already surfaces as a toast; cfg is mutated only once the whole payload is known good. Bounds live in config.HUB_INT_BOUNDS beside the defaults. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The hub binds 0.0.0.0 by default with no authentication, so any device on the LAN could rewrite config.toml through PUT /api/config — including data_dir and per-engine env vars, which land in a spawned worker process. Setting [hub] auth_token now gates every /api/* call behind a bearer token compared with compare_digest. Leaving it unset changes nothing. Streams that cannot send a header (SSE, <audio>, download links) accept ?token=; the static shell stays open so the browser can load the page that asks for the token. The secret is never echoed by GET /api/config, cannot be rotated through the API, and save_config preserves it so saving from Settings can never drop it. Covered by pytest (both credential paths, 401s, config round-trip), node tests for the URL/retry logic, and a Playwright suite that drives a real browser against a real hub for the gate, reload persistence and recovery from a stale token. CI gains an e2e job; the WebUI strings ship in en/fr/zh with a new locale parity test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The History page asked for a fixed limit=100 and the endpoint returned a
bare array, so with retention off (the default) everything older than the
hundredth generation was unreachable from the UI.
GET /api/history now returns {items, total, offset, limit} with the limit
clamped to 1–200, and the page appends with a "Load N more" button. New
generations are folded into the top of the list rather than resetting it,
so a finished job no longer discards the pages already loaded, and deleting
a row adjusts the count in place.
Covered by pytest for paging, clamping and totals, node tests for the merge
rules, and Playwright walking 230 seeded rows to the oldest one while
asserting no row is served twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
Workers cache every uploaded reference clip under /tmp by content hash and never removed one, so a long-lived worker accumulated each voice it had ever cloned. Startup now drops clips untouched for a week, and a cache hit refreshes the mtime so a voice still in regular use is never collected. Failures while scanning are ignored — a temp-dir problem must not stop a worker booting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The token work rewired imports across six modules; a broken specifier or a missing translation key would still render a shell and pass the existing assertions. Walking all five pages with the console watched catches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
loadMore asked the server for offset=rows.length, but a job finishing while that request was in flight ran load(), which merges a fresh first page and inserts rows above that offset. The page then came back shifted and half of it was dropped as already-seen: no data was lost and the next click healed it, but the "Load N more" count wobbled and the user paid an extra click. A refresh arriving during a page request now queues itself and runs once the page has landed, so the offset a request was built from stays valid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
The middleware matched paths starting with "/api", which also covers static WebUI files whose names merely begin with those letters. None exist today, but a future one would answer 401 to a browser that cannot yet ask for the token. No route is a bare /api, so the slash costs nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
withAuthRetry replayed a request once and then gave up. If the token was rotated in config.toml mid-session, or the user mistyped it at the prompt, the replay was still unauthorised and the call failed with a toast, leaving no way back except a page reload. The retry is now a loop, matching ensureAuthenticated: each 401 re-opens the prompt. It only ever awaits promptForToken(), so it is paced by the user and cannot spin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
PUT /api/config ran str() and int() over the incoming values, so via curl host = null was saved as the string "None", port = 3.9 became 3 and retention_days = true became 1. The WebUI only ever sends numbers and strings, so none of this was reachable from the UI. Values are now type-checked: host and data_dir must be strings, and the numeric settings must be whole numbers — bools and floats are refused rather than truncated. Digit strings stay accepted, since hand-written clients send them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the six findings from the 2026-08-14 repo review, one commit each, on top of
5b51173.What's in it
f9f9cece51387espawn()helper so detached lifecycle tasks can't be GC'd mid-flight06109f2PUT /api/configvalidates before applying; 422 instead of 50006944bdauth_tokengating/api/*(default behaviour unchanged)55c9023e6c34b0Notes for review
P1 — adapters import their model libraries lazily, so
scripts/export_openapi.pyregenerates all eight worker documents from the hub venv alone, no engine venv needed. It reproduces the committeddocs/api/byte for byte; pytest asserts they stay in sync. CI installs the fake engine venv so the existing worker integration test runs instead of skipping.P4 is the only change with a behavioural surface, and it is inert unless
[hub] auth_tokenis set. Two details worth a look: EventSource and<audio src>cannot send a header, so those URLs fall back to?token=(which lands in the access log of the machine that already holds the plaintext token — documented in both READMEs); andsave_confignow preserves a token the WebUI never sees, so saving from Settings cannot silently drop it. The token cannot be rotated through the API it protects.P5 changes
GET /api/historyfrom a bare array to{items, total, offset, limit}. The WebUI is the only consumer — audio-book-maker uses discovery/lifecycle plus direct worker generation. New generations now fold into the top of the list rather than resetting it, so a finished job no longer discards loaded pages.Verification
All run locally on macOS; CI runs the first three on every push.
pytest— 56 passed (9 pre-existing + 47 new)ruff check— clean across hub, sdk, engines, scriptsnode --test— 21 passed (4 pre-existing + 17 new)playwright test— 8 passed in Chromium against a real hub process, covering the token gate, reload persistence, recovery from a stale token, paging 230 seeded rows to the oldest without duplicates, and every page loading without console errorsscripts/export_openapi.py --check— docs current🤖 Generated with Claude Code
https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s