Commit Graph
2757 Commits
Author SHA1 Message Date
alfred-rootson b71c88f292 fix(workspace): preserve remote POSIX paths across Session & streaming callsites (#7131) (#7168)
Closes the remote POSIX workspace path preservation gap across Session and streaming. Profile-aware runtime path resolution.

Co-authored-by: alfred-rootson <alfred.wojtasordzik@gmail.com>
2026-09-15 15:37:11 -07:00
94fd2da830 Release: semantic fingerprint for the Codex catalog dependency (#7556) (#7558)
* fix(config): semantic fingerprint for the Codex catalog dependency (#7540)

* fix(#7556): guard the recursive codex-cache strip against RecursionError

Codex found a valid-but-deeply-nested models_cache.json crashes
_codex_models_cache_fingerprint with RecursionError (the recursive strip ran
outside the fallback try), 500ing /api/models. Move _strip_volatile_codex_cache_fields
inside the existing encode try/except so a transform failure degrades to the
stat-based fingerprint. +1 mutation-proven regression (forces the strip to raise;
fails with RecursionError if the guard is reverted).

* docs(changelog): stamp #7556 (semantic fingerprint for Codex catalog dependency)

---------

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-13 17:39:39 +00:00
e2ce040f1f Release: actionable ids for raw id-less pending approvals (#7510) (#7551)
* fix(approvals): mint actionable ids for raw id-less local pending entries

A raw id-less approval entry in _pending (agent-side _pending_result drops
{command, pattern_key, pattern_keys, description} verbatim when no gateway
notifier is registered for the session) is unactionable by contract: the
frontend owner-capture requires an approval_id, so the approval card can
neither be approved nor dismissed - the 1.5s poll re-serves it forever.

reconcile_gateway_pending_mirror_locked already mints gwlocal:<token> ids
for id-less _gateway_queues producers; extend the same contract to raw
non-mirror _pending entries with a stable gwlocal-mirrorless:<uuid> id
minted once onto the stored entry dict, so the id persists across polls
and frontend dismiss markers stick.

The exact-id respond path needs no further change: once the entry carries
an id, _resolve_approval_legacy pops it (also clearing the stale
stream-pointer 409 guard), stale explicit ids still fail closed (#527),
and the yolo Skip-all release drains it.

* docs(changelog): stamp #7510 (actionable ids for raw id-less pending approvals)

---------

Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
Co-authored-by: n <a@n>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-13 03:07:49 +00:00
e4d9b799e4 Release: manual title regeneration for queued-user transcripts (#7545) (#7547)
* fix(api): scan to first complete exchange for title regeneration (#7543)

_first_exchange_snippets() aborted at a second consecutive user message
before collecting assistant text, so transcripts opening with queued user
rows (channel-backed imports) hit the missing_exchange rejection and the
deterministic fallback got persisted with HTTP 200 — invisible in the UI
and unrecoverable by retrying.

Keep scanning until the first complete user+assistant pair instead, and
fall back to the last complete exchange when the opening user message
yields no user text (privacy scrubbing) instead of failing with
empty_user_message.

Tests: tests/test_issue7543_title_first_exchange.py (8 cases, incl. the
issue-pinned transcript shape and walker-mocked discriminator tests for
both fixes).

* test(api): drop local-machine notes and sys.path hack from 7543 test module

- Docstring described the change as a local-only uncommitted hotfix with
  machine-specific paths — wrong for an upstream PR and unsafe to publish.
- Drop the defensive REPO_ROOT sys.path insert; the repo conftest already
  puts the package root on sys.path (matches the neighboring test modules).

* fix(api): drop unreachable latest-exchange fallback from title regeneration

Greptile flagged the mocked-walker fallback test (P2) as fabricating
internal state, and an exhaustive enumeration of realistic transcript
shapes (5460 combinations, lengths 1-6) confirms it: both walkers share
the same text extraction and the forward walker scans every message, so
'first walker empty while latest walker yields a pair' is unreachable
from real transcripts. The fallback branch was dead code.

Remove it together with its two walker-mocked tests; the fixed first-
exchange walker alone resolves the reported issue shape. Replace the
mocked tests with an observable E2E error-path test through the real
walkers, and make the aux mock guard-faithful (empty assistant_text ->
missing_exchange per the generate_title_raw_via_aux contract) so the
fail-before proof actually exercises the endpoint contract.

* fix(#7543): scope first-exchange scan-past to the manual-regen path only

The full suite caught that broadening the SHARED _first_exchange_snippets also
activated the automatic in-stream background-title path for consecutive-user-row
transcripts, moving stream teardown from the synchronous stream_end branch to the
background-title-thread branch (test_issue3929 emits_done regressed: stream_end
was no longer in the synchronous queue). #7543 is specifically about the MANUAL
'Regenerate title' endpoint, so gate the scan-past behavior behind an opt-in
scan_past_consecutive_users flag (default False = master behavior) and pass True
only from generate_session_title_for_session. Auto in-stream path is now
byte-identical to master; added a default-behavior regression guard test.

* docs(changelog): stamp #7545 (manual title regen for queued-user transcripts)

---------

Co-authored-by: Raylands <Raylands@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 21:12:52 +00:00
b1286878a4 Release: models_discovered/discover_models live-catalog fix (#7409) (#7538)
* Apply reviewed change

* fix(models): honor discover_models opt-out + preserve discovered catalog on live-probe failure

Codex gate found two CORE edge cases in the #7404 fix:
1. discover_models:false must override models_discovered:true (explicit opt-out
   pins the configured allowlist; honor Agent string forms false/no/0).
2. a transient empty live-catalog probe dropped a discovered provider's models
   (cached empty ~24h); now falls back to configured discovered IDs merged with
   any static catalog (deduped, ordered first).

Mirrors the Hermes Agent canonical semantics (model_switch_providers._discover_flag,
model_switch._models_config_is_allowlist). Adds 3 mutation-proven regressions.

* docs(changelog): stamp #7409 (models_discovered/discover_models live catalog)

---------

Co-authored-by: evgenyponomarev <712802+evgenyponomarev@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 05:32:16 +00:00
n c196306fcb Stage: PR #7337 — chat-attachment upload anchored O_EXCL (TOCTOU) by @zicochaos 2026-09-12 01:26:51 +00:00
Sebastian B f7523f63a2 fix(api): create chat-attachment uploads with anchored O_EXCL (TOCTOU)
handle_upload deduped filenames with an exists() check and then wrote with
plain write_bytes — two concurrent uploads of the same name both passed the
check and the last writer silently won. Mirror the #3398 workspace-upload
hardening: O_CREAT|O_EXCL|O_NOFOLLOW anchored open from the session
attachment dir; a raced duplicate now 409s instead of overwriting and a
raced symlink cannot redirect the write.
2026-09-12 00:55:45 +00:00
Hermes c076fe9e09 fix: detect MIME types for static binaries 2026-09-12 00:55:23 +00:00
nesquena-hermesandnesquena-hermes f79325498e fix(cron): restore messaging platforms in the delivery picker (#7525)
* fix(cron): restore messaging platforms in the delivery picker

The Agent moved _KNOWN_DELIVERY_PLATFORMS from cron.scheduler to
cron.scheduler_delivery. _handle_cron_delivery_options imported the old
path inside a bare except and fell back to an empty frozenset, so the
import error was swallowed and GET /api/crons/delivery-options silently
returned only local and origin — telegram, discord, slack, feishu and
every other messaging platform disappeared from the cron delivery picker
with no error surfaced anywhere.

Resolve the authority across both module paths (current first, legacy
fallback) so the picker keeps working across Agent versions, and add a
regression test that pins the resolution order and cross-checks the
endpoint output against the resolved authority.

* test(cron): skip the authority-resolution test where the Agent is absent

CI runs agent-free (tests/browser_smoke.py), so neither cron.scheduler_delivery
nor cron.scheduler is importable there and the authority resolves empty. The
new regression test asserted non-empty and therefore failed shard 3 on both
Python versions. Skip instead: with no cron package there is no authority to
resolve and nothing meaningful to assert. The test still fails loudly wherever
the Agent IS installed, which is where a future relocation would bite.

* test(cron): register the new test in _AGENT_DEPENDENT_TESTS

Use the project's existing skip mechanism (tests/conftest.py) rather than an
ad-hoc inline pytest.skip, matching the five sibling delivery-options tests.
CI runs agent-free, so there is no cron package and no authority to resolve
there; with the Agent installed the test still asserts loudly, which is the
environment where a future relocation would actually bite.

Verified both directions: 6/6 pass with the Agent present, and forcing
AGENT_MODULES_AVAILABLE=False skips all 6 (was 5) as CI will.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-10 22:30:57 -07:00
webtecnica 4359ed4fee fix(onboarding): use OpenRouter namespace z-ai/ for Z.AI models (#7514) 2026-09-10 22:30:44 -03:00
rh-idandnesquena-hermes 40a6a7696a feat: add GLM-5.3 Flash to Z.AI model list (#7323)
* feat: add GLM-5.3 Flash to Z.AI model list

Add glm-5.3-flash next to glm-5.3 in _PROVIDER_MODELS["zai"] and mirror
it in _FALLBACK_MODELS, following the existing glm-4.5 / glm-4.5-flash
sibling pattern. Reasoning-effort gating needs no change: the >= 5.2
version gate already classifies the id at the full effort tier.

Adds a regression suite asserting catalog presence, label, ordering,
fallback entry, and end-to-end propagation through
get_available_models().

* test: pin glm-5.3-flash sibling adjacency in zai provider catalog

Per PR review feedback (#7323): the provider-catalog ordering test now
asserts glm-5.3-flash immediately after glm-5.3 and glm-5.2 immediately
after the flash sibling, matching the adjacency the fallback test
already pins. The base<flash relative check is subsumed; flash<glm-5.1
is kept to pin 5.1 after the whole 5.3 generation.

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 13:08:49 -07:00
zicochaosandnesquena-hermes a6b187fc84 refactor(routes): extract the 585-line GET /api/session handler to _handle_session_get (#7340)
* refactor(routes): extract the 585-line GET /api/session handler to _handle_session_get

Pure move out of handle_get's if/elif chain into a named module-level handler, matching the pattern the other ~200 routes already use. Body is verbatim (one-level dedent only); AST identity of the moved statements verified; no behavior change — the GET /api/session test slice passes identically before and after.

* test: repoint four source-window tests to _handle_session_get

The moved block's source-window extractors (chat_start synthesiser/read-only
pins, regressions runtime-streaming-state window, issue1436 load-path
markers, security-redaction inspect) now anchor on the extracted handler;
assertions unchanged.

* test: repoint messaging-session loader pin to _handle_session_get

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:49:14 -07:00
656575a400 Fix geometric duplication of recovered reasoning-only rows at turn settle (refs #7388, independent of #7416) (#7443)
* Fix geometric duplication of recovered reasoning-only rows at turn settle

_restore_display_reasoning_metadata() re-inserted every display-only
reasoning row at its API-safe position in the new result. After context
compaction that position is past the end of result_messages, so the row
was appended after the new reply; _message_identity() is None for an
empty assistant row, so the display merge could not dedup it. Each
settled turn therefore re-added every existing empty
_recovered_from_run_journal anchor once more (1,1,2,4,8,... blocks).

Only restore a reasoning-only row in front of its own API-safe
successor; a compacted result with no aligned slot is skipped.

Refs #7388, #7416 (independent: that PR fixes linear journal replay).

* tests: keep docstrings within the 5-line block budget

* Prefer stable ids when aligning a reasoning row's successor at restore

The successor-alignment guard compared the historical successor with the
aligned current row via _message_identity(), which ignores stable ids and
returns equal keys for repeated prompts ("continue"). A distinct current
turn with the same text false-matched the historical successor, every
recovered anchor before it was re-inserted, and 64 anchors became 128
after one restore->merge settle (nesquena-hermes gate on ed2076eda).

Alignment is now stable-id-first: when either row carries an id, both
must carry the same id; _message_identity() is the fallback only when
neither row has one. Adds a production-order regression through
_settle_result_messages() with 64 anchors, stable ids and repeated
"continue" prompts (RED on ed2076eda and origin/master: 128 != 64), plus
a unit test pinning all four id-presence branches.

* Fail closed on stable-id identity: positive-int only, unique, no content fallback

_restore_display_reasoning_metadata() compared ids with raw equality, so a
persisted historical successor with id=True (or 1.0) matched a distinct
current row minted as id=1 and restored the reasoning card before the
wrong turn. Ids are now accepted only when type(value) is int and > 0,
both rows must carry a valid equal id whenever either has an id field,
and the id must be the sole owner in both API-safe projections.
_assign_stable_message_ids() re-mints invalid ids so they cannot survive
into a collision with minted integers.

* tests: explicit zip strict= for the ruff B905 gate

* Resolve the active-turn boundary before restoration; never carry historical ids/rows into it

A historical assistant successor with the same content as the distinct
current assistant ("Done." / "Done.") let _restore_reasoning_metadata copy
the historical stable id onto the current row; the uniqueness guard then
accepted that forged identity and 64 recovered cards settled to 128.

_active_turn_boundary() is decided once in _prepare_marker_clean_writeback
before any restore (token > strict context prefix > last matching user row
> 0) and threaded through _settle_result_messages into both restores.
Rows at/after it receive no historical id/metadata and no historical
reasoning-only row. Assistant-only results are boundary zero.

* Keep the pinned _restore_reasoning_metadata signature; boundary goes through _restore_reasoning_metadata_before_boundary

tests/test_sprint49.py pins the two-arg signature as source text, so the
boundary-aware core lives in _restore_reasoning_metadata_before_boundary
and the original name stays a thin no-boundary wrapper.

* Content-only prefix is not turn ownership: fail closed to boundary 0; sync route passes result/Agent turn authority

- _active_turn_boundary: a previous-context prefix matched by _message_identity
  (ids/timestamps ignored) only narrows where the current user may be; the
  boundary is the prompt-matching user AT OR AFTER that prefix, else 0.
- _handle_chat_sync: resolve authority via _resolve_active_turn_authority
  (result, agent) instead of hard-coding None.
- Regressions: three-row current-only collision (boundary 0, one 64 block),
  exact-authority + append-only controls, ID-less-successor settle/save/load
  pinned at the persisted-display boundary, sync-route authority pass-through.

---------

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:29:33 -07:00
Arush Khasru b39bf1a113 Fix key_cmd credentials in agent cache signatures 2026-09-10 04:40:26 +00:00
394ab31eea release: #7351 bind turn workspace as agent cwd (#7504)
* rebase #7351 onto master (bind turn workspace as agent session cwd)

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

* fix test: use deployed agent.runtime_cwd API (_SESSION_CWD.get) not nonexistent _session_cwd_override

The two failing assertions referenced agent.runtime_cwd._session_cwd_override(), a
helper not present in the installed Agent (which exposes _SESSION_CWD, resolve_agent_cwd,
set/clear_session_cwd). The production binder is correct and verified: binding workspace=/tmp
sets _SESSION_CWD.get()=='/tmp' and resolve_agent_cwd()=='/tmp', reset restores _UNSET.
Line 343 was redundant with the surrounding assertions; line 386 now reads _SESSION_CWD.get().

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

* docs(changelog): stamp #7351 turn workspace cwd binding

---------

Co-authored-by: n <a@n>
Co-authored-by: samfoy <samfoy@users.noreply.github.com>
2026-09-10 03:40:45 +00:00
47e394895d release: #7160 stale-runtime report + hardened Agent-update marker read (#7502)
* rebase #7160 onto master (report unverified Agent updates, no auto-restart)

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

* harden agent-update marker read: no-follow, fstat regular-file, bounded (fixes gate CORE)

The Agent update marker is attacker-adjacent shared state. _read_live_agent_update()
used Path.read_text(), which follows symlinks, blocks forever on a FIFO, and reads an
unbounded regular file — a rejected/stale-runtime request could hang or OOM. Now opens
O_RDONLY|O_NONBLOCK|O_NOFOLLOW, fstat-verifies a small regular file, and reads a bounded
max, classifying anything else as 'unknown' (fail-closed). Adds FIFO/oversized/symlink/
happy-path regression tests.

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

* fix marker-read portability: gate os.open fast-path on O_NONBLOCK+O_NOFOLLOW

os.O_NONBLOCK is Unix-only; accessing it unconditionally raised AttributeError
on native Windows, breaking every stale-revision barrier (chat/compression/etc).
Now both flags resolve via getattr and the os.open() fast path is taken only
when BOTH are available (_MARKER_SAFE_OPEN_AVAILABLE); otherwise fall back to an
lstat-only classification (absent vs unknown) so a fallback platform can never
symlink-traverse or crash. Adds a Windows-fallback regression test.

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

* docs(changelog): stamp #7160 stale-runtime report + hardened marker read

---------

Co-authored-by: n <a@n>
Co-authored-by: hrdwdmrbl <hrdwdmrbl@users.noreply.github.com>
2026-09-10 02:21:04 +00:00
nandMartin Dell 3e616ad455 fix: harden bounded sidebar cache projection (#7489)
Preserve nested request isolation, make the response allowlist single-source,
and strip heavy fields from existing index rows after reconciliation.

Co-authored-by: Martin Dell <martindell@users.noreply.github.com>
2026-09-09 23:36:41 +00:00
n 989d7f3668 Merge remote-tracking branch 'origin/pr-7489' 2026-09-09 22:49:54 +00:00
Martin Dell 7150b8fb2b fix: bound sidebar session-list cache 2026-09-09 21:22:38 +00:00
cfdfe4db8f release: #7261 provider-native auxiliary model IDs (approved) (#7491)
* fix(settings): persist provider-native auxiliary model IDs

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

* docs(changelog): stamp #7261 provider-native aux model IDs

---------

Co-authored-by: n <a@n>
Co-authored-by: lowlandsheperd <lowlandsheperd@users.noreply.github.com>
2026-09-09 20:49:47 +00:00
2477d992f4 release: lock-disciplined STREAMS reads (#7339) (#7488)
* fix(streaming): lock-disciplined STREAMS reads via peek_stream()

Writers pop STREAMS under STREAMS_LOCK, but four hot reads used bare
STREAMS.get(): the streaming worker start, the gateway run start, and the two
SSE attach paths in routes.py. Each is individually saved today by GIL-atomic
dict.get plus a None-guard, but the check-then-act windows (get → register,
get → subscribe) race teardown pops, and the bare pattern invites unprotected
copies. peek_stream() centralizes the locked read; all four sites migrate with
no behavioral change.

* test: dual-patch config.STREAMS alongside routes.STREAMS in SSE seam tests

peek_stream() reads the canonical api.config.STREAMS registry; tests that
inject a fake registry by patching only the routes-level re-export left the
helper reading the real (empty) registry and hung the SSE loop to the
pytest-timeout ceiling. Mirror the test_issue6623 pattern: patch both
bindings.

* docs(changelog): stamp #7339 lock-disciplined STREAMS reads

---------

Co-authored-by: Sebastian B <sb@wmc.sh>
Co-authored-by: n <a@n>
2026-09-09 20:22:36 +00:00
b1a5ade137 release: multimodal title-generation fix (#7306) (#7484)
* fix(title): preserve multimodal title generation

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: Claude Code:claude-opus-5

* docs(title): explain multimodal title generation

Assisted-by: Hermes Agent:gpt-6-astra

* docs(changelog): stamp #7306 multimodal title-generation fix

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-09 18:50:13 +00:00
n f9443e17b2 stage: merge #7270 (canonical Windows restart entrypoint) 2026-09-09 06:14:20 +00:00
nandzicochaos 1e17127f49 fix: treat whitespace-only body as empty ({}) not a 400
Gate finding on #7336: read_body() rejected whitespace-only bodies that
worked on master (a body-optional DELETE/PATCH/PUT can send b' \n'), returning
400 Invalid JSON body. Normalize a fully-whitespace body to {} before json.loads,
matching the pre-#7336 lenient behavior for that shape. Adds a parametrized
regression test.

Co-authored-by: zicochaos <zicochaos@users.noreply.github.com>
2026-09-09 04:21:46 +00:00
n 1f57afd84c stage: merge #7336 (reject malformed JSON bodies) 2026-09-09 04:02:32 +00:00
n 927c4fa1b6 stage: merge #7476 (warn once per state.db) 2026-09-09 02:10:36 +00:00
Rodrigo Gomes 12df57498a fix(sessions): warn once per state.db when 'source' column is missing
Observed: on a multi-profile install where one profile's state.db predates
the agent's `source` column, errors.log gained one identical line every
~15 s (276 lines in ~70 min):

  api.models WARNING agent session listing skipped: state.db at
  /data/hermes/profiles/planner/state.db has no 'source' column (older
  hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to
  fix this.

Mechanism: api/agent_sessions.py read_importable_agent_session_rows()
runs `PRAGMA table_info(sessions)` and warns unconditionally when the
column is absent (api/agent_sessions.py:564 before this change).
api/models.py get_cli_sessions(all_profiles=True) calls it for every
profile on every sidebar poll, behind only the 5 s
_CLI_SESSIONS_CACHE_TTL_SECONDS cache, so the same DB re-emits the same
warning on every cache miss. The condition is a property of the DB
file, not of the poll.

Fix: a module-level `_SOURCE_COLUMN_WARNED_DB_PATHS: set[str]` keyed by
the resolved db_path. The warning fires the first time a given path is
seen in the process and is suppressed after that; a different path
warns on its own. Message text is unchanged. Process-lifetime only: a
restart warns again, which is intended (the line remains the operator's
cue that the agent still needs upgrading). Plain set mutation under the
GIL is sufficient; a duplicate line from two racing first calls is
harmless.

Tests (tests/test_issue634.py, real pre-`source` sqlite DBs under
tmp_path, asserting on captured log records):
- same db_path called twice -> exactly one WARNING (failed before:
  `assert 2 == 1`)
- two db_paths, then the first again -> exactly two WARNINGs, one per
  path (failed before: `assert 3 == 2`)
The seven existing #634 source-shape tests still pass; neighbours
test_issue3762/3930/5455/1494 pass.

Deliberately not changed: the exception-path warning in
get_cli_sessions() (api/models.py), the read-only-open fallback warning,
and the 5 s cache TTL. The operator-side root cause (the agent
reconciler creating a pre-`source` state.db) is a separate upstream fix;
this only stops the repetition.
2026-09-08 22:38:07 -03:00
webtecnica 61f3cf7ed9 fix(title): publish conversation context for aux title calls
Title generation via the aux client ran without the Agent's ambient
conversation context, so relay targets that key on it (OpenCode Go)
rejected the completion call with HTTP 400.

Publish the webui session id as the Agent conversation context around
the aux completion (restoring it in a finally block) and pass it from
all three title entry paths: background update, background refresh and
the on-demand regenerate helper.

Refs #7470
2026-09-08 22:06:32 -03:00
n ad620c3211 stage: merge #7356 (provider-hint slash-id) 2026-09-08 18:20:45 +00:00
n 4311143c12 Merge PR #7428 for gate 2026-09-08 06:29:12 +00:00
n 2a804ae38d Merge PR #7402 for gate 2026-09-08 05:23:11 +00:00
n edf6c6a686 Merge PR #7288 for re-gate on current master 2026-09-08 04:37:42 +00:00
n e69d681e82 Merge PR #6620 for exact-head re-gate 2026-09-08 03:01:00 +00:00
n e7841a5988 gate fixes: opt-in pipe separator (#7331) + IPv6-safe Origin (#6772)
Two Codex adversarial-security-gate findings on the batch:

1. #7331 CORE (backward-compat break): making '|' an unconditional trusted-groups
   separator would re-split an existing deployment whose group NAME legitimately
   contains a literal '|' (e.g. 'unprivileged|admins' read as one group), silently
   changing its profile binding. Pipe splitting is now OPT-IN via
   HERMES_WEBUI_TRUSTED_GROUPS_PIPE_SEPARATOR; comma/newline stay the default.
   Tests: literal-pipe preserved by default, pipe splits only with the flag set.

2. #6772 follow-up (my own hardening introduced it): rebuilding the origin from
   parsed parts dropped the brackets around an IPv6 host, so 'http://[::1]:8787'
   became 'http://::1:8787' and never matched the browser's bracketed
   clientDataJSON.origin -> passkey login over IPv6 would fail. Re-bracket IPv6
   literals. Added rp_context regression tests (Origin-first, port preserved,
   IPv6 bracketed, malformed->Host fallback, userinfo stripped).
2026-09-08 01:53:01 +00:00
n 289b539bc4 harden(passkeys): accept only a well-formed http(s) Origin for RPID derivation
Follow-up on #6772. Preferring Origin over Host for the WebAuthn RPID is correct
(the RPID must match the page origin, not the backend's), and it is not a trust
decision: the authenticator only releases a credential whose RPID is a registrable
suffix of the real page origin, clientDataJSON.origin must equal the stored origin,
rpIdHash must match the stored RPID, and the assertion signature is verified against
the stored credential public key.

Still, only accept a syntactically valid http(s) Origin carrying a hostname, and
rebuild the stored origin from the parsed scheme/host/port instead of echoing the raw
header — so a malformed, non-http (javascript:/file:), or userinfo-bearing Origin
falls through to the Host-derived path rather than being stored verbatim.
2026-09-08 01:31:26 +00:00
n 0f76602790 Merge PR #7331 2026-09-08 01:30:36 +00:00
n 1e00ef131b Merge PR #6772 2026-09-08 01:30:35 +00:00
n 94b70ce8b8 Merge PR #7385 2026-09-08 00:16:17 +00:00
n bee7b8a9e4 fix(state-sync): repoint to renamed agent SessionDB API + fix agent-coupled tests
The Hermes Agent's state-module refactor (agent commit 53db597201, released
v2026.9.7) renamed SessionDB.set_auto_title_if_empty -> set_auto_title(
session_id, title, *, source) and (agent commit 14791b4d4e) dropped the
tools.approval._ApprovalEntry back-compat facade (class moved, unchanged, to
tools.approval_gateway_wait).

Production impact (real, silent): api.state_sync.sync_session_title called the
removed set_auto_title_if_empty, whose AttributeError was swallowed by the
surrounding `except Exception: logger.debug(...)`, so WebUI auto-generated
session titles stopped persisting to state.db on agent >= v2026.9.7 — hermes
sessions list went blank again (the exact #6892 regression). Now calls
set_auto_title with LLM provenance, which preserves the identical semantics
(only fills NULL/lower-authority title, returns False untouched on a manual
rename, ValueError -> get_next_title_in_lineage #N de-dup).

Test fixes (agent-coupled; skip on GitHub CI which installs no agent, so they
only surface on an agent-equipped box like our gate):
 1. tools.approval._ApprovalEntry -> idempotent autouse conftest compat shim
    backfilling the class from its post-split location only when the installed
    agent lacks the dropped facade. No production code imports it.
 2. test_goal_command_webui::test_profile_goal_falls_back_when_session_db_path_is_frozen
    was missing the monkeypatch.syspath_prepend(...) that every sibling test in
    the file has, so its bare `import hermes_state` failed under full-suite
    ordering after a predecessor stripped the agent dir from sys.path. Added it.

Verified: authoritative 5-shard CI-equivalent split all green (0 failed);
each failure proven pre-existing on clean master (not this diff).
2026-09-07 23:34:45 +00:00
Evan Light d7ed506d21 fix(skills): report the total when skill-not-found truncates available_skills (#7426)
The not-found reply capped available_skills at 20 with no total and no
truncation marker, so a caller with more skills than the cap was handed a
partial list that reads as the complete set. A skill sorting past the cut
looked uninstalled.

Keep the bound; stop it lying. The payload now carries total_skills and
available_skills_truncated, and the hint names how many of how many were
shown, matching the (list, truncated) shape already used for capped commit
subject lists in api/updates.py.
2026-09-04 15:40:17 +00:00
Sebastian B da5703d501 fix(agent-v0.21): port out-of-band normalizers to the transport API
hermes-agent v0.21 removed agent.anthropic_adapter.normalize_anthropic_response and the AIAgent._normalize_codex_response method; the sanctioned out-of-band contract is now agent._get_transport(<mode>).normalize_response(...) returning NormalizedResponse (.content). Port the anthropic_messages and codex_responses branches of generate_title_raw_via_agent (api/streaming.py) and _agent_text_completion (api/routes.py); on agent v0.21 those branches currently die on import/attribute errors and silently fall back, degrading title generation and handoff summaries. build_anthropic_kwargs, _anthropic_messages_create, and _run_codex_stream are unchanged in v0.21 and stay as-is.
2026-09-02 15:07:17 +01:00
webtecnica ec4bac9fc9 fix: model picker search matches OpenRouter display names (#7228) 2026-08-31 15:29:28 -03:00
Manny7717 1a31003e9e fix(config): gate custom provider hints on unique configured entries
Named custom providers are only routable when the slug resolves to a real,
unique custom_providers[] entry. A stale session provider such as
'custom:missing' must not be minted into an @custom:missing:... route -
resolve_model_provider() would take the named-provider lane and find no
matching endpoint. _unique_custom_provider_entry() returns None for unknown
slugs and raises AmbiguousCustomProviderError for collisions, matching the
point-of-return guard used by resolve_model_provider.

Addresses maintainer review on #7356. Adds two regression tests: unknown
named custom slug stays bare, ambiguous normalized slug fails closed.
2026-08-29 09:12:17 -05:00
Manny7717 da367cc692 fix(config): keep provider hint for slash ids when session provider differs
model_with_provider_context() passed any slash-bearing model ID through
bare when the session provider was not OpenRouter/ACP/plugin and not
listed under config.yaml -> providers:. That dropped the hint for portal
providers (nous, nvidia, opencode-zen, opencode-go) and named custom
providers (custom:<slug>), so resolve_model_provider() fell back to the
profile default provider + base_url and sent a vendor-scoped id to the
wrong endpoint (HTTP 404, e.g. upstage/solar-pro4:free to api.x.ai).

Emit an explicit @provider:model hint for any known routable provider
that differs from the configured default; keep the bare passthrough for
unknown/ambiguous provider slugs so custom/proxy base_url routing stays
in charge. Closes #7333.
2026-08-28 20:29:05 -05:00
Anthony Ruiz c6a2c1b7d2 perf(streaming): fold the echo search window once instead of per cut index
`_strip_compact_echo_suffix` probed every candidate cut index and re-folded
the whole remaining tail for each probe, so the cost grew with
`window x suffix` rather than with the data actually inspected. It runs up to
four times per interim assistant message, on the reasoning buffer, the shared
stream mirror and up to two per-message segments.

The practical effect is worst on the longest message of a turn: 297ms for a
200-character message but 5.6s for a 6000-character one, measured on the live
process. That time is spent holding the GIL, so a single long final message
also stalls every other stream served by the same worker. Profiling a live
turn attributed ~5% of total CPU to this one helper.

Fold the window once, then walk backwards across exactly as many non-space
characters as the folded suffix holds. `str.isspace` and the `\s` pattern used
by `_compact_for_echo_compare` agree on all 1114112 Unicode code points, so
the folded view and the walk stay consistent.

Behaviour is unchanged, including the leftmost-cut tie-break on repeated text
and the bounded search window. The new test pins equivalence against a naive
reference oracle over ~2020 cases (exact, partial and absent echoes, mixed
whitespace, unicode, empty and None inputs, repetitions, out-of-window
echoes), asserts the fold no longer runs once per cut index, and caps the
runtime of a long-conclusion call.

Measured: 200 chars 297ms -> 0.149ms, 6000 chars 5645ms -> 0.720ms.
2026-08-27 23:58:18 +00:00
Sebastian B 743b757a77 fix(api): reject malformed and non-object JSON bodies with clean 400s
read_body() silently turned malformed JSON into {} (clients then got a
misleading 'Missing field' 400) and passed non-dict JSON through to handlers
where body.get() raised AttributeError and surfaced as a generic 500.
Raise ValueError instead — handle_post already maps it to a clean 400 — and
give the PATCH/DELETE/PUT dispatchers the same mapping they lacked. Empty
bodies keep the documented {} default.
2026-08-28 00:49:50 +01:00
SuperAgent BEAM3 bb5347e1cd fix: accept pipe-separated trusted-groups header values
Authentik's proxy provider on this deployment joins multiple group
names with "|" instead of "," (e.g. "admins|developpeur"). The
comma-only split in _trusted_groups_header_value() treated that whole
string as a single unmatched group name, silently dropping the
session to the unbound "default" profile even for identities that
are legitimate members of a mapped group. Single-group identities were
never affected since there was no separator to parse.

Accept comma, pipe, and newline as equivalent separators so the parser
works regardless of which the outpost happens to send.
2026-08-26 08:35:10 +00:00
webtecnica b36fbdd633 fix(regeneration): refuse bounded tail on in-tail duplicate keys + real WAL interleaving test (#6826 r5) 2026-08-26 00:24:07 +00:00
webtecnica 7abd4773ca fix(regeneration): snapshot reuses canonical projection+ordering; WAL data_version guard (#6826 r4) 2026-08-26 00:24:07 +00:00
webtecnica b33aff9d09 fix(regeneration): occurrence-count guard + single-snapshot (no TOCTOU) for bounded tail read (#6826 r3 round-3) 2026-08-26 00:24:07 +00:00