Commit graph

3460 commits

Author SHA1 Message Date
liuhao1024
e4ea0a0ed7 fix(config): preserve string-typed config values 2026-07-12 23:44:16 -07:00
Teknium
8a5f8379ed fix(agent): honor custom-provider extra_body for multi-model catalogs
_custom_provider_model_matches() only compared the session model
against the entry's single 'model' field. A custom provider declaring
a multi-model catalog (providers.<name>.models mapping / models list)
whose default model differed from the session model silently failed to
match — dropping the entry's extra_body entirely. Real impact: an
OpenAI custom provider pinning service_tier=flex via extra_body ran
every request at STANDARD tier (~2.3x billing) with zero signal.

- Model matching now accepts the session model when it appears in the
  entry's models catalog (dict keys or list), case-insensitive;
  single-model 'model' field behavior unchanged; entries with neither
  still match everything.
- Usage report ('hermes -z --usage-file') now carries service_tier
  (the tier requested via request_overrides.extra_body) so batch
  pipelines can audit the billed tier per run.

Validation: 8 new tests; live E2E via real 'hermes -p sweeper -z'
with httpx-level wire capture — service_tier=flex present in the
outgoing /v1/responses body and in the usage report.
2026-07-12 23:30:52 -07:00
agent
80b58ec71d fix(kanban): spawn goal_mode workers with -Q so the goal loop actually runs
_default_spawn sets HERMES_KANBAN_GOAL_MODE=1 but launched 'chat -q' without
-Q; _run_kanban_goal_loop_q only executes in the quiet single-query branch,
so goal-mode never ran for dispatcher-spawned workers — they got one turn,
printed text, exited rc=0, and tripped the protocol-violation circuit
breaker (2026-06-09, cards t_d9cbe312 et al). Root-cause report + upstream
issue draft in kanban workspace t_720c5c60.
2026-07-12 23:10:33 -07:00
Teknium
8030b01a2a fix(kanban): harden durable artifact handoff 2026-07-12 23:09:41 -07:00
Kevin Yin
e6c42b5d80 fix(kanban): preserve scratch completion artifacts 2026-07-12 23:09:41 -07:00
Robert Blaas
da6d6164ba fix(dashboard): correct approvals.mode select options
The web UI CONFIG_SCHEMA showed ['ask', 'yolo', 'deny'] for the
approvals.mode select field. These don't match any real config values
and 'smart' mode was entirely unreachable from the dashboard.

Correct the options to ['manual', 'smart', 'off'] which match the
values defined and documented in hermes_cli/config.py.

Adds a regression test to TestBuildSchemaFromConfig to pin the correct
option names and guard against future drift.

