Commit Graph

272 Commits

Author SHA1 Message Date
Frank Song 70b019f710 feat: let extension skins declare base scheme 2026-06-30 18:17:40 +08:00
Frank Song eff01bf322 Gate settled anchor fallbacks by raw index 2026-06-30 08:00:54 +08:00
nesquena-hermes f968de8d35 merge #5155 extension settings surface 2026-06-28 23:07:00 +00:00
Rod Boev a76c794d6d feat(extensions): add browser-local extension settings surface (#5094) 2026-06-28 17:28:20 -04:00
nesquena-hermes a17080d61f Merge branch 'pr-5113-stage' into stage/5113-brand 2026-06-28 20:36:46 +00:00
nesquena-hermes dcf8643093 docs: add desktop icon decision preview images 2026-06-28 08:04:53 +00:00
nesquena-hermes e1091ba977 docs: add brand-icon preview screenshot for PR 2026-06-28 07:52:50 +00:00
Frank Song 90fd37dbd3 Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 15:21:55 +08:00
Paladin173 96766081d8 docs: clarify changelog release ownership 2026-06-28 07:10:21 +00:00
Frank Song 63d69a0c5e Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 14:22:50 +08:00
nesquena-hermes cd71403be9 feat(extensions): TTS-engine registration capability (registerHermesTtsEngine)
Lets a trusted local extension contribute a TTS engine that appears in the
Settings -> TTS Engine dropdown and is used by BOTH playback paths (voice-mode
auto-read in boot.js _speakResponse + the per-message Listen button in ui.js
speakMessage). Mirrors window.registerHermesSkin.

  window.registerHermesTtsEngine({ id, label, synthesize(text, opts) })
    -> Promise<ArrayBuffer|Blob> of audio; core coerces + plays through the same
       <audio> lifecycle as the Edge engine (stop/rearm in voice mode).

Core owns selection, the dropdown <option> (label via textContent, never HTML),
and playback. Built-in ids (browser/edge/elevenlabs) are reserved; invalid
descriptors are rejected; empty/failed synth degrades gracefully. The settings
panel re-adds registered options on render (the markup hardcodes only built-ins).

Unblocks TTS-engine extensions (e.g. a local VOICEVOX engine). Docs in
docs/EXTENSIONS.md; CHANGELOG [Unreleased]. New regression test drives the real
registry via a Node harness (register/validation/reserved-guard/synth coercion).

Full suite: 10893 passed, 0 failed. Verified end-to-end in a live browser with a
companion VOICEVOX extension: engine registers, appears in the dropdown
(browser/edge/elevenlabs/voicevox), and the Listen button routes through the
registered synthesize (both VOICEVOX steps called).
2026-06-28 05:43:02 +00:00
Frank Song c5727846e0 Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 13:30:15 +08:00
nesquena-hermes dfe64313dc theme-registration: preserve pending ext skin across reload + expand accent token allowlist
Found via live browser testing of the first consumer (e-ink skin):
- An extension-registered skin selected by the user was lost on reload: both
  the pre-paint inline script in index.html AND the boot-time appearance sync
  normalized the persisted skin against the (extension-less) known-skins list
  and clobbered localStorage back to 'default' before the extension's
  registerHermesSkin() could run. Both sites now PRESERVE an unknown non-default
  persisted skin (a likely pending extension skin), so it survives the reload
  and the extension re-applies it on load. The server push is skipped for a
  still-pending ext skin.
- Expanded the token allowlist to the full accent family
  (--accent-hover/-text/-bg/-bg-strong/-rgb + --surface-subtle) so a skin can
  fully recolor accent-driven chrome (active rail, chips, profile labels), not
  just --accent. Value regex now also accepts a bare RGB triple for --accent-rgb.

Regression tests added for both preservation sites. Full suite: 10876 passed.
2026-06-28 05:22:26 +00:00
nesquena-hermes 6d8135df3a feat(extensions): theme-registration capability — registerHermesSkin()
Lets a trusted local extension contribute a custom skin to the NATIVE
Settings -> Appearance picker (instead of a parallel theme switcher).
window.registerHermesSkin(descriptor) validates + sanitizes the skin in
core, once, so every theme extension inherits the guard:
- token names restricted to a documented allowlist
- values sanitized against a strict color/dimension regex (rejects url(),
  expression(), semicolons, braces, and other CSS-injection vectors)
- reserved built-in skin keys cannot be overwritten
- descriptor with no valid tokens is refused
- idempotent re-register (updates in place) for a live theme editor

Injects a managed <style> for the :root[data-skin] tokens, appends to
_SKINS + _VALID_SKINS, refreshes the picker, and re-applies a persisted
selection that was waiting on a late-registered skin.

Unblocks theme-pack and theme-creator extensions. Documented in
docs/EXTENSIONS.md. Regression test drives the real registration +
sanitization logic via a Node harness (adversarial input).

Full suite: 10874 passed, 0 failed.
2026-06-28 05:22:26 +00:00
Frank Song d44c5a2f7b Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 13:02:12 +08:00
Frank Song 571db9d81e Document anchor fallback ownership 2026-06-28 12:09:12 +08:00
nesquena-hermes 58ba991c0d feat(extensions): opt-in CSP frame-src allowlist (HERMES_WEBUI_CSP_FRAME_EXTRA)
Adds a configurable frame-src directive so an extension can embed a
self-hosted web app in an iframe (e.g. an 'external app tab' pinning
Grafana / Vaultwarden / a dashboard). Mirrors the existing
HERMES_WEBUI_CSP_CONNECT_EXTRA knob: space-separated http(s) origins with
optional *.subdomain wildcard + port, validated, ignored if malformed
(directive-injection / paths / ws scheme / bad ports all rejected).

- Default frame-src is 'self' only, so existing same-origin iframes are
  unchanged; default-off when the env var is unset.
- frame-ancestors stays 'none' -- this only governs what THIS page may embed,
  never who may embed the WebUI.
- Threaded through both the enforced and report-only CSP builders + the
  server.py Handler wrapper, so both headers stay identical.

Docs in docs/EXTENSIONS.md. 8 new regression tests
(tests/test_csp_frame_src_extra.py) cover default, valid widening, enforced
parity, directive-injection rejection, path/ws/port rejection, and
connect/frame independence. Existing CSP test suite still green.

Note: full suite shows 2 pre-existing order-dependent flakes
(test_issue1574 spawn-context, test_issue3283 import-order) that pass in
isolation and are untouched by this CSP-only diff.
2026-06-28 04:08:04 +00:00
nesquena-hermes e2ade27bfe fix(anchor): preserve settled mixed content order (#4958)
When a settled assistant turn carries _anchor_activity_scene and has mixed
content[] (text interleaved with tool_use), promote that ordering into anchor
scene rows (prose / tool / thinking rows in sequence) instead of only the raw
content[] fallback, and mirror the same scene model in the backend hydration
path so a cold reload reconstructs an identical transcript.

Also fixes: post-tool_use text/thinking in a NON-FINAL assistant message was
silently dropped — only the turn-final assistant's post-last-tool text is the
"final answer" tail; earlier assistant messages keep post-tool content as
activity rows. Tail thinking is excluded from the final-answer text but still
emitted as a thinking row (verified on both JS and Python paths).

Tool-row de-duplication is conservative — ID-equal always merges; name/
invocation matches are cardinality-gated and same-message; body-prefix is only
a secondary confirmation inside the id-flexible branch (also requires matching
started_at + compatible name + invocation). It biases toward an extra visible
tool card over ever silently merging two distinct calls.

Gate: Codex SAFE (tail-thinking edge verified already-handled both sides),
Opus SHIP (dedup fails toward duplicate-not-loss; non-final preservation, XSS,
non-mixed regression all clean), full suite 10778 passed (only the 4 known
pre-existing flakes). One non-blocking follow-up noted: bring the prose/thinking
peer-dedup into JS↔Python parity (cosmetic cold-reload extra-narration row).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 22:54:29 +00:00
nesquena-hermes 11260c8df5 feat(extensions): show post-install next steps in the gallery (#4964)
Renders generic registry post_install guidance + lifecycle requirements in the
Settings extensions gallery card (manifest-driven, no vendor special-casing).
Fixes #4959. Rebased onto current master (squash-applied #4964's net diff so it
coexists with the #4979 sidecar-runtime-status feature that landed in v0.51.697).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 05:44:10 +00:00
Frank Song d2eedd85f8 Show extension sidecar runtime status 2026-06-27 05:12:50 +00:00
nesquena-hermes 0562bdbd31 fix(#4933): one-click extension install works with no env (bootstrap managed default root)
Settings -> Extensions offered an Install button even when extensions were
unconfigured, then dead-ended on 'Extensions not configured' because
install_extension required HERMES_WEBUI_EXTENSION_DIR to be set to an existing
directory. Default to a WebUI-managed root under STATE_DIR/extensions and
bootstrap it on first install so install just works with zero config; the env
var remains an optional override (never auto-created). Gallery-installed
extensions already auto-load via the runtime manifest, so the extension loads
on the next app-shell render with no further setup.

- api/extensions.py: _default_extension_root + _writable_extension_root;
  _extension_root/_extension_root_status fall back to the managed default;
  install_extension bootstraps via _writable_extension_root; dir-unavailable
  warning only fires for an explicitly-set-but-broken env path.
- tests: update disabled-by-default expectations (configured=True, valid=False
  pre-install) + new regression proving install bootstraps + auto-loads with
  no env.
- docs/EXTENSIONS.md: document zero-config one-click install + env now optional.
- CHANGELOG [Unreleased].
2026-06-25 18:45:09 +00:00
Frank Song 5f17ba55ed Fix gallery-installed extension activation 2026-06-25 07:10:17 +00:00
nesquena-hermes c888dd6a79 stage #4637 (santastabber): allow toggling installed extensions (extensions phase-2) (#4649)
UI enable/disable of installed manifest extensions via a bounded fail-safe
extension-overrides.json state file. Safe boundary: no manifest edits, no asset
fetches, no sidecar proxy, no extension-owned routes. /api/extensions/toggle is
auth+CSRF gated; anchored _EXTENSION_ID_RE (no path traversal); atomic UTF-8 write
under _EXTENSION_STATE_LOCK; 32KiB/512-id DoS bounds; disabled override suppresses
asset/sidecar injection end-to-end. Nathan greenlit the extensions-phase-2 concept.
Code byte-faithful from PR head 8769b4a390. 49 own tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 19:15:54 -07:00
nesquena-hermes 0fd9bf3c37 Release v0.51.564 — loopback sidecar diagnostics (#4612) (#4632)
* stage #4612 (santastabber): surface loopback sidecar diagnostics + CHANGELOG

Settings -> Extensions surfaces manifest-declared loopback sidecar companion services
(type:loopback, http(s) on 127.0.0.1/localhost/::1 only, sanitized origin+health_path)
as read-only health cards w/ a browser-side credentials-omitted health probe. Backend
hard-whitelists scheme + loopback host, rejects userinfo/path/query/traversal, rebuilds
output from parsed components (no echo of rejected input); frontend esc()'s every field,
whitelists badge status, fetch never reads body. Purely additive to GET /api/extensions/status.
Code applied byte-faithful from PR head 7080929f37. 33 own tests pass.
Nathan concept-approved (loopback-companion direction).

Co-authored-by: santastabber <santastabber@users.noreply.github.com>

* fix #4612 Codex gate findings: reject encoded query/fragment in health_path + align CSP loopback allowlist

Codex SHIP-WITH-FIXES (2 reproduced SILENT findings):
1. _normalize_sidecar_health_path percent-decoded AFTER the raw query/fragment ban,
   so /health%3Ftoken=abc -> ?token=abc survived into the probed URL. Now re-rejects
   decoded ? and # + regression test (%3F/%23 skipped with sidecar_health_path_rejected).
2. The origin validator accepts https:// and [::1], but CSP connect-src only listed
   http 127.0.0.1/localhost -> accepted https/IPv6 loopback sidecars would be CSP-blocked.
   Aligned _CSP_CONNECT_BASE (added https v4/localhost + http(s) [::1]) + updated the 4
   exact-string CSP tests + a regression test asserting all loopback forms are allowed.

Co-authored-by: santastabber <santastabber@users.noreply.github.com>

* Release v0.51.564 — Release TW (loopback sidecar diagnostics; #4612)

* fix(#4612): drop browser-invalid [::1]:* CSP source (browser-smoke fail)

The CSP-loopback-alignment fix added http(s)://[::1]:* to connect-src, but CSP
host-source grammar can't express a port wildcard on a bracketed IPv6 literal —
Chromium rejects '[::1]:*' as an invalid source, which browser-smoke flags as a
console error (CI fail). Keep the valid https://127.0.0.1:*/localhost:* additions;
drop the IPv6 wildcard entries. IPv6 [::1] sidecars are still accepted by the
validator (displayed in the card); their browser health probe surfaces as
'blocked' under CSP unless an operator adds a specific-port [::1]:<port> via
HERMES_WEBUI_CSP_CONNECT_EXTRA. Tests updated to match + assert [::1]:* absent.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 13:07:32 -07:00
nesquena-hermes 11625aac1a fix(bootstrap): discover agent installed as root (FHS) or behind the bash launcher
A fresh root install on Linux (e.g. a new Proxmox/Ubuntu VM) places the Hermes
Agent at /usr/local/lib/hermes-agent and links the CLI into /usr/local/bin,
while the modern `hermes` launcher is a `#!/usr/bin/env bash` wrapper that
exec's the venv entrypoint. The old discovery only checked
~/.hermes/hermes-agent-style paths and parsed the launcher's shebang as a Python
interpreter, so on a root VM discover_agent_dir() returned None, bootstrap built
a WebUI-only .venv, and chat aborted at launch with "Python environment cannot
import both WebUI dependencies and Hermes Agent."

Two complementary fixes:
- Add /usr/local/lib/hermes-agent to discover_agent_dir()'s candidate list.
- Harden _agent_dir_from_hermes_cli() to follow the launcher's exec target (any
  absolute path quoted in the wrapper) up to run_agent.py, not just a Python
  shebang. Skips /usr/bin/env indirection.

Now `python3 bootstrap.py` works out of the box for root/container installs with
no HERMES_WEBUI_PYTHON override needed. Adds tests for the bash-wrapper shape,
the env-indirection no-false-positive case, and the FHS candidate probe; updates
docs/troubleshooting.md.
2026-06-21 16:55:40 +00:00
nesquena-hermes 813e5aa3b2 stage #4588 — extensions settings diagnostics panel 2026-06-21 07:10:05 +00:00
nesquena-hermes 8f3f6ea67a stage #4593 onto master (v0.51.550) — document extension sidecar CSP support + CSP test coverage
santastabber: docs (README + EXTENSIONS.md) clarifying extension sidecar CSP behavior +
test_issue1909_csp_enforcement / test_issue2901_csp_connect_extra assertions. Docs+tests only, no
prod code. Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 07:04:31 +00:00
nesquena-hermes 1b7c1fe619 rebase #4569 onto master (v0.51.547) for ship-gate — extension status diagnostics (#4561 follow-up)
santastabber: authenticated /api/extensions/status endpoint reporting WHY an extension didn't load
(stable codes + coarse sources only, no path/env/URL leak). Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 04:08:44 +00:00
nesquena-hermes 3e229141af stage #4561 + stamp v0.51.545 — extension manifest asset bundles
santastabber. HERMES_WEBUI_EXTENSION_MANIFEST: bundle extension assets via a 64KB JSON manifest
instead of long URL lists. Same same-origin allowlist validator, traversal-guarded manifest path,
default-off. Nathan blessed (richer extension system). Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 00:37:08 +00:00
nesquena-hermes 6951d10ae5 rebase #4411 onto master (v0.51.542) for deep-review — Stable Assistant Turn Anchors capstone
franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-20 21:48:55 +00:00
nesquena-hermes 8d56dcdbed stage #4491: multi-container gateway URL (#4483) + v0.51.517 CHANGELOG 2026-06-19 19:09:00 +00:00
Frank Song 409fb59760 Add assistant turn anchor renderer snapshot adapter 2026-06-14 02:03:45 +08:00
Frank Song c52c832d05 Add assistant turn anchor dual-run reconciler 2026-06-14 01:04:29 +08:00
Frank Song aa96bc52bc Add assistant turn anchor live shadow feed 2026-06-13 16:26:52 +08:00
Frank Song 59c2ccd9cf feat(anchor): project activity scene rows 2026-06-13 13:23:27 +08:00
Frank Song b990bd09dd feat(anchor): project settled final answer through anchor 2026-06-13 13:23:15 +08:00
nesquena-hermes dc909043cf Merge branch 'master' into franksong2702/stable-assistant-turn-anchors-slice3-registry 2026-06-12 19:41:20 -07:00
nesquena-hermes 9f1933f81e chore(#4024): drop docs/ui-ux/review-shots/ trifecta scratch from the release diff
These 5 PNGs (722K) are the agent trifecta review-pass screenshots copied into
the worktree so the Opus gate could read them — review scratch, not durable UX
evidence. The intended evidence lives in docs/ui-ux/evidence/pr4024-*.png and is
kept. Mirrors the .review-crops/ cleanup; gitignore the dir so review scratch
can't leak into a release diff. No code or test change.
2026-06-13 02:15:01 +00:00
nesquena-hermes 5dad3e03e5 test(#4024): update CSS-literal assertions to the new quiet treatment + add 8 trifecta-fix regression guards
- Re-anchor test_old_event_fading + test_transparent_event_row_quiet_metadata_visual_rhythm to the new contract (left-rail output, inline-row args, underline active tab, WCAG fade floor, live-only fade).
- Add guards: live-turn-restore rehydrate (C1), echoed-thinking dedup (O-Bug1), thinking-card flat reset (V1), skin-proof reset (V3), legible failed status (V2), interrupted settled status (O-Edge), completion preserves expand state (O-Bug2), live-only entrance animation (V9).
2026-06-13 01:12:09 +00:00
nesquena-hermes ed3ac13fc5 fix(#4024): trifecta UX + correctness pass — rehydrate live-restore, thinking dedup, quiet thinking rows, legible failures, skin-proof reset, compact args, flat output, live-only fade/anim
Functional (Codex+Opus trifecta findings):
- C1: restoreLiveTurnHtmlForSession now calls _rehydrateTransparentStreamDom (controls were dead after active-session live-turn restore).
- O-Bug1: per-turn echoed-thinking dedup in the transparent settled path (mirrors the compact seenReasons), fixes duplicate out-of-order Thinking rows.
- O-Bug2: tool completion preserves the row's open state + Full/Output tab.
- O-Edge: settled/reloaded done===false tools show 'Interrupted' (not a permanent Running shimmer); progress bar clears for Interrupted too.

Visual (trifecta CSS):
- V1: reset .thinking-card to flat/transparent (was the heaviest object in the stream).
- V2: failed=error color + left border, running/interrupted legible (was an invisible muted badge).
- V3: scoped !important reset beats the 6 per-skin .tool-card re-card rules.
- V4: inline key:value args (was a tall stacked form).
- V5/V7: output pre flattened to a quiet left-rail, max-height:none (one expanded zone, no nested scroll).
- V6: drop redundant tool/status pseudo-args from the detail body.
- V8: row fading gated to the live turn only + floor raised for WCAG; settled history stays full-opacity.
- V9: entrance animation scoped to #liveAssistantTurn (was replaying on every renderMessages).
- V10: larger mobile header tap targets + active detail-tab underline.

O-Bug3 (live thinking overwrite when no seq) intentionally deferred — documented follow-up; settled render is already correct and a live heuristic risks a streaming regression.
2026-06-13 01:08:41 +00:00
nesquena-hermes 92f7a24448 Merge #4024 Transparent Stream onto master (rebase)
# Conflicts:
#	CHANGELOG.md
2026-06-13 00:40:17 +00:00
Frank Song d516f9dd00 fix(anchor): harden registry contract before wiring 2026-06-13 00:44:26 +08:00
Rod Boev 010127d0c0 docs(onboarding): place AIML setup with custom-provider guidance (#4045) 2026-06-12 12:41:43 -04:00
Rod Boev b426067e6f docs(onboarding): document AIML API custom-provider setup (#4045) 2026-06-12 12:35:59 -04:00
Mohamed Habib a84bd8908d docs: add transparent stream ux evidence 2026-06-12 12:54:30 +03:00
Frank Song 53051ad4a3 fix(anchor): harden registry review findings 2026-06-12 16:15:09 +08:00
Frank Song 65cb2a8b46 feat(anchor): add shadow source registry harness 2026-06-12 15:52:28 +08:00
Frank Song 56a89ef11e feat(anchor): add inert turn registry skeleton 2026-06-12 15:52:28 +08:00
Frank Song 36123628d5 feat(anchor): add assistant turn source normalizer 2026-06-12 13:57:54 +08:00
nesquena-hermes 48860418c9 Release LW stage — v0.51.359 (#3962 assistant turn anchor phase 0 scaffold) (#3977)
* feat(anchor): add stable assistant turn phase 0 scaffold

* chore: stamp v0.51.359 — Release LW (assistant turn anchor phase 0 scaffold, #3962)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <agent@local>
2026-06-10 23:14:18 -07:00