Two CI-red issues, both fixed:
1. Lint (F401): the new tests/test_session_unread_dot_on_visit.py imported
`re` but never used it. Dropped the import.
2. Test hang/timeout: loadSession() now routes its viewed-count/unread clear
through the new _acknowledgeSessionVisit() helper, but the PRE-EXISTING
tests/test_cross_session_message_load_isolation.py harness (which extracts
loadSession and stubs its deps) didn't define it — so the extracted
loadSession broke and the node driver hung to the 60s pytest-timeout.
Stubbed _acknowledgeSessionVisit + _sessionVisitHasUnreadState to no-ops
there, mirroring the pre-existing _setSessionViewedCount /
_clearSessionCompletionUnread stubs they replaced. That harness covers
cross-session load ordering + stale-reject, not unread-dot state, so a
no-op stub preserves its intent; the real helper is exercised by the PR's
own test_session_unread_dot_on_visit.py.
Verified: ruff forward gate clean; the timeout test passes in ~1s; the PR's
unread-dot suite 7/7.
Salvaged from #4946 by @neaucode-bot. Rebuilt fresh on master (repro-confirmed),
without regressing hidden-tab completion state or lineage-parent unread dots.
Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
CI ruff gate flagged 3 new violations: two api/routes.py KeyError re-raises in an
except-KeyError block (add 'from None') + an unused importlib import in the share
security test. All trivial.
The re-gate found the always-on sanitizer still had two str()-on-dict paths:
a list text-block whose 'text' is itself a dict (via /api/session/import), and
a dict-valued session title. Both are now type-checked to str; a dict title
falls back to 'Untitled'. Regression cases added. Co-authored-by: MicoRobot.
Adversarial security re-gate on the rebased head found 4 issues; fixed:
1. [CORE] api/shares.py public snapshot could leak credentials/paths/tool
payloads. The sanitizer relied on redact_session_data (gated on the
user-toggleable api_redact_enabled) — disabling it exposed API keys, a dict
assistant content was stringified as raw tool data, and session.workspace
survived in transcript text. Now the per-message sanitizer applies ALWAYS-ON
credential redaction (_redact_fn_cached, force=True, ignores the setting) +
scrubs known session/workspace/home paths, and refuses to stringify non-text
structured content. Title is force-redacted too. Regression test proves the
boundary holds with api_redact_enabled=False.
2. [SILENT] api/models.py: my rebase wrongly kept anchor_activity_scenes in
METADATA_FIELDS, moving 250-480KB scene bodies before messages and undoing
master's metadata fast-path (#5854). Removed; 17 #5854 tests pass again.
3. [SILENT] static/index.html: the Share/StopSharing/ExportHTML button triplet
was double-applied by the rebase (duplicate IDs → only the first wired).
Deduped to one each + a count assertion test.
4. tests: fixed the stale Session.load-after-/api/session/new setup (master
keeps new sessions memory-only until first message).
Also (UX, found in the visual flow sweep): a clipboard-copy failure after a
SUCCESSFUL share showed a red 'Share failed' toast and skipped opening the page.
The copy is now isolated — the share succeeds, the page opens, and the toast
shows the link if the clipboard write fails, never a false failure.
Co-authored-by: MicoRobot.
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).
Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
Two non-blocking UX improvements from the Fable gate on #5739:
1. Full date+time tooltip (title attr) on the worklog event-time label — the bare
clock is date-ambiguous when a settled session is reviewed later or a run crosses
midnight. New _activityFullClockLabel (range-guarded so a bad epoch never yields
'Invalid Date' in the tooltip) + test.
2. Hide the time label under 420px — its ~50px of fixed chrome squeezes the
collapsed row's flexible preview toward zero on phones.
Co-authored-by: rodboev.
Two gate findings on the worklog-event-timestamp feature (#5700/#5739):
1. Thinking-row timestamps disappeared after settlement. Reasoning source events
carry no server timestamp; the live DOM showed a fallback time but the settled
scene row rebuilt with created_at:null. Now stamp created_at on the source event
the first time it's seen (messages.js apply path) and carry row.created_at
through snapshot hydration, so the timestamp survives live->settled.
2. A garbage huge numeric timestamp (e.g. 1e20) passed the finite/>0 checks and
rendered literal 'Invalid Date'. _timestampSeconds now rejects epochs outside
JavaScript's valid Date range. Added a regression test.
Co-authored-by: rodboev. Gate findings from adversarial Codex re-gate.
Codex round-3 finding #2 [CORE]: when reconciliation replayed the active run's
terminal event at the subscribe cutoff, the live queue's copy hit the cutoff/dedup
'continue' BEFORE the terminal 'break' check — so the handler never broke, stayed
blocked on the dead run's queue, and missed subsequent session runs. Check the
terminal event type before the already-delivered continue: a terminal always ends
the loop (after emitting if not already sent). Regression test asserts the handler
unsubscribes (doesn't hang) when the terminal is deduped. 24/24 SSE tests green.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Final-gate Codex caught a TOCTOU race in the prior idle-seam fix: the fingerprint
baseline was captured AFTER the first attach_active_stream() lookup, so a run
completing DURING that first attach folded into the baseline and was silently
missed (only : keepalive emitted, no snapshot). Move the baseline capture before
the first attach so any completion from that point on is detected. Add an ordering
regression test (baseline fp precedes the idle-loop attach + advance still emits
one snapshot). 23/23 SSE tests green.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Fable gate found a seam Codex's 3 boundary findings didn't cover: an idle no-cursor
subscriber to /api/sessions/{id}/events misses a run that starts AND finishes inside a
single keepalive tick — the wait loop only looked for a live in-memory STREAMS entry to
attach, never noticing the journal advanced, so the run's transcript was silently missing
until manual refresh.
Add a cheap bounded session_journal_fingerprint() (file-count/max-mtime/total-size from
stat only, never parses bodies) and, inside the idle attach-wait loop, emit a session_snapshot
re-sync whenever the fingerprint advances with no live stream to attach — the same honest
recovery contract used for a failed reconciliation. Re-baselines after each re-sync so a quiet
idle connection never spams snapshots. 2 regression tests; 22/22 SSE tests green.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
The pricing3-null case failed on one CI shard with StopIteration: pytest-shard's
partitioning interleaves this test with modules that mutate config.cfg away from
the openrouter active provider, so get_available_models() omits the openrouter
group and next(...) raised. Make the group lookup resilient (next(..., None) +
pytest.skip) — the exact defensive guard the sibling live-fetch tests in this
file already use for the same pollution. Passes both with and without hermes_cli.
Co-authored-by: igorroncevic <igorroncevic@users.noreply.github.com>
The new test_openrouter_free_tier_pricing_fails_closed did an unguarded
'from hermes_cli import models', but hermes_cli (the agent package) is an
optional dependency not installed in the WebUI CI test environment, so the
test failed with ModuleNotFoundError on 4-5 shards (master green, PR head red).
Wrap the import in the same try/except guard the sibling tests in this file
already use — when hermes_cli is present we force its live-fetch to empty so
the fail-closed static path is exercised; when absent, that static path is
already the only one and the api/config.py fix under test still runs via the
mocked urlopen. Verified: 10/10 with hermes_cli, 5/5 with it blocked.
Co-authored-by: igorroncevic <igorroncevic@users.noreply.github.com>
Auxiliary-model task definitions were maintained in BOTH Python and JS and could
drift (the retired session_search task lingered in one). Centralize to a single
backend catalog that panels.js renders+saves from exclusively; purge the retired
slot on reset while preserving other custom bindings; old configs still load.
Gate-pass: Codex SAFE (only session_search removed, 11 other tasks + defaults
unchanged, 22 keys match across 15 locales, legacy migrate preserves custom
bindings, executable DOM harness rendered all rows). 53 own + 887 regression green.
Co-authored-by: t <a@b>
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
* fix(updates): scope installed-version fallback to webui + verified current_sha (#5864 gate fixes)
Gate (Codex) found 2 CORE in the #5862/#5864 experimental-version-display fix:
1. api/updates.py:947 — the installed-version fallback ran in shared _check_repo_release
for BOTH repos, injecting WEBUI_VERSION (v0.52.0, not an Agent ref) into Agent payloads.
Now scoped to name=='webui'.
2. api/updates.py:996 — current_sha reused current_version_display, but WEBUI_VERSION can be
vX.Y.Z-dirty-<hash>/-N-g<sha>/bare-SHA/unknown (not git refs) -> broken /compare link +
lost update-summary subjects. current_sha now derives from a git-VERIFIED exact-match tag
(channel-neutral) or None; current_version keeps the real display string.
+2 regression tests (agent-repo-no-inject, dirty-HEAD-verified-sha). 21/21 update-channel green.
* changelog: #5864 experimental-channel version display fix (#5862)
---------
Co-authored-by: t <a@b>
Reasoning-heavy sessions accumulate 250-480KB of anchor_activity_scenes serialized
BEFORE the messages array, so the 64KB metadata-prefix read always overflowed ->
full-parse fallback on every read -> memory growth. Move scene data AFTER messages;
metadata prefix now ~2KB, modern reads skip the full parse. Old-layout files still
load (one authoritative parse then cached); round-trip preserves scenes exactly.
Independent review: nesquena APPROVED (end-to-end, on head). Gate-pass: Codex SAFE
(933KB legacy sidecar loads clean + round-trip verified), 17 own + 2945 regression green.
Co-authored-by: t <a@b>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
On .docx/.xlsx/.pptx upload, write a best-effort <name>.md sidecar with extracted
text (reuses api/office_documents.py — no new dep). Path-contained (O_NOFOLLOW
openat-walk within workspace), collision-safe (O_CREAT|O_EXCL never overwrites an
existing .md), best-effort (sidecar failure leaves the Office file intact, partial
cleanup on failure). Nathan concept-approved.
Gate-pass: Codex SAFE (containment + exclusivity + best-effort all verified), 27 own
+ 1443 upload/workspace regression tests green.
Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(security): trusted-proxy-verified local-origin gate (maintainer fix of #5764)
Rebuild _onboarding_request_is_local with a single, symmetric trust model that
closes the embedded-terminal local-gate bypass AND fixes the direct-LAN lockout
regression from the contributor's #5764 attempt (bounced with these findings):
1. SPOOF CLOSED: a forwarded client IP is honored ONLY when the un-spoofable raw
socket peer is a trusted proxy (loopback, or HERMES_WEBUI_TRUSTED_PROXY_CIDRS).
A public direct client sending X-Forwarded-For: 127.0.0.1 is judged on its raw
public peer -> DENY. New _raw_peer_is_trusted_proxy + _forwarded_client_ip_from
_trusted_proxy consume the FULL XFF chain (get_all, repeated headers), walk it
right-to-left skipping trusted hops, and fail closed on empty/blank/garbage.
2. NO LAN LOCKOUT: a direct loopback/LAN client with no proxy header stays local
(onboarding, first-password/passkey, passwordless terminal keep working).
3. COMPAT: TRUST_FORWARDED_FOR=1 stays the opt-in that consults the chain; the new
TRUSTED_PROXY_CIDRS only WIDENS trust to a non-loopback proxy (loopback always
trusted). Shipped private-peer-with-forwarded-header deny nuance preserved.
Updated the one existing test that encoded the vulnerable behavior; added a
21-case adversarial truth-table regression (tests/test_cvd3_terminal_local_origin_gate.py).
66 gate tests + 720 auth/terminal regression pass.
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
* fix(security): family-aware trusted-proxy membership (IPv4-mapped-IPv6)
Codex re-gate CORE: a mapped-IPv6 address (::ffff:10.9.9.9) never matched an
IPv4 CIDR allowlist via 'addr in net' -> (1) legit proxy treated as untrusted =
lockout; (2) mapped trusted HOP in the XFF chain mis-returned as client -> a
preceding public client admitted to the terminal gate. New _ip_in_networks
helper checks addr AND addr.ipv4_mapped; both the peer check and the chain-walk
_is_trusted_hop route through it. +3 mapped-IPv6 regression cases (incl the
public-client-hidden-behind-mapped-trusted-hop security case).
---------
Co-authored-by: t <a@b>
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>