June 20, 2026

Closing an unauthenticated voice endpoint on LAN

A security fix nobody should have needed, a dead classifier finally buried, and 2,300+ lines of frontend cruft cut — all in one day of hardening the 0.9.0 line.

Some days are about adding. This one was about subtracting and closing doors — the kind of day that doesn’t produce a headline feature but makes everything underneath it trustworthy.

The one I care about most: /voice/health, /voice/synthesize, and /voice/transcribe had no auth, and had none since the voice blueprint’s first commit. Chalie binds to 0.0.0.0 so it’s reachable across your LAN, meaning anyone on the same network could burn your speech-to-text and text-to-speech compute for free, and the health check leaked absolute file paths off your machine. I wrote a feature test that drives the real Flask app through the real auth path — no mocks — and watched it fail 3 of 4 checks before the fix and pass clean after. That’s the standard I hold security fixes to: if the test can’t fail first, it isn’t proving anything. Same sweep, the transcript garbage collector turned out to be a permanent no-op — querying a tool_calls/compaction shape the rewritten writer hadn’t produced in ages, with a foreign-key violation in the parallel cleanup path silently swallowed by an overly broad except. Both are now unified on one watermark-based cleanup with the FK ordering fixed, logging its deletion count instead of failing quietly forever.

The second thread was burying a corpse properly. mode_gate_service, its classifier, and a ~800KB pre-trained ONNX head had been sitting in the tree steering nothing — dead weight from an earlier approach to conversation mode detection that never shipped. Deleting it meant tracing every place it was still wired in (privacy config, ONNX inference, user config) and rewiring those call sites clean rather than leaving a stub. Same treatment for a vestigial 4-argument handler ABI in the capability tools — 29 handlers all took (params, telemetry) directly already, so the extra adapter layer was ceremony nobody used. This is the unglamorous half of building fast: every shortcut you don’t leave behind is one less thing that confuses the next person reading the code, including future-me.

The frontend took the biggest single cut: a lean-cleanup pass plus a comment-strip pass landed together for a net -2,361 lines across 53 files, zero behavior change. I don’t trust “behavior-preserving” as an assertion — I verified the comment-only edits were comment-only via transpile-compare across 90 TypeScript and Vue files (byte-identical compiled output), then did an adversarial hand pass over all 46 .vue/.scss files. Inline SVG icons across 25 components were replaced with direct @lucide/vue imports, cutting 200+ more lines of duplicated markup. None of this is visible to you today — it shows up in how fast the next feature ships, since there’s less to wade through and less that can silently drift out of sync.

And because a big refactor day deserves a hard floor: the entire backend — every file touched across 101 commits since the last strict-typing pass — now runs clean under mypy --strict. Zero errors, 473 files, no relax blocks, no # type: ignore lines. Alongside it, the Processes panel got real teeth: you can see every live delegate task Chalie is running in the background, what it’s doing, how long it’s run, and stop it with one click — wired into the same cancellation chain the model already uses.

None of this is the feature you’ll screenshot. It’s why the features after it don’t wobble. Next: the recursive turn-chain work from the day before starts paying off as the interface catches up to it, and pairing a mobile client to your desktop instance is close enough to see from here.

  • Closed an unauthenticated LAN exposure on all three voice routes (/voice/health, /synthesize, /transcribe), proven with a real end-to-end auth test

  • Fixed a transcript-cleanup job that had been silently failing (swallowed foreign-key error) since the writer’s last redesign

  • Deleted a dead mode-detection classifier and its ~800KB pre-trained model, plus a vestigial capability-handler adapter layer

  • Cut 2,361 lines from the frontend via a verified behavior-preserving cleanup and a migration to @lucide/vue icons

  • Brought the entire backend under mypy --strict with zero errors across 473 files, and shipped a live Processes panel for watching and stopping background delegate work