Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
## Unreleased

### Security: the Werkzeug debugger no longer follows you onto the network (2026-09-10)
- **Why:** `_run_server` called `app.run(..., debug=True, ...)` unconditionally, and two defaults turned that into real exposure rather than a theoretical one. `--debug` is `store_true, default=True`, so dev mode is what you get unless you ask for `--no-debug`; and the startup banner advertises the LAN and public URLs, so reaching the dashboard from another machine means adding `--host 0.0.0.0`. Do both — which the product invites — and any unhandled exception serves Werkzeug's interactive traceback page, with source, local variables and a PIN-gated eval console, to whoever reached the port. This is bandit B201, HIGH.
- **What:** the debugger is enabled only on a bind proven to be loopback. `helpers/server.is_loopback_host` fails closed — anything it cannot positively prove is loopback reads as remote — and accepts `localhost`, IPv4/IPv6 loopback literals, a bracketed `[::1]` and a zone-suffixed form. `0.0.0.0` and `::` are wildcard binds covering every routable interface and are correctly NOT loopback. Hostnames are never resolved to decide this: a name that resolves to loopback today can resolve elsewhere tomorrow, and DNS is not a thing to trust when the answer decides whether to expose an eval console.
- **The reloader is not sacrificed.** `use_reloader=True` stays unconditional and is separately pinned by a test. Auto-reload is what dev mode is *for*, and a fix that quietly removed it would cost the feature it was protecting.
- **The guard did not cover the thing it was guarding.** The original 22 tests all exercised the helper in isolation and none exercised its use, so restoring `app.run(debug=True)` left the whole suite green. `dashboard.py` is now parsed and the `app.run()` call inside `_run_server` asserted structurally: `debug` is not a hardcoded `True`, it is a name bound from a loopback check in that same function, and `use_reloader` is unconditionally `True`. Both regressions proven red first — `debug=True` (the original bug) and `debug=False`, which passes a naive "not True" check while removing dev mode's debugger for everyone.
- **The status line is not the control.** Restoring `debug=True` left the note still printing "debugger off" while the debugger was on: the message and the flag are computed independently. That is now a recorded contract, so no future guard accepts the banner as evidence of the behaviour.
- **Verified live**, one fresh process per host, intercepting what `_run_server` actually hands `app.run`: `127.0.0.1`, `localhost` and `::1` keep `debug=True`; `0.0.0.0`, `192.168.1.50` and `::` get `debug=False`, all six with `use_reloader=True`. The same probe on the pre-fix tree returns `debug=True` for `0.0.0.0` and `192.168.1.50`.
- **Recorded in the product record.** The Local Observability Service blueprint now carries the constraint as five contracts plus an ADR on asserting the call site rather than only the helper; `helpers/server.py` points at it. Also dropped an `import ipaddress` in `dashboard.py` that the helper made redundant.
- **Verified:** 25 tests in `tests/test_debugger_loopback_only.py`, named in `ci.yml`. Carries #5382.

### Fixed: every OpenAI session reported the wrong cache-hit rate (2026-09-10)
- **Why:** the two providers count cached tokens differently and one denominator was applied to both. Anthropic reports cached tokens ON TOP OF uncached input, so the rate is `cr / (in_t + cr)`. OpenAI reports them ALREADY INSIDE `input_tokens`, so the rate is `cr / in_t`. Using the additive form for OpenAI understates every figure, and not by a rounding margin: on a session with 100,000 input and 80,000 cached, the true answer is **80.0%** and the old formula returned **44.4%**. That reads as "caching is barely working" for a session where it was working nearly perfectly, which is the kind of number a reader acts on.
- **What:** `_session_cost_intel` resolves the provider from the model through `providers_pricing.provider_for_model`, the one place model-to-provider is decided, so this cannot drift from pricing. OpenAI takes the inclusive denominator, every other provider the additive one, and a provider that cannot be resolved degrades to the additive form, which is the majority case and the historical behaviour.
Expand Down
Loading