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.
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>
Codex round-3 finding #1 [SILENT]: baselining after end_sse_headers() left a window
where a run completing between header commit and baseline was absorbed into the
baseline + silently lost. Move resume-id parse + fingerprint baseline before
send_response/headers (both side-effect-free: header read + stat-only fingerprint).
Codex round-3 also flagged finding #2 [CORE] (reconciliation terminal-event-at-cutoff
dedup keeps the handler attached to a dead run queue) — NOT addressed here; escalating
to Nathan per Codex-depth diminishing-returns (3rd round into the same SSE seam class).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Codex re-gate found the baseline was still after the preliminary
_active_run_stream_for_session() lookup + the initial cursor read_session_run_events()
replay — a run completing during either window folded into the baseline and was
silently missed. Move the fingerprint baseline to immediately after resume_event_id
parsing, before both operations, so no pre-baseline completion window remains. Remove
the now-redundant later baseline. 23/23 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>
Codex re-gate caught 2 defects in the prior UX commit:
1. streaming.py:8921 — the disclosure fired for ANY process_wakeup failure (e.g.
rate-limit) even when no pause was recorded -> falsely claimed 'retries paused'
while process_wakeup_pause stayed empty. Now gated on the record_* return value.
2. streaming.py:10130 — the raised-exception credential_pool_empty path (exercised
by the existing agent test) recorded the pause but omitted the disclosure. Added
the same conditional disclosure there.
Both paths now keep _error_payload['hint'] in sync with the persisted bubble, and
the disclosure appears only when a pause was actually recorded. 69 tests green.
Fable UX-gate follow-up on #5732: after the first credential-exhausted error,
automatic wakeups are silently suppressed. Disclose it in the wakeup-source
error card ('Automatic retries for this conversation are paused until you send
a message, switch the model/provider, or fix the credentials.') so the silence
reads as intentional, not a stuck agent — Gmail/Dropbox account-needs-attention
convention. Scoped to the process_wakeup + credential_pool_empty seam only.
Co-authored-by: franksong2702 <franksong2702@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>
Fixes the #5831 brick: "WebUI sessions (N)" chip showed a count but an empty list.
Server is_cli_session_row classified read-only external_agent imports (Claude Code,
Codex) with a real title as non-CLI (counted under webui_session_count), while the
client _isCliSession filed them in the CLI bucket -> mismatch. Server now classifies
external_agent/external-agent as CLI, matching the client. WebUI/messaging/subagent
unaffected (their exclusions precede the new branch); external_agent imports stay read-only.
Self-built (nesquena-hermes); independent nesquena review APPROVED on this head (17:28);
gate-pass (Codex SAFE, no over-capture, caller audit clean), 14 own + 180 regression green.
Co-authored-by: t <a@b>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
WebUI hard token-cap on protected-tail tool-result payloads in the MODEL CONTEXT
only, applied once after a confirmed compression event, to prevent post-compression
context blowup. Deep-copy content-replace only (no message removal/reorder → no
orphaned tool_result); visible transcript (s.messages) untouched.
Deep-review re-gate: Codex SAFE (pairing-safety + context-only + idempotent verified
by reproduction), 65 own + 992 regression tests green. Crown-jewel streaming path.
Co-authored-by: t <a@b>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>