Commit Graph

291 Commits

Author SHA1 Message Date
kshitij 4a3942d948 fix: show explicit member spend cap message instead of 'no credits'
When the Nous Portal returns paid_service_access.allowed=false with
reason=member_spend_cap_exceeded, Hermes was falling through to the
generic 'no active subscription or usable credits' message — even
though the user has ample purchased credits and the real blocker is
an org-level per-member spend cap.

This adds a dedicated branch that surfaces the actual cause: names the
spend cap, shows the cap/spend amounts, and tells the user to ask their
org admin to raise it. Also adds member_spend_cap_exceeded to the
billing error code set so the error classifier and auth error formatter
route it through the Nous entitlement message path.
2026-08-07 19:47:40 +05:30
Justin Bennington a9acb400ba feat(providers): add Actual Computer inference provider 2026-08-05 14:08:32 -07:00
kshitijk4poor 219bb35c35 fix(minimax-oauth): read streamed error bodies inside the client context + real-transport tests
Follow-ups on the salvaged bounded-read fix:
- refresh flow: the non-200 branch reads a STREAMED body, which fails
  (ReadError/StreamClosed) once the httpx.Client context has exited —
  moved inside the context. Repro + regression test use a real socket
  server (MockTransport buffers in memory and cannot catch this).
- truncation guard: >limit bodies end with ...[truncated] (mutation-checked
  against the is_stream_consumed fallback).
- test mocks now model the streamed-read surface (is_stream_consumed,
  iter_bytes, client.send) so non-200 paths exercise the real bounded read.
2026-08-03 17:38:13 +05:30
luyifan 94ef36a7f7 Bound MiniMax OAuth error responses 2026-08-03 17:38:13 +05:30
kshitijk4poor 0e4daade14 perf(zai): early-exit when the highest-priority endpoint wins (simplify finding)
The as_completed drain + `with` join made the parallel version WORSE than
sequential main in the common case (first endpoint succeeds fast, others
slow/unreachable): main returned at first success, the parallel version
waited for every straggler. Now: after each completion, walk endpoints in
priority order and return as soon as a success is unbeatable (all
higher-priority probes already finished); pool uses shutdown(wait=False) so
losers drain in the background. Mutation-checked: removing the early exit
makes the new timing test fail (8.2s vs <1.5s).
2026-08-03 17:37:00 +05:30
light-merlin-dark 9891f4b63f perf(zai): parallelize endpoint detection probes
Z.AI has separate billing for general vs coding plans and global vs
China endpoints. On startup, detect_zai_endpoint() probes up to 4
endpoints sequentially with 8s timeout each, taking 8-9 seconds when
the first endpoints return non-200 (rate limited) before a working one
is found.

Replace the sequential loop with concurrent.futures.ThreadPoolExecutor
to probe all 4 endpoints in parallel. Results are returned in
ZAI_ENDPOINTS priority order so the preference chain is preserved.

Benchmark on macOS M4 Max, Python 3.11, Hermes v0.8.0:
  Before: 8.8s (sequential: global=0.9s/429, cn=1.6s/429,
           coding-global=4.3s/200, coding-cn=2.0s/200)
  After:  ~4.5s (single round-trip, bounded by slowest endpoint)

Signed-off-by: Merlin <merlin@merlin.me>
2026-08-03 17:37:00 +05:30
kshitijk4poor 06b4f64c31 fix(auth): memoize the valid-token fast path too, add memo tests
Follow-ups on the startup-burst memo:

- Populate the memo on the valid-token fast path as well. The startup
  burst usually finds a VALID token, and each check_fn call still paid
  two cross-process file locks + state reads to reach that return; the
  original memo only engaged after a refresh. The token has at least
  refresh_skew_seconds (>=120s) of life at that return, so a 5s memo can
  never serve an expired token.
- Clear the module-level memo in test_nous_portal_staging_allowlist's
  refresh-capture helper: with the fast-path populate, a token memoized
  by an earlier test would otherwise short-circuit the refresh these
  tests assert on (3 tests failed without this).
- Add dedicated memo behavior tests (TTL hit, TTL expiry, insecure
  bypass) — the original PR shipped none. Mutation-checked: all 3 fail
  against main's un-memoized function, pass on this branch.