Fixes #31925
2026-07-12 17:42:42 -07:00
Teknium
9a15fad0d6
fix(web): preserve declared providers in model writes (#63058)
Unify the named-provider fixes from #52506, #57185, #60337, and #60901 at the main-model normalization chokepoint.

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
2026-07-12 03:08:08 -07:00
Teknium
2b5d4ae916
fix(model): merge configured models into picker rows (#63055)
Preserve the root cause and precedence direction from #43538 while applying the merge before truncation and covering all declared model shapes.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-07-12 03:05:12 -07:00
teknium1
8c77206859 refactor(model): gate picker rows by runtime capability 2026-07-12 03:04:07 -07:00
Ahmett101
938c2622f6 fix(model_switch): filter /model picker for unregistered providers (#57503)
list_authenticated_providers() emits picker rows for every slug in
PROVIDER_TO_MODELS_DEV that has any credential env-var set. Several of
those slugs (notably 'mistral') have no PROVIDER_REGISTRY entry, so
resolve_provider() rejects them as 'Unknown provider' once the user
selects a model — leaving the picker showing rows that cannot actually
be selected.

Add a resolve-gate in section 1: if PROVIDER_REGISTRY.get(hermes_id)
is None, skip the slug. The picker now only lists providers that can
actually be switched to at runtime.

This automatically resolves the duplicate-Mistral dedup symptom too:
once the broken-from-models.dev row is filtered, the conflict between
PROVIDER_TO_MODELS_DEV['mistral'] and a custom_providers 'Mistral' row
is moot.

Composes with #50289 (which promotes mistral to first-class via the
provider-plugin path): when that lands, PROVIDER_REGISTRY gains a
'mistral' entry and the gate becomes a no-op for it. No conflict.

Tests (regression suite):
- tests/hermes_cli/test_model_switch_filter_unresolved.py (new, 4 tests):
  Picker excludes 'mistral' when MISTRAL_API_KEY is set; 'deepseek' and
  'xai' (PROVIDER_REGISTRY-backed) still appear; 'mistral' stays
  excluded when no key is set. Confirmed by reverting the fix and
  seeing the test fail with 'mistral leaked into /model picker'.

Cross-checked against the existing 51 test_model_switch_* and
test_custom_provider_* cases — 55/55 PASS, no regressions.
2026-07-12 03:04:07 -07:00
Teknium
4a4a0c2fc7
fix(auth): enforce credential pool provider boundaries (#63048)
Retain the provider-boundary core of #52799 while reusing the pool reload and handoff paths already landed in #53591 and #62417.

Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
2026-07-12 03:00:53 -07:00
teknium1
c36f6b7259 fix(skills): install referenced bundle files with scan provenance 2026-07-12 02:59:27 -07:00
teknium1
1e75744b79 refactor(model): centralize picker credential availability 2026-07-12 02:59:14 -07:00
AIalliAI
3a67a7be55 fix(model-switch): don't treat an exhausted credential pool as authenticated
An aggregator whose pooled credentials are all exhausted/dead still counted as
an authenticated provider during no-provider /model resolution. It then won the
model-name match, was set as the sticky session provider, and poisoned every
later switch with "empty API key" errors while still routing through the dead
aggregator.

list_authenticated_providers now requires a pool to have at least one available
entry (has_available, not has_credentials / bare key presence) at all three
credential-pool gates. Simple token-style entries that don't parse into
exhaustion-tracked entries keep the prior behaviour, so providers whose creds
live only in the auth-store credential_pool still appear.

Fixes #45759
2026-07-12 02:59:14 -07:00
Brooklyn Nicholson
b5f0e451c1 feat(cli): restore cwd on resume (--no-restore-cwd)
Resuming a session cd's back into its recorded working directory, so it resumes
in the repo it belonged to. `--no-restore-cwd` opts out; skipped under
--worktree (that path owns its dir); best-effort — a missing dir warns and stays
put rather than failing the resume.

Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
2026-07-12 05:08:13 -04:00
Brooklyn Nicholson
0c4aed2499 feat(cli): sessions list --workspace filter + Workspace column
`hermes sessions list --workspace <needle>` filters to one workspace (git repo
root or project dir, matched by path substring or basename) and adds a
Workspace column. The column only appears once at least one listed session
carries a workspace, so all-unbound listings render exactly as before.

Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
2026-07-12 05:08:03 -04:00
Teknium
7550c594ce
feat(reasoning): add max and ultra effort levels (#62650) 2026-07-12 00:26:49 -07:00
Teknium
62a76bd3d5
feat: make smart approvals the default (#62661) 2026-07-12 00:25:55 -07:00
teknium1
31152ae108 fix(providers): align Fireworks integration with project policy 2026-07-11 05:43:35 -07:00
Alex Jestin Taylor
c97d9a4c07 feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.

- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
  so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
  models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
  override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
  valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
  to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
  profile fallback_models, so the default is an agentic chat model instead of
  whatever the live catalog returns first (Fireworks listed an image model,
  flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
  Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
  i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
  doctor incl. the slash-form regression, aux headers, credentials), discovery
  spot-check, and a live smoke test driven through the Hermes runtime.

Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
2026-07-11 05:43:35 -07:00
teknium1
8041be7954 fix(model): keep configured provider authoritative 2026-07-11 05:43:07 -07:00
konsisumer
0ca6a98541 fix(cli): keep current provider visible in model pickers 2026-07-11 05:43:07 -07:00
briandevans
5ecc07986f fix(cli): preserve -t/-m/--provider/--tui/--dev before chat subcommand
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.

Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.

Reproduction (origin/main, before fix):

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  None
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

After fix:

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  'web'
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.

Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
  (not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
  `-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
  parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.

Fixes #28780.
2026-07-11 03:51:43 -07:00
kshitijk4poor
d83cd6f7c3 fix(security): secure Azure catalog probes 2026-07-11 12:28:55 +05:30
kshitijk4poor
1f46145e03 fix(security): order sanitizer after installed hooks 2026-07-11 12:28:55 +05:30
kshitijk4poor
92c2146039 fix(security): sanitize after installed request hooks 2026-07-11 12:28:55 +05:30
kshitijk4poor
4530a4ca4c fix(security): preserve opener-level header policy 2026-07-11 12:28:55 +05:30
kshitijk4poor
cf34a1e8c7 fix(security): cover remaining catalog credential paths 2026-07-11 12:28:55 +05:30
kshitijk4poor
27a1042b13 fix(security): preserve installed urllib policies 2026-07-11 12:28:55 +05:30
kshitijk4poor
6e75ba7fa0 fix(security): enforce one redirect credential policy 2026-07-11 12:28:55 +05:30
dsad
b8dd1bf3a5 test(models): preserve catalog urlopen monkeypatches 2026-07-11 12:28:55 +05:30
dsad
5415b77658 fix(models): strip credentials on catalog redirects 2026-07-11 12:28:55 +05:30
brooklyn!
291eae63b7
Merge pull request #62016 from NousResearch/bb/desktop-vibe-hearts
feat: vibe reactions — floating hearts on affection, across CLI/TUI/desktop
2026-07-10 16:14:27 -05:00
gigakun3030
dfdc3156fb
fix(models): remove unavailable OpenCode Zen free models (#61163) 2026-07-10 05:25:50 -07:00
kshitijk4poor
0e67c7231d fix(auth): validate and persist shared Nous routing 2026-07-10 17:50:43 +05:30
kshitijk4poor
03b8a00e26 fix(auth): recompute Nous routing after shared recovery 2026-07-10 17:50:43 +05:30
Sami Rusani
ca6513542d fix(auth): recover runtime Nous token from shared store 2026-07-10 17:50:43 +05:30
Brooklyn Nicholson
0e2adf9dad feat(gateway,cli): emit + consume the reaction signal
tui_gateway forwards reaction_callback as a `reaction` event (shared by the TUI
and the desktop app). The interactive CLI wires reaction_callback to flash the
pet's celebrate ("jump") pose — the CLI's analogue of hearts.
2026-07-10 05:42:04 -05:00
kshitijk4poor
0629caac62 fix(model): derive catalog policy from declarations 2026-07-10 13:10:30 +05:30
luyifan
5f00f36ba9 fix(model): probe no-key custom provider catalogs 2026-07-10 13:10:30 +05:30
Brooklyn Nicholson
301acc9eaa fix(web): paste/drop images into dashboard Chat via HERMES_HOME/images
Dashboard Chat is an xterm mirror of a TUI inside the gateway, so
server-side clipboard.paste never sees the browser clipboard. Upload
pasted/dropped images to the profile's images/ dir (same place
clipboard.paste / image.attach use), then drive /image over the PTY.

Uses a dedicated /api/chat/image-upload endpoint (magic-byte check,
25MB cap, profile scope) instead of relative managed-files uploads that
400 on local dashboards without a locked root. Ctrl/Cmd+Shift+V also
tries clipboard.read() for images before falling back to text, since
preventDefault on that chord suppresses the DOM paste event.

Salvages #57912 (client composition + /image PTY drive) and folds in
#48563's upload endpoint + drop path.

Co-authored-by: bird <6666242+bird@users.noreply.github.com>
Co-authored-by: tt-a1i <53142663+tt-a1i@users.noreply.github.com>
2026-07-10 02:20:04 -05:00
brooklyn!
9cb2a8abb0
Merge pull request #60757 from giggling-ginger/bugfix/issue-hunt-20260708
fix(desktop): keep configured MoA presets in model picker
2026-07-10 01:49:00 -05:00
kshitijk4poor
f82c71396d fix(cron): scope profile runtime during webhook fire 2026-07-10 11:43:09 +05:30
embwl0x
ec0227b435 fix(cron): isolate profile store paths by context 2026-07-10 11:43:09 +05:30
teknium1
07271a6f62 fix(tools_config): widen null guard to known_plugin_toolsets write path
Sibling of the salvaged #53196 read-path fix: setdefault() does not
replace a present-but-null key, so saving platform tools with
known_plugin_toolsets: null in config.yaml crashed on indexing None.
2026-07-09 20:28:44 -07:00
sonxi
df886d0a45 fix(tools_config): guard against None in known_plugin_toolsets config
When config.yaml has known_plugin_toolsets set to null (or any value
mapped to None by the YAML loader), config.get returns None (dict.get
only falls back to the default when the key is absent, not when its
value is None). The subsequent set(known_map.get(platform, [])) then
crashes with TypeError: NoneType object is not iterable and the gateway
fails to start, even though no plugin toolsets are configured.

Add or-empty-dict and or-empty-list guards so a null/None value is
treated as empty instead of crashing the platform-tools resolver.
2026-07-09 20:28:44 -07:00
Teknium
4c03032a24
fix(cli): normalize malformed skills config in get_disabled_skills (#61797)
skills: null crashed with AttributeError, and a bare scalar
disabled: my-skill was split into a set of characters. Both now
normalize the same way agent.skill_utils._normalize_string_set does:
null -> empty set, scalar -> single-item set. Non-dict skills
sections are ignored.

Closes #13026.
2026-07-09 19:57:54 -07:00
Koho Zheng
a4dd08a977 fix(session-export): escape html tool call names 2026-07-09 19:54:02 -07:00
briandevans
b5c655c89e fix(cli): normalize role into a single CSS token for the message class
Addresses Copilot review on #61348: the HTML-escaped role, while safe from
injection (quotes are escaped), still contains whitespace when a crafted role
is supplied, which splits the class attribute into several unintended CSS
classes. Keep the escaped role for the display badge, and reduce the raw role
to a single safe CSS token (alnum/-/_) for the class name. Real roles
(user/assistant/system/tool) are unchanged, so the existing .message-<role>
rules still match.
2026-07-09 19:54:02 -07:00
teknium1
46613071e4 fix(config): widen empty-section guard to _deep_merge in load_config
Sibling site of the load_cli_config fix (#58277): _deep_merge treated a
YAML-null section (terminal: with no value) as an override, replacing
the entire DEFAULT_CONFIG dict for that section with None. Every
downstream consumer expecting a mapping was a latent crash, and default
sub-keys were silently lost. A None override of a dict default is now
ignored, matching the CLI loader's behavior. Scalar-null overrides are
unchanged.
2026-07-09 18:23:18 -07:00