Release v0.50.265 — opt-in WebUI extension hooks
Single contributor PR + 4 Opus pre-release follow-ups.
What ships
feat
- #1445 Opt-in WebUI extension hooks — @ryansombraio — adds a deliberately-small, self-hosted extension surface for administrators who want to inject local CSS/JS into the WebUI shell without forking the core repo. Disabled by default; activates only when
HERMES_WEBUI_EXTENSION_DIRpoints to an existing directory. Three env vars (HERMES_WEBUI_EXTENSION_DIR,HERMES_WEBUI_EXTENSION_SCRIPT_URLS,HERMES_WEBUI_EXTENSION_STYLESHEET_URLS) expose the surface. New/extensions/...route is auth-gated (NOT inPUBLIC_PATHS, unlike/static/...). URL validation rejects external schemes, protocol-relative URLs, fragments, traversal (raw + percent-encoded + double-encoded + quadruple-encoded after Opus follow-up), control characters, quotes, and angle brackets. Filesystem serving sandboxes paths viaPath.resolve()+relative_to(), rejects dotfiles, dot-directories, encoded backslashes, and symlink escapes. CSP unchanged — extensions live at same origin. 7 regression tests intests/test_extension_hooks.py. Documentation indocs/EXTENSIONS.mdcovers extension authoring + trust model.
Opus pre-release advisor follow-ups
_fully_unquote_pathiteration cap raised from 3 to 10 — quadruple-encoded..now correctly collapses to literal..and is rejected by the validator (defense in depth; not previously exploitable but a contract violation).- Trust-model callout at top of
docs/EXTENSIONS.md— moved the strongest warning ("extensions execute with full WebUI session authority") from middle of doc to a blockquote callout right after the lead paragraph. Adds explicit "do not point at user-writable directory" guidance. - URL list cap (32 entries) + reject-URL logging — caps configured URL lists to avoid pathological rendering. Logs one-shot WARNING per process for each rejected URL so misconfigurations are visible.
- MIME map expansion — adds
ttf(font/ttf),otf(font/otf),wasm(application/wasm) so modern fonts and WebAssembly modules serve correctly (Chrome refuses.wasmastext/plain).
Stage-only test harness
- Test-only fix:
tests/test_extension_hooks.py::test_extension_route_remains_behind_webui_authSimpleNamespace neededquery=""(auth.py accessesparsed.querysince v0.50.258). - 5 regression tests in
tests/test_pr1445_opus_followups.pypin all Opus follow-up invariants.
Verification
- Full pytest:
3705 passed, 2 skipped, 3 xpassed, 0 failedin 86s on the stage branch. - Browser sanity: 20 + 11 API checks green.
- Opus pre-release advisor: SHIP verdict on the merged stage diff. Two pre-merge nits + three nice-to-haves all applied in stage.
- Security audit (deep check, see brief): path validation correct, encoded traversal blocked at all encoding depths, dotfile/symlink rejection works, extension route auth-gated (verified via server.py:76), CSP unchanged.
Constituent merges (no-ff, attribution preserved)
4ee9368 Opus pre-release follow-ups for PR #1445
73cb3c1 stage-265: test fix + CHANGELOG for v0.50.265
3de70c5 Merge PR #1445: feat: add opt-in WebUI extension hooks
9de61a0 feat: add opt-in webui extension hooks
Stage rebased cleanly on fb66ba5 (current master, post-v0.50.264). Original PR was 12 releases behind master at fork point; rebased clean with no conflicts.
Stats
- Files: 6 (api/extensions.py, api/routes.py, README.md, docs/EXTENSIONS.md, tests/test_extension_hooks.py, tests/test_pr1445_opus_followups.py, CHANGELOG.md)
- Diff: ~860 LOC added (624 from contributor + ~236 from Opus follow-ups + tests)
- Tests added: 12 (7 from #1445 + 5 from Opus follow-ups)
Closes
Author credit
--no-ff merge preserves original authorship and timestamp for @ryansombraio. CHANGELOG explicitly credits the contributor.