2026-08-02 22:13:12 +05:30
JeffStone69 9929743b72 fix(auth): memoize resolve_nous_access_token to collapse startup burst
check_tool_availability runs once per managed-tool check_fn (browser,
image_gen, etc.) during banner render. Each one independently triggers a
~15s blocking Nous Portal token-refresh network call when the stored token
is expired. On a slow/constrained host (e.g. a small monitoring CT) that
serial burst stretched startup to many minutes, appearing 'stalled'.

Add a per-process memo (5s TTL) so the burst collapses into a single network
round-trip. Only successful, non-forced resolutions are cached; force_fresh
and insecure/ca_bundle callers bypass and don't populate the cache, so
normal refresh semantics are unchanged.

Verified: 3 rapid resolve_nous_access_token() calls -> 1 underlying refresh.
2026-08-02 22:13:12 +05:30
kshitijk4poor 840fb55a8a fix(auth): enrich device-auth timeout at the source to cover the dashboard poller
/simplify-code review found _poll_for_token has a second caller:
web_server._nous_poller (dashboard/desktop device login), which surfaces
str(e) as the UI error_message — so wrapping only in
_nous_device_code_login left the dashboard showing the bare timeout.

Move the enrichment into _poll_for_token's deadline raise so every
caller inherits the guidance, and drop the now-redundant try/except
wrap in the CLI login. Add a source-level regression test driving the
real poll loop (authorization_pending stub client) to the deadline.
2026-08-02 13:32:41 +05:30
HexLab fbf26e3845 fix(auth): actionable CAPTCHA-aware guidance on Nous device-auth timeout
A bare 'Timed out waiting for device authorization' gives the user
nothing to act on. The most common cause is Portal sign-in failing in
the opened browser tab (including the server-side CAPTCHA loop from
issue #20605), so point at the Portal login page and the hermes portal
retry command.

Salvaged from PR #75290 by @HexLab98 (timeout-guidance kernel only).
The URL-rewrite portion of that PR was dropped: the live Portal has no
/device route (verified 404 with a real user_code), so rewriting the
manage-subscription verification URL would break login entirely.
Guidance text reworded to reference only real URLs.
2026-08-02 13:32:41 +05:30
MaxFreedomPollard 31032b4f51 fix(auth): a transient read failure is not corruption
_load_auth_store() treated every exception from reading auth.json as
corruption and returned an empty store. EMFILE under fd exhaustion,
EACCES, EIO and a stalled network mount all reached that branch. This
module does read-modify-write in roughly fifteen places, so the empty
store was one _save_auth_store() away from erasing every stored
credential.

Separate OSError from parse failure: a file that exists but cannot be
read now raises, naming the real cause and leaving the file on disk
untouched. Only a genuine parse failure takes the preserve-and-start-
empty branch, which is unchanged.

The backup was also unreliable in exactly the conditions that triggered
it: shutil.copy2 opens a file, so under EMFILE it failed too, its bare
except swallowed that, and the log still said "Corrupt file preserved
at ..." when nothing had been written. Track whether the copy landed
and say so accurately.
2026-07-31 22:34:52 -07:00
praneshnikhar 35b44e0d5f fix(auth): apply same source-path write-through fix to non-pool xAI path
_save_xai_oauth_tokens had the identical self-sealing bug as
_sync_device_code_entry_to_auth_store: key-presence check before
_store_provider_state, which unconditionally creates the key.
Use _load_provider_state_with_source to decide write-through from
the actual grant source, not key presence.

Also update regression test per review: use the real
_write_through_provider_state_to_global_root helper and assert
rotated token pair values in the root store after each refresh
instead of just counting mock calls.
2026-07-31 22:32:48 -07:00
Teknium ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
teknium1 5b751dc0ad chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across
root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/
(tests/, plugins/, skills/ excluded).

Intentionally KEPT (false positives / test-patch surfaces):
- agent/transports/__init__.py package re-exports
- cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area,
  used by tests/cli/test_cli_browser_connect.py)
- hermes_cli/main.py _prompt_auth_credentials_choice /
  _model_flow_bedrock_api_key (accessed via main_mod attr in tests)
- gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports
  and _PORT_BINDING_PLATFORM_VALUES (test-referenced)
- hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and
  _OAUTH_TOKEN_URL availability probe
- hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811
  chain) and yaml availability-probe import
- hermes_cli/nous_subscription.py managed_nous_tools_enabled
  (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled)
- try/except ImportError availability probes (env_loader, tts_tool,
  mcp_tool, web_server anthropic OAuth block)
- tools/web_tools.py noqa F401 re-exports
- hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible
  missing-guard bug, flagged for separate review
- unused function parameters (signature changes out of scope)

Side-effect RHS calls preserved where only the binding was dead
(e.g. web_server proc = _spawn_hermes_action -> bare call).
2026-07-29 11:53:39 -07:00
teknium1 7c198c5e44 refactor: single shared Retry-After parser 2026-07-29 10:13:50 -07:00
Teknium 5ea2e0a0bc fix(auth): use refresh-free Nous status snapshot on read-only display paths
Follow-up widening of the /api/status fix: add get_nous_auth_status_local(),
a refresh-free auth-store snapshot (local invoke-JWT decode only), and use it
on the read-only display surfaces that previously called
get_nous_auth_status() -> resolve_nous_runtime_credentials() -> live OAuth
refresh POST:

- hermes_cli/status.py  (hermes status auth-provider panel)
- hermes_cli/doctor.py  (hermes doctor auth-provider checks)
- hermes_cli/portal_cli.py  (hermes portal status display)
- hermes_cli/web_server.py  /api/portal endpoint and the accounts-tab
  provider card dispatcher (_resolve_provider_status nous branch)

Action paths (login flows, portal operations needing a live credential)
keep using get_nous_auth_status(). Part of NS-592.
2026-07-28 22:42:19 -07:00
Shannon Sands 0764163f80 fix(auth): stop JWT refreshes from status polling 2026-07-28 22:42:19 -07:00
Fangliquan 0d0ad3f9d9 fix(hermes_cli): lock dashboard xAI active_provider contracts and setup unsuppress
Cover preserve-vs-mark-if-unset for dashboard OAuth, assert TTS setup clears
device_code suppression, and clarify set_active docstring callers.
2026-07-28 11:54:01 -07:00
Fangliquan fce06e909d fix(hermes_cli): keep TTS/setup xAI OAuth from switching active chat provider
Save side-tool OAuth tokens without promoting xai-oauth via active_provider
or model.provider so hermes setup tts login no longer hijacks inference routing.
2026-07-28 11:54:01 -07:00
HexLab98 c63e0cd3e3 fix(models): match bare Kimi Coding k3 when searching kimi
Kimi Coding discovers the flagship as wire id `k3`. Picker search used
only that id, so typing "kimi" hid it next to every other kimi-* model.
Add picker-only search aliases without changing the wire id.
2026-07-26 21:22:40 -07:00
annguyenNous efaba061fb fix(cli): add explicit encoding to read_text/write_text calls
Path.read_text() and Path.write_text() without explicit encoding
default to the system locale encoding. On Windows this is typically
cp1252, which causes UnicodeDecodeError for UTF-8 content (JSON
configs, user data, service scripts).

Add encoding="utf-8" to all read_text() and write_text() calls
across 8 CLI files, matching the pattern established in PR #50534
(security_audit_startup.py) and ruff rule PLW1514.

Fixed files:
- main.py: 4 read_text calls
- auth.py: 3 read_text calls
- banner.py: 1 read_text + 1 write_text
- service_manager.py: 1 read_text + 4 write_text
- container_boot.py: 1 read_text + 4 write_text
- doctor.py: 3 read_text calls
- uninstall.py: 2 read_text calls
- gateway.py: 1 write_text call
2026-07-24 17:10:39 -07:00
srojk34 3ea35d6711 fix(vertex,moa): register vertex in PROVIDER_REGISTRY and HERMES_OVERLAYS
The Vertex AI provider (added same-day, commit c73e74386) was never added to
either of the two provider registries that agent/auxiliary_client.py and the
MoA slot-resolution chain depend on, breaking Vertex outside the main
conversation loop:

1. hermes_cli/auth.py::PROVIDER_REGISTRY had no "vertex" entry. The
   plugin-auto-extend loop that normally fills gaps explicitly skips
   non-api_key auth types (`if _pp.auth_type != "api_key": continue`), and
   Vertex was never hand-declared like "bedrock" is. Because
   resolve_provider_client() in agent/auxiliary_client.py gates everything
   on `pconfig = PROVIDER_REGISTRY.get(provider)` and returns (None, None)
   immediately when pconfig is None, its `elif pconfig.auth_type == "vertex"`
   branch was permanently dead code — every auxiliary Vertex call (vision,
   title generation, reflection, context compression, MoA reference/
   aggregator slots) failed outright, not just a MoA-specific edge case.

2. hermes_cli/providers.py::HERMES_OVERLAYS also had no "vertex" entry, so
   hermes_cli.providers.get_provider("vertex") returned None. This backs
   _preserve_provider_with_base_url() in agent/auxiliary_client.py, which a
   MoA slot's resolved (base_url, api_key) pair needs to keep its "vertex"
   identity instead of silently collapsing to "custom" — losing the
   identity _refresh_provider_credentials() needs to re-mint an expired
   OAuth2 token (~1h lifetime) on a 401, and permanently breaking every
   subsequent call in that MoA preset for the rest of the session.

Fix mirrors the existing "bedrock"/aws_sdk entries in both registries
exactly, plus adds a "vertex" branch to _refresh_provider_credentials() (it
had branches for openai-codex/nous/anthropic/xai-oauth but not vertex,
so a 401 fell through to `return False` without evicting the stale cached
client).

- hermes_cli/auth.py: hand-declared vertex ProviderConfig(auth_type="vertex")
  in PROVIDER_REGISTRY, matching bedrock's shape.
- hermes_cli/providers.py: vertex HermesOverlay(auth_type="vertex") in
  HERMES_OVERLAYS + "Google Vertex AI" label override.
- agent/auxiliary_client.py: vertex branch in _refresh_provider_credentials
  that re-mints the token via get_vertex_config() and evicts the stale
  cached client.
- 8 new regression tests across tests/hermes_cli/test_vertex_provider.py and
  tests/agent/test_auxiliary_client.py: registry membership, end-to-end
  resolve_provider_client("vertex", ...) building a working client (proving
  the previously-dead branch is now reachable), and the 401-refresh/cache-
  eviction path.
2026-07-23 16:55:41 -07:00
baenregod 3638136e7e fix(auth): count MoA preset slots as explicit provider configuration
A user who configured a provider only inside a MoA preset (advisor or
aggregator slot) has explicitly opted into that provider — the consent
gate (is_provider_explicitly_configured) now scans moa.reference_models,
moa.aggregator, and all moa.presets.* slots, so Claude Code OAuth pool
seeding and the auxiliary auto-fallback chain treat MoA-only Anthropic
users consistently with model.provider users.

Salvaged from PR #57778 (trimmed): the auxiliary_client fallback half of
the original PR was independently landed on main in ddd3a2d247 and is
dropped here; a secret-scrubber artifact in the gate test fixture is
restored to the real placeholder token.
2026-07-23 11:21:13 -07:00
Blade 3d67f00fe1 fix(credential-pool): stop lost-update cooldown erasure and wrong-key quarantine
Two related races in credential-pool cooldown state:

1. Lost update across processes: write_credential_pool merged only
   entries missing from the caller's snapshot; for entries present on
   both sides the caller's in-memory copy won wholesale. A process
   holding a snapshot taken before another process marked a key
   exhausted would, on its next persist (e.g. a round-robin rotation),
   write the key back as healthy — erasing the cooldown so every
   process resumes hammering a rate-limited key. Merge status fields by
   last_status_at recency: adopt the on-disk status only when it is
   strictly newer AND still binding (DEAD, or EXHAUSTED with an
   unexpired cooldown), and never onto re-authed (token-changed)
   entries, so legitimate expiry-clears and fresh logins are preserved.

2. Wrong-key quarantine: when mark_exhausted_and_rotate received an
   api_key_hint that matched no entry, it fell through to
   current()/_select_unlocked() — on a freshly loaded pool that selects
   the NEXT healthy key and benches it for the full cooldown TTL,
   punishing an innocent credential. When a hint is provided but
   unmatched, rotate without marking anything instead of guessing.

Includes regression tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 09:22:02 -07:00
rob-maron 0ee05d72f2
Nous portal model pricing (#69579)
* nous portal model pricing

* update top message
2026-07-22 16:47:06 -04:00
Teknium fea838c9f2
fix(auth): detect upstream Codex quota resets and lift stale pool cooldowns (#69494)
When Codex returns 429 usage_limit_reached, Hermes persists the provider's
reset_at on the pool entry and freezes the credential until it elapses --
which can be days out for weekly windows. But the upstream window can
reopen EARLY: the user redeems a banked rate-limit reset (Codex CLI /
ChatGPT UI), upgrades their plan, or OpenAI resets the window. Hermes
never re-checked, so it kept erroring with 'Codex provider quota
exhausted (429); retry after Ns' until a manual re-auth rewrote the
tokens (issue #43747, externally-reset variant).

- hermes_cli/auth.py: add _probe_codex_quota_restored() -- a throttled
  (5 min/token) GET of the Codex /usage endpoint; quota counts as
  restored when every reported window is <100% used. Add
  clear_codex_pool_quota_cooldowns() to lift 429/quota-shaped cooldowns
  from persisted pool entries (DEAD and auth-shaped entries untouched).
- resolve_codex_runtime_credentials(): before surfacing a pool-only
  cooldown as 'quota exhausted', probe upstream; on a positive probe
  clear the cooldown and return the pool credential.
- agent/credential_pool.py: _available_entries() probes frozen
  openai-codex entries (clear_expired path only) and unfreezes them when
  upstream confirms the reset.
- agent/account_usage.py: a successful /usage reset redemption now
  clears persisted pool cooldowns immediately.

Negative paths preserved: probe 429/exhausted/indeterminate keeps the
cooldown; read-only enumeration never probes; non-JWT tokens never
probe (no network in hermetic tests).
2026-07-22 10:58:22 -07:00
Siddharth Balyan b0da653ac8
fix(billing): rename user-facing "terminal billing" copy to Remote Spending (#68355)
* fix(billing): rename user-facing "terminal billing" copy to Remote Spending

The capability was renamed Remote Spending on the portal (consent CTA:
"Allow Remote Spending"; per-terminal states Granted/Stopped), but the
terminal, desktop, and docs still said "terminal billing" everywhere.

- Feature name: Remote Spending in titles/labels, lowercase mid-sentence.
- Step-up action verb is now "allow", matching the portal consent CTA.
- Kill-switch-off recovery copy points at the actual control ("a billing
  admin can turn it on from the portal's Hermes Agent page") instead of
  the dead-end "manage it on the portal".
- Per-terminal revoke copy uses the portal vocabulary ("stopped").
- Wire identifiers (cli_billing_enabled, cli_billing_disabled, ...) are
  unchanged; copy, comments, docs, and test expectations only.

* fix(billing): correct the post-step-up denial diagnosis + finish the desktop rename

Adversarial review findings: (1) a repeated insufficient_scope after a
successful step-up is a per-terminal authorization failure, but the copy
blamed the org kill-switch and pointed at the wrong recovery control —
now: "Remote Spending still isn't active for this terminal — the
authorization didn't take. Retry, or make this change on the portal."
(2) the desktop step-up flow started in Remote Spending vocabulary but
finished in "billing management access" — renamed both end states.
(3) prettier formatting on the touched files (matches the post-merge
fmt bot).
2026-07-21 12:20:25 +05:30
Gille 7cb2d2cd4a
fix(auth): detect configured providers absent from registry (#66017)
* fix: detect env-var-configured providers absent from PROVIDER_REGISTRY

is_provider_explicitly_configured() only checked PROVIDER_REGISTRY (a
manually-maintained dict) for env-var names. Providers that exist solely
in the models.dev catalog — e.g. openrouter — were never recognised as
explicitly configured, so they were filtered out of the desktop model
picker even when their API key was set in .env.

Add a fallback to get_provider() (which reads the models.dev catalog)
when PROVIDER_REGISTRY returns None. Both ProviderConfig and ProviderDef
expose .auth_type and .api_key_env_vars with the same shape.

* test: keep OpenRouter provider gate assertion behavioral

* chore(release): map salvaged OpenRouter contributor

---------

Co-authored-by: zzpigpinggai <zzpigpinggai@users.noreply.github.com>
2026-07-16 22:56:45 -04:00
Dan Schnurbusch a7a05024c1 fix(auth): key reentrancy by auth store path
Remove the dynamic active-store holder so a profile context switch cannot inherit another auth store's lock depth and skip its kernel lock.
2026-07-16 06:14:56 -07:00
Dan Schnurbusch f68fd80f41 fix(auth): preserve fallback routes and OAuth state
Switch provider and model together after setup-time auth failure. Serialize global auth-store merges under target-specific locks and preserve auth-to-shared lock ordering for profile OAuth refreshes.
2026-07-16 06:14:56 -07:00
Ben Barclay 5fc2d9e64d
docs(auth): scrub Fly.io host detail from quarantine-log comment (#60145)
hermes-agent is public/OSS; the forensic-logging comment in
_quarantine_nous_oauth_state named 'Fly' (the specific managed-hosting compute
provider) twice. Reword generically ('a hosted agent', 'a managed log drain may
be WARNING-only') — the behaviour is unchanged, only the comment. Follows the
same scrub applied to the boot re-seed helper (#59983) before merge; this one
slipped through in #59976.
2026-07-16 08:44:43 +10:00
kshitijk4poor 899e420ab9 refactor(upstage): drop manual auth/models registrations covered by profile auto-extend
PROVIDER_REGISTRY, its alias map, and CANONICAL_PROVIDERS all auto-extend
from registered ProviderProfiles since the provider-modules refactor
(20a4f79ed). Verified with real imports: registry entry, 'solar' alias
resolution via resolve_provider(), and the picker entry are identical
with the manual entries removed. The hermes_cli/providers.py overlay
stays (models.dev has a stale /v1/solar base URL and no UPSTAGE_BASE_URL
var), and the manual OPTIONAL_ENV_VARS entries stay (non-advanced key +
curated prompt text, matching the fireworks convention).
2026-07-15 00:09:24 +05:30
Changhyun Min 20502b407c feat(agent): add Upstage Solar as a model provider
Adds Upstage Solar as a bundled model-provider plugin. Solar exposes an
OpenAI-compatible chat-completions endpoint at https://api.upstage.ai/v1, so
the generic chat_completions transport handles request/response/streaming/tool
calls — the profile is the core integration.

Provider registration (Upstage isn't in models.dev, so each registry that does
not auto-wire from the plugin layer needs an explicit entry — same pattern as
nvidia/gmi):
- plugins/model-providers/upstage/: UpstageProfile + plugin.yaml. Picker default
  and offline catalog list only the agentic Solar Pro models, led by `solar-pro`
  (rolling alias for the latest Pro). default_aux_model empty so aux tasks use
  the main model. `solar` alias. UPSTAGE_BASE_URL overrides the host.
- hermes_cli/providers.py: HERMES_OVERLAYS + label + `solar` alias, so
  resolve_provider_full('upstage') resolves (without this, an explicit
  `provider: upstage` in config was dropped and fell through to auto-detect).
- hermes_cli/auth.py: PROVIDER_REGISTRY entry + `solar` alias, so `hermes
  doctor` / resolve_provider recognise upstage (the static-registry path the
  lazy profile-extension doesn't reliably cover at validation time).
- hermes_cli/models.py: CANONICAL_PROVIDERS entry places Upstage Solar in the
  curated picker order (above the auto-appended `custom`).
- agent/model_metadata.py: context-window fallbacks (/v1/models omits
  context_length); `solar-pro` carries the 128K Pro context as the catch-all.

Reasoning: UpstageProfile.build_api_kwargs_extras wires Solar's top-level
`reasoning_effort` (low|medium|high; xhigh/max→high). Reasoning-capable families
are solar-pro* and solar-open*; solar-mini/syn-pro never receive it. Defaults ON
at medium when unset (matches the /reasoning "medium (default)" label);
`/reasoning none` disables; explicit/saved settings are honored. No
reasoning_content echo handling needed (unlike DeepSeek/Kimi).

Web dashboard:
- web/src/pages/EnvPage.tsx: add an "Upstage Solar" provider group so
  UPSTAGE_API_KEY / UPSTAGE_BASE_URL appear under LLM Providers (not "Other").

Docs/tests:
- .env.example: documents UPSTAGE_API_KEY / UPSTAGE_BASE_URL.
- tests: profile wiring, reasoning_effort mapping (pro/open/mini, efforts,
  disabled, default-on), provider-resolver regression (resolve_provider_full /
  get_provider / solar alias / overlay), `solar-pro` default.

Testing: pytest tests/providers tests/plugins/model_providers
tests/hermes_cli/test_upstage_provider.py tests/run_agent/test_provider_parity.py
tests/hermes_cli/test_api_key_providers.py; ruff clean. Verified end-to-end:
`hermes doctor` shows "Upstage Solar", and live chat works via both
`--provider upstage` and `--provider solar`. Reasoning wire format per
https://console.upstage.ai/api/docs/for-agents/raw. Platforms tested: macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
kshitijk4poor 2fc3f9c1ff fix(deepinfra): harden multimodal provider routing
Prevent credential forwarding across catalog redirects, retain explicit opt-in semantics for paid media backends, fail closed on invalid provider configuration, avoid mixed-catalog and output-limit assumptions, and reserve native STT provider names.
2026-07-14 02:59:39 +05:30
Georgi Atsev fe002eb124 feat(providers): Support DeepInfra as an LLM provider 2026-07-14 02:59:39 +05:30
teknium1 8c77206859 refactor(model): gate picker rows by runtime capability 2026-07-12 03:04:07 -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
kshitijk4poor 1192f29450 fix: Z.AI endpoint persist failure must not break URL resolution
Review findings (hermes-pr-review Phase 2, 3-angle):
- _save_auth_store() does real filesystem I/O (mkdir, O_EXCL create, fsync,
  atomic replace) and can raise on disk-full/permissions/lock-timeout. The
  persist ran bare in the success path, so a persist failure aborted
  _resolve_zai_base_url() after detection had already succeeded. Wrap the
  persist in try/except: log a warning and still return the detected URL
  (worst case: next start re-probes).
- Readability: stage the payload in a local detected_endpoint instead of
  writing through the stale pre-lock 'state' dict, which is no longer what
  gets persisted.
2026-07-08 17:33:40 +05:30
kshitijk4poor 6eeed3f1e8 fix: don't flip active_provider when caching Z.AI probe result
_save_provider_state() sets auth_store['active_provider'] as a side effect.
The Z.AI endpoint probe runs from credential-pool env seeding for any user
with a Z.AI key in env — persisting the probe cache must not silently make
zai the active provider. Use _store_provider_state(set_active=False).

Follow-up to PR #41201 salvage.
2026-07-08 17:33:40 +05:30
veradim 832c5f9bc9 Fix slow Z.AI startup by caching auto-detected endpoint to disk
(cherry picked from commit 6ed884933a)
2026-07-08 17:33:40 +05:30
Ronald Reis 37a4cf9000 fix: limit desktop model pickers to explicit providers 2026-07-07 15:12:54 -07:00
Ben 5eac665252 feat(status): expose nous_session_valid on /api/status for hosted-agent self-heal
A hosted agent whose Nous bootstrap session dies terminally (invalid_grant /
quarantine) looks HEALTHY to every liveness/connectivity probe — the machine,
relay ws, and dashboard all stay up — yet every inference turn hard-fails with
a provider-auth error until a human re-logs-in. Nothing currently surfaces that
condition to NAS.

Add get_nous_session_validity() (valid|terminal|unknown), classified from local
auth-store state (no working token required), and report it on the public
/api/status payload. NAS's 2-min health sweep reads it and re-mints the
bootstrap session in place on 'terminal'.

Anti-flap: only a terminal failure (relogin_required / persisted quarantine
marker with tokens cleared) maps to 'terminal'; transient/mid-rotation blips and
merely-expiring tokens report 'unknown' so a healthy box never triggers a
spurious re-mint.

Part of the hosted-agent bootstrap-session self-heal (NAS side reads this field).
2026-07-07 00:53:56 -07:00
Ben 444dc0da89 feat(auth): log forensic detail at Nous quarantine so terminal auth death is visible
A NAS-hosted Fly agent's Nous bootstrap session can take a terminal
invalid_grant and get quarantined in _quarantine_nous_oauth_state, which
clears the dead tokens from auth.json. Until now this quarantine was
completely silent: the only signal was a downstream "No access token found"
WARNING once the credential pool was already empty, which is too late to
root-cause. Because the Fly log drain is WARNING-only, nothing about the
terminal death reached centralized logging, and a real incident could not be
diagnosed because the evidence was never recorded.

Emit a WARNING+ forensic record AT the quarantine point, before the token
material is cleared. Fields: refresh_token hash prefix (12-char SHA-256 hex,
correlates to NAS's refreshTokenHash), client_id, agent_key_id, error code,
reason, auth.json path/size/mtime/exists, and whether the token was already
past its own expiry. WARNING level is deliberate — INFO never reaches the Fly
drain.

Redaction safety (load-bearing): the log dict is built only from computed
values (hash prefix, sizes, booleans). No raw refresh_token, access_token, or
agent_key bytes are ever passed into the log call, avoiding Hermes's known
credential-literal corruption bug class. A test asserts the raw refresh token
substring is absent from all emitted log output.

Note: no session_id field exists on Nous auth state; provenance is captured
via client_id + agent_key_id, which are non-secret routing identifiers.
2026-07-07 00:53:48 -07:00
luyifan b109adede6 fix(config): refuse unreadable config overwrites 2026-07-05 23:00:34 +05:30
Jaaneek 5ef0b8acb0 feat(auth): make xAI Grok OAuth device-code-only, drop loopback login
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.

- Poll the token endpoint with server-provided interval, honoring
  slow_down and expires_in; store tokens with auth_mode
  oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
  rotated tokens sync back to auth.json, the global root store, and the
  credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
  drop the duplicate dashboard pool entry so exactly one seeded
  device_code entry exists.
- Use the shared device_code source name for consistency with the
  nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
  pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
  --manual-paste flag from documented commands.
2026-07-02 13:17:41 -07:00
Ben Barclay 9be39de0f2
fix(auth): make HERMES_PORTAL_BASE_URL/NOUS_PORTAL_BASE_URL bypass the Portal host allowlist (#56864)
Ben caught that the initial approach (widening _NOUS_PORTAL_ALLOWED_HOSTS to
include the staging host) was the wrong fix -- env vars are supposed to
override the allowlist, mirroring how NOUS_INFERENCE_BASE_URL already
bypasses _ALLOWED_NOUS_INFERENCE_HOSTS via _nous_inference_env_override().

The actual bug: both resolve_nous_access_token and
resolve_nous_runtime_credentials read
`_optional_base_url(state.get("portal_base_url")) or os.getenv(...) or ...`
-- a plain `or` chain where the STORED state value wins first (short-circuits
before the env vars are even read), and then whichever value won gets run
through the same _NOUS_PORTAL_ALLOWED_HOSTS gate regardless of its source.
So a hosted agent stamped with HERMES_PORTAL_BASE_URL=<staging> in its env
AND a staging portal_base_url already persisted to auth.json would still
get silently rewritten to prod on every refresh, because the env var never
even got a chance to be consulted.

Revert the previous _NOUS_PORTAL_ALLOWED_HOSTS widening entirely --
staying prod-only preserves the allowlist's actual job (rejecting an
untrusted network-provided portal_base_url persisted to auth.json by a
compromised Portal response).

Add _nous_portal_env_override() (mirrors _nous_inference_env_override())
and restructure both call sites so the env override is checked FIRST and,
when set, wins outright and skips the allowlist gate entirely -- the
allowlist only ever runs against the fallback (stored-state-or-default)
path now.

Rewrote tests/hermes_cli/test_nous_portal_staging_allowlist.py to test the
actual fix: the helper function, and an end-to-end
resolve_nous_access_token proof that the env override wins even when state
ALSO has the staging host stored (the exact incident shape), that it wins
over a stored PROD host too, and that the allowlist's heal-to-prod
behaviour for an untrusted stored value is preserved when no override is
set.
2026-07-02 06:52:46 +00:00
HODLCLONE 70f8b96d17 fix: preserve Nous runtime auth path label 2026-07-01 05:06:00 -07:00
HODLCLONE 6ed2f5d76f fix: make Nous Portal access token resolution resilient
- Track auth store source path on Nous state reads and write rotated
  OAuth refresh tokens back to the same store, preventing stale-token
  replays when Hermes falls back to a global/root auth.json.
- Skip Nous fallback entries locally when no access/refresh token is
  present, suppressing repeated failed resolution attempts within a
  session.
- Sync session model metadata after fallback switches so the gateway
  DB reflects the backend that actually served the latest turn.
2026-07-01 05:06:00 -07:00