Commit Graph

49827 Commits

Author SHA1 Message Date
Austin Pickett
a6b65df5bd fix(tui): fall back to checkpoints config when HERMES_TUI_CHECKPOINTS unset (#79625)
Desktop sessions (via 'hermes serve' -> TUI gateway) never set
HERMES_TUI_CHECKPOINTS, so agent creation gated filesystem checkpoints on
that env var alone: checkpoints.enabled: true in config.yaml was silently
ignored on the desktop surface while the CLI and messaging gateway honored
it. _make_agent now resolves enablement via the new topical sibling
tui_gateway/checkpoints.py: the env var (explicit '--tui --checkpoints'
opt-in) wins when present, otherwise fall back to the checkpoints config
section with DEFAULT_CONFIG defaults, mirroring gateway/run.py
_checkpoint_agent_kwargs. The pure _resolve_checkpoint_hash moves to the
same sibling to offset the facade's line cap.

Reimplements https://github.com/NousResearch/hermes-agent/pull/79743 by
@thatssoheil (its branch pre-dates current main and no longer applies);
test coverage carried over from that PR.
2026-10-06 14:51:15 -04:00
Teknium
1070b91da6 Revert "plugin-catalog: add tirith (opt-in pre-exec scanning, moved out of core)"
This reverts commit 945ffcd257.

Hermes does not maintain a tirith plugin. If upstream ships one, it goes through
the normal catalog admission under their name.
2026-10-06 11:49:00 -07:00
kshitijk4poor
3fc3caeb98 fix(relay): native Anthropic stream spans keep their input and cache token counts
On the native Anthropic stream, every Relay LLM span exported prompt, cache-read and
cache-write token counts as missing; only output tokens survived. The SDK's
MessageDeltaUsage serializes its unset fields (input_tokens, cache_read_input_tokens,
cache_creation_input_tokens) as null, and AnthropicStreamAccumulator's message_delta merge
let those nulls overwrite the real values captured from message_start. The Anthropic SDK's
own accumulator merges only non-null delta fields; ours now does the same. Hermes' own
session accounting was unaffected (it reads the SDK snapshot); the loss was the Relay
trace, which is what cache-reuse analysis of exported traces runs on.
2026-10-07 00:18:10 +05:30
Brooklyn Nicholson
9ab4a4fdbb fix(desktop): extract recovery-request toast handling into useRecoveryRequestToasts hook 2026-10-06 13:39:19 -05:00
Jakub Wolniewicz
5736ea3782 fix(desktop): decouple background queues from foreground connectivity 2026-10-06 13:39:19 -05:00
Brooklyn Nicholson
3f62bc5342 feat(desktop): full-width butterbar above the statusbar 2026-10-06 13:24:06 -05:00
Brooklyn Nicholson
a10faf9e5a feat(desktop): butterbar store — register notices from anywhere 2026-10-06 13:24:06 -05:00
Sergei Kabuldzhanov
fd4dc86146 feat(kanban-dashboard): show profiles on disk as lanes even when idle
`GET /api/plugins/kanban/board` returns an `assignees` list used by the
dashboard for the lane-by-profile sub-grouping in the running column.
Previously this came from a `SELECT DISTINCT assignee FROM tasks WHERE
... status != 'archived'` query, so a freshly-added profile only got
its own lane after being assigned its first non-archived task.

Switch the lookup to `kanban_db.known_assignees()`, the helper that
already backs the `/assignees` picker endpoint at plugin_api.py:1660.
It unions profiles configured on disk with profiles currently holding
non-archived tasks, so a new profile appears in the lane set
immediately. This also keeps the two endpoints from drifting —
previously they could disagree about who's "known."

Updates `test_board_empty` to reflect the new semantics: the implicit
`default` profile that the fixture's `HERMES_HOME` creates now shows
up in `assignees` even on an empty board.

Test plan:
  - `scripts/run_tests.sh tests/plugins/test_kanban_dashboard_plugin.py`
    — 94/94 pass (was 43/44 pre-test-update).
  - Manual: add a new profile via `hermes profile create foo`, then open
    the dashboard kanban; `foo` appears as an empty lane in the running
    column without needing to assign it work first.

Platforms tested: Linux (WSL2). Pure Python, no platform-specific code.
2026-10-06 14:23:10 -04:00
Austin Pickett
a39c2610e5 fix(dashboard): bracket IPv6 URL authorities everywhere hosts compose URLs
An IPv6 literal followed by :port is only a valid URL authority inside
brackets; http://::1:9119 is unparseable by browsers and urllib. Add
hermes_cli/url_utils.format_url_host and use it at every site that
builds an http URL from a (possibly IPv6) bind host: the named-profile
dashboard attach URL, the browser auto-open URL, the startup banner,
the plugin-activation loopback request, the proxy listening banner, and
the gateway-migration listener URLs. Zone identifiers (%eth0) are
percent-encoded per RFC 6874.

Fixes #67367

Co-authored-by: sjungwon03 <sjungwon03@gmail.com>
2026-10-06 14:23:00 -04:00
Teknium
32a79afb27 Merge pull request #132361 from NousResearch/fix/update-commit-point
fix(update): make the git/ZIP swap a single crash-safe commit point
2026-10-06 11:11:39 -07:00
ethernet
59956e6b07 fix(nix): remove ELECTRON_OZONE_PLATFORM_HINT, clean up args
this hasn't worked since electron 38.
2026-10-06 14:11:29 -04:00
ethernet
fa2dcb1077 fix(desktop): don't use xwayland on nvidia open kernel module
#127048 detected both modern nvidia open kernel drivers AND propetiary
drivers as the proprietary one, disabling wayland.

this fix looks for the "open kernel module" string in
/proc/driver/nvidia/version, and lets Wayland rendering continue to be
used.
2026-10-06 14:11:29 -04:00
Teknium
e032fdbca1 refactor(telemetry): registry prefixes come from the hub adapters; update reasons use tables
- `skills_hub._registry_prefixes` builds the identifier prefixes from the adapters' own
  SOURCE_IDs and `SkillsShSource._ID_PREFIX_ALIASES`, case-insensitively, so `skills.sh/...`,
  `skils-sh/...` and `OFFICIAL/...` stop reading `unresolved`. A matched source is named by
  `source_id()`, the method the router and the contract test use; two test fakes gain it.
- `update_failure_class`: exception type -> class and fixed stop-reason prefix -> class are
  tables instead of an if-ladder. `Windows gateway recovery failed: ...`, the fixed prefix
  update_completion writes when the post-update gateway resume fails, now reads
  `restart_failed` instead of `other` (prefix only, never the message).
- `mcp.servers.add` records through `mcp_rpc_helpers.record_mcp_add`, dropping the 175-column
  noqa line that kept methods_tools.py at its cap (2011 lines now).
2026-10-06 11:10:43 -07:00
Teknium
d6813a5dd7 fix(telemetry): extension failure classifiers are inert; the contract rejects contradictory install rows
- `_publish_failure_class`, `probe_failure_class` and `exception_failure_class` read a raise
  site's `failure_class` with `inspect.getattr_static` (`tagged_failure_class`), and the MCP
  type walk falls back to `connect_failed`. A third-party exception whose `__getattr__` or
  property raised replaced the user's PluginOperationError with RuntimeError (they run at the
  raise site, collection on or off), and the metric row was dropped; it now reads `exception`.
- `counter_dimensions_are_valid` adds the cross-field rule the per-field enums cannot state:
  failure_class in the kind's own set, `none` exactly on success, registry only on skill rows.
  Pre-split rows (no failure_class/registry) still validate.
- A refused dependency consent returns its reason as `_ConsentRefusal`, a str carrying the
  closed class (`non_interactive`, `deps_declined`, `manifest_invalid`), so publication no
  longer classifies by matching the user-facing copy. The text the user reads is unchanged.
- `extension_registry` imports the hub's `_SOURCE_ID_ALIASES` instead of a hand copy.
- test file: schema read uses utf-8-sig (windows-footguns).
2026-10-06 11:10:43 -07:00
Teknium
d3674513c8 docs(telemetry): the send explainer and Desktop consent window name the fresh-install note
The fresh-install receipt is kept from before the consent answer and counted
after an opt-in, so "data from before you opt in stays on this machine" was
false for it on the CLI send explainer and Desktop's consentWindow line in all
9 locales; only the pre-chat offer named the exception. Both now carry the same
"apart from the fresh-install note (noted on this machine and counted only once
you opt in)" clause. No i18n key changes; the CLI lines stay under 80 columns.
2026-10-06 11:10:43 -07:00
Teknium
ce14639879 fix(telemetry): a partial update with a failed build or restart keeps that failed_stage
update_cmd_fleet._verify_fleet_after_update writes `partial` for a failed build
(`not update_complete`) and for an incomplete restart as well as for a
verification gap. The relabel marked the synthetic verify stage failed for every
`partial`, so failed_stage moved from build/restart to verify and every such run
added a failed hermes.update.stage verify row. Verify is now failed by `partial`
only when no stage mark already failed (a stale/down fleet row still fails it).
The appendix row states the rule.
2026-10-06 11:10:43 -07:00
Teknium
717d8a3e5e fix(install-metrics): count the install note on a consented day; purge receipts on every opt-out
report_pending_installs recorded the fresh-install row at the first start,
usually the opt-in day itself (setup asks inside the install). The sender's
CONSENT_GATE_SQL needs period_start >= opened_at, so that day's package is
never sent: the metric mostly measured re-runs over already-consented homes.

- A receipt is recorded only when collection is on AND (sending is off, or an
  open send consent window opened at or before today 00:00Z); otherwise it is
  kept for the first start on a later day. Tested against the real store,
  real reconcile_send_consent and the real CONSENT_GATE_SQL: kept on the
  opt-in day, recorded the next day, and that package passes the gate.
- Every opt-out answer purges pending_installs AND pending_updates:
  save_consent(enabled=False) (setup, pre-chat offer, dashboard banner,
  `hermes setup telemetry`) and the tui_gateway shared_metrics.set RPC, via
  one shared_metrics_process.purge_pending_receipts used by begin_process too.
- Receipts are claimed by a plain rename, so an unparseable or non-dict
  receipt is deleted instead of sitting forever; a receipt no start counted
  within 7 days is deleted unreported (it would count under a later version).
- With Relay instrumentation off the store "settles" a row without recording
  it; the reporter now leaves the receipts for a start that records, instead
  of latching and deleting them uncounted.
- Docs: the hermes.install.run row states the consented-day rule, the 7-day
  cap, opt-out deletion and the interrupted rules.

Invariant tests red at 6246dd0f69d (5 in test_shared_metrics_install_run,
1 in test_shared_metrics_consent_rpc).
2026-10-06 11:10:43 -07:00
Teknium
5f7f9e143d fix(install-metrics): a Ctrl-C a stage handles no longer aborts the install; user aborts read interrupted
The ladder's INT/TERM traps wrote the receipt and exited as soon as the
foreground stage returned, even when the stage's child caught Ctrl-C and
succeeded (setup's provider picker prints "Provider setup skipped." and
carries on). gateway and complete were skipped, the bootstrap marker was
never written and rc went 0 -> 130: a telemetry write changed installer
control flow.

install.sh:
- The ladder traps (INT, TERM and now HUP, a closed terminal) only note the
  signal while a stage runs. rc==0 clears a Ctrl-C and the ladder carries on,
  as bash's cooperative exit did on main; rc!=0 with a signal noted writes
  `interrupted` and re-raises the signal. Outside a stage (platform check,
  banner, between stages) the signal stops the run at once with a receipt.
  The traps are installed with the EXIT trap, before check_platform.
- Inside a ladder stage, a signal marks STAGE_INTERRUPTED; stage_result
  writes `interrupted` over the stage's own class, so pm.cli /
  source_completion / `hermes setup` catching KeyboardInterrupt and exiting 1
  no longer reads deps_install_failed / setup_failed. A child the signal
  killed still ends the stage at once (status 128+N).
- A TERM/HUP sent to the installer alone takes effect when the running stage
  returns (documented; the stage stays in the foreground because a
  background stage's children ignore Ctrl-C under bash 3.2), and the receipt
  names the stage the run stopped before, not a finished one.
- A stage killed outright (SIGKILL/OOM) now leaves a `failed/<stage>/other`
  receipt; a failed write (full disk) removes its .tmp; the fallback id uses
  $(( RANDOM*32768+RANDOM )) (no octal parse); no clock -> no receipt.

install.ps1:
- A native child that ended on Ctrl-C (130 or STATUS_CONTROL_C_EXIT) reads
  `interrupted` over the stage's class; the receipt write removes its .tmp
  on failure; the dead `other` class on the pre-ladder Initialize-ResolvedPaths
  Fail is dropped (it runs before the ladder's try, so it never wrote one).

Tests (real install.sh main block with stub stage bodies, real SIGINT to the
process group): a handled Ctrl-C finishes the install with a success
receipt; a child that turns Ctrl-C into exit 1 reads interrupted. Both red
at 6246dd0f69d.
2026-10-06 11:10:43 -07:00
Teknium
4e0eae06e6 docs(telemetry): consent copy names install/update failure reasons and the local install note
CLI setup, the pre-chat consent offer and the Desktop collectedReliability line (all 9 locales,
no new key) now say update and install results carry a fixed-list reason and stage, and that a
fresh install is noted on this machine and counted only once the user opts in. The offer's
'data from before you opt in is never sent' sentence names that one exception.
2026-10-06 11:10:43 -07:00
Teknium
faf3a1e5c0 fix(install-metrics): one receipt per install run, interrupted runs counted, latch after save
install.sh: a run id per ladder, first writer wins (a parent-only SIGTERM no longer
leaves two receipts); Ctrl-C / SIGTERM write failure_class=interrupted.
install.ps1: same once-only guard, finally{} records an interrupted run.
Reporter: latch a receipt id only after its row is saved, so a short-lived start
whose daemon reporter is cut off at exit re-reports on the next start.
2026-10-06 11:10:43 -07:00
Teknium
74e0225377 feat(observability): hermes.install.run from a local installer receipt
install.sh / install.ps1 leave one closed-token receipt per full ladder run
(installer, outcome, failed_stage, failure_class, start/finish epoch) under
$HERMES_HOME/telemetry/shared_metrics/pending_installs. Every fail()/Fail call
site passes a closed failure class; the installer never sends anything.

The next Hermes start records each receipt once (latched by id, deleted after
the row is saved) only while collection is on; a start with collection off
purges the receipts unreported. Out-of-contract receipts are dropped.
2026-10-06 11:10:43 -07:00
Teknium
ef55a3f0fa chore: keep methods_tools.py at its line cap 2026-10-06 11:10:43 -07:00
Teknium
eb47ca507c fix(telemetry): a parked update receipt never claims it stopped before apply 2026-10-06 11:10:43 -07:00
Teknium
2cfab5a886 fix(telemetry): an unserved hub skill names the registry its identifier was for 2026-10-06 11:10:43 -07:00
Teknium
ab6f123eb4 docs+smoke(telemetry): failure_class/registry appendix rows; relay smoke asserts a real failed plugin install and a failed update run 2026-10-06 11:10:43 -07:00
Teknium
cbcbd2c8aa test(telemetry): exact failure_class/registry sets and the failed-stage-other invariant 2026-10-06 11:10:43 -07:00
Teknium
1b76735a5f feat(telemetry): extension installs and update runs say why they failed; hub skills name their registry
hermes.extension.install.count gains failure_class (closed per-kind set named at each failure exit
of the skills hub, plugin installer and MCP catalog emitters; raised exceptions classify by their
tagged class or TYPE, exception:<Type> collapses to exception, never the message) and registry
(the skills-hub adapter SOURCE_ID that resolved/served a skill; none/unresolved/other otherwise).

hermes.update.run gains failure_class derived from the FINAL receipt only (admission step,
stop_reason exception type, exit_code, stage marks, fleet states, gateway_restart). failed_stage no
longer reads 'other' for runs whose every mark passed: a 'partial' receipt (written only by the
post-restart verification) synthesizes a failed verify stage, and a failed run ending on a skipped
restart reads restart.

Package schema v4 is extended in place (canary-only): the pre-split field sets stay valid as the
second oneOf shape and in _LEGACY_METRIC_FIELDS so pending rows drain.
2026-10-06 11:10:43 -07:00
Austin Pickett
950ea4d51e test(desktop): shellSshDouble tolerates a child that exits before reading stdin
The spawn fixtures run commands that never read stdin, so on a fast Linux
runner the shell can exit before shellExec's stdin write lands and the
write fails with EPIPE. The stream had no error listener, so vitest
reported it as an unhandled error and failed the electron project even
though every test passed. The close verdict is the result; the EPIPE is
noise.
2026-10-06 14:07:07 -04:00
xxxigm
db6a9c65e6 test(desktop): prove the relaunch gate survives a transport that refuses big commands
A double that rejects any command over 4 KB - the shape of an OpenSSH mux
socket that cannot pass descriptors behind a large session request - must still
get a verdict, with the judge on stdin and out of argv. Red on base, where the
inlined program makes the gate raise "Could not prove that the remote Hermes
install is clear for SSH startup."

shellSshDouble forwards stdinData the way the real mux exec does, so the live
spawn and v2-marker fixtures now run `python3 -` against a real python3 and keep
proving the argument positions, lock behaviour and verdicts unchanged.
2026-10-06 14:07:07 -04:00
xxxigm
0630e548eb fix(desktop): ship the SSH relaunch marker gate on stdin, not in the ssh command
assertRemoteInstallUpdateClear inlined the ~7.5 KB marker gate program with
`python3 -c`, making the ssh command ~8.1 KB. ssh.exec rides the POSIX
ControlMaster mux socket, where ssh writes the session request and only then
hands our stdio descriptors over with sendmsg(SCM_RIGHTS); a request that still
fills the socket buffer - 8 KB for a unix-stream socket on macOS - leaves no
room for that descriptor message, so the pass fails with EMSGSIZE:

    mm_send_fd: sendmsg(0): Message too long
    mux_client_request_session: send fds failed

ssh exits 255 and the fail-closed gate reads it as "Could not prove that the
remote Hermes install is clear for SSH startup." on every attempt, so no remote
host connects while nothing is wrong on the remote. The window depends on the
client's socket buffer and the command's exact length, which is why a shorter
probe to the same host can still answer.

Hand the program to `python3 -` on stdin instead. Python compiles the whole
program before running it and the gate never reads stdin, so argv keeps the same
`<marker> [payload] [hermes...]` positions while the command stays a few dozen
bytes however the judge grows.
2026-10-06 14:07:07 -04:00
Teknium
945ffcd257 plugin-catalog: add tirith (opt-in pre-exec scanning, moved out of core)
Pins NousResearch/hermes-plugin-tirith@77405987: a pre_tool_call hook that runs
`tirith check --offline` on terminal commands and escalates findings to the normal
approval gate. Uses a tirith binary the user installs; nothing is downloaded.
Validated at the pin with `hermes plugins validate --install-deps` (scan: safe).
2026-10-06 11:00:51 -07:00
1052326311
c8ef24f8fe fix(review): preserve effective Git commit identity 2026-10-06 12:59:08 -05:00
kshitijk4poor
4ca6f9e91c fix(free-tier): a failed stdio announce still shows the challenge link
The stdio JSON-RPC transport deliberately re-raises host I/O errors
(ENOSPC, a locale that cannot encode the server's copy). _announce now
catches (OSError, ValueError) and returns False so present() falls back
to the terminal instead of losing the link and failing the run.

Also pins the ui-tui event -> presenter routing with a dispatch-level
test, and names the billing presenter factory like its sibling
(createBillingVerificationPresenter).
2026-10-06 23:14:10 +05:30
kshitijk4poor
69a12d2203 refactor(desktop,tui): free-tier challenge wiring lives in topical modules, not the facades
- desktop main.ts: the challenge IPC handler moves to
  electron/challenge-window-ipc.ts (registerChallengeWindowIpc), and
  resolvePortalBaseUrl moves next to the portal session that already
  consumed it (portal-session.ts), so the facade ends below its main
  line count; imports are sorted.
- desktop renderer: free_tier.challenge gets its own family handler
  (gateway-event/free-tier.ts) in the dispatcher table instead of a
  branch in handleLifecycleEvent.
- ui-tui: the challenge presenter and the existing billing step-up link
  presenter move to app/gatewayBrowserLinks.ts, keeping the event
  switch at or below its main size; the presenter test moves to its own
  file.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
9cb52daec2 refactor(free-tier): keep auth.py and anon_challenge.py inside the code-health ratchet
- _nous_portal_base_url moves from the auth.py facade to auth_nous.py
  (its sibling _nous_portal_env_override already lives there) and is
  re-exported with the rest of the Nous names, offsetting the
  run_with_challenge seam: auth.py ends below its main value.
- _announce no longer wraps the gateway broadcast in a blind
  `except Exception`: _broadcast_global_event already contains a wedged
  peer per transport, so anything else is a bug worth surfacing.
- _present_in_terminal catches what webbrowser.open can raise
  (webbrowser.Error, OSError) instead of Exception.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
8c0568ee73 test(free-tier): drop change-detector challenge tests; keep the invariants
Removed: the desktop/gateway surface-string tests (the gateway's
behaviour is pinned by "never waits", the desktop's by the inherited-flag
invariant), the full-payload-dict snapshot of a status read (the RPC test
covers the field), the server-copy whitespace/length detector, and the
TS "passes optional through as not-required" pass-through check.

Kept: wait outside the locks, background/gateway callers never block,
off-origin URLs refused, no loop on a second 428, the final mint is
authoritative, blips vs verdicts, PoW verdicts unchanged off the token
exchange, control characters never reach a terminal.

Also: FreeTierChallengePayload sorted into events.__all__.
2026-10-06 23:14:10 +05:30
kshitijk4poor
d0461ba6c3 refactor(desktop): free-tier challenge reuses the window-open policy and the contract's outcome type
- challenge-window.ts uses createWindowOpenHandler (deny + sanitised
  origin log, the link-title window's own hardening) instead of an
  inline deny.
- The renderer's outcome type is the generated contract's
  FreeTierChallengeResultParams['outcome']; global.d.ts uses the main
  process's ChallengeOutcome instead of a third hand-copied union, so a
  drift between the window and the contract fails the renderer typecheck.
- runFreeTierChallenge takes the gateway requester itself and builds the
  free_tier.challenge_result call, so lifecycle.ts and free-tier.ts stop
  each hand-rolling a reporter closure.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
d1f587331e fix(desktop): a required challenge ask never joins or inherits an optional run
The main process de-duplicated challenge windows by URL alone, and
remembered settled outcomes by url:attempt. If an optional (measuring,
never-revealed) run of a ticket was in flight, or had just ended
`failed` on #interactive, and the same ticket turned required, the
required ask got the optional run or its remembered `failed`, so no
window was ever shown for the human to clear. Key both the in-flight
run and the settled memory on URL + required.
2026-10-06 23:14:10 +05:30
kshitijk4poor
fa6ef8ba6d refactor(free-tier): one per-profile challenge record behind one lock
_pending (dict-of-dicts keyed "payload"/"deadline"/"outcome"),
_work_locks and _gave_up_until were three per-profile module globals,
two of them mutated outside the lock. They become one _ProfileChallenge
dataclass in a single _profiles map behind _state_lock; the record holds
the BrowserChallenge itself, so lookups are attribute reads and
as_payload() runs only when a status read asks for it. _set_pending
becomes _record(challenge, new_attempt=...). The test-only _presenter
seam goes: tests patch present() itself. _sleep stays, same idiom as
free_tier_bootstrap.

One edge differs, for the better: an EXPIRED same-URL record is now
treated as a new ticket (fresh deadline) instead of re-recorded with
min(new, old-expired) deadline, i.e. born expired.
2026-10-06 23:14:10 +05:30
kshitijk4poor
50b0082ed0 refactor(free-tier): build AnonChallengeRequired once; endpoint overrides table; codes live in anon_auth
- AnonChallengeRequired takes portal/credential/auth_state in its
  constructor. exchange_anon_jwt (given auth_state by its one caller,
  refresh_guest_state) builds it; the two catch-mutate-reraise blocks and
  the empty/None defaults are gone.
- _raise_for_anon_status no longer reads `portal_base_url=""` as an
  "is token exchange" flag. The exchange passes its verdicts as an
  `overrides` table keyed like _NAS_REFUSALS; 428 pow_* routes to an
  explicit proof-of-work verdict ahead of the 428 sign-in catch-all.
- ANON_CHALLENGE_REQUIRED / ANON_SIGNIN_REQUIRED and their copy are
  defined next to their siblings in anon_auth; anon_challenge imports
  them (and _anon_headers/_mint_memo_key) at module level, anon_auth
  imports anon_challenge lazily.
- client_info/user_agent read get_version_info().base_version instead
  of the old-updater __version__ shim.
- test: the stdio-TUI-gateway presentation test patches the real
  tui_gateway.server attributes; a SimpleNamespace stand-in broke the
  conftest server teardown.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1eec4cf274 fix(free-tier): put the challenge link in front of ui-tui users
The TUI gateway child reports surface `cli`, so a 428 printed the link
to the child's stderr, which ui-tui keeps as a /logs tail and never
shows (createGatewayEventHandler: gateway.stderr). Over SSH or headless
that was a silent 120 s wait followed by "finish the check in your
browser" with no link. For a required challenge a foreground caller is
waiting on, the stdio TUI gateway now announces it on its JSON-RPC
channel, and ui-tui renders the message + link and opens one tab per
ticket. Background reads (keepalive, status paint) still never pop a
browser, and optional checks stay the desktop's to run hidden.
2026-10-06 23:14:10 +05:30
kshitijk4poor
4a370f1c76 fix(free-tier): only the desktop-spawned backend reports the desktop surface
client_surface() trusted raw HERMES_DESKTOP=1, which every terminal-pane
shell and agent child inherits (#116107 class). A `hermes` run in the
desktop terminal pane told NAS `surface: desktop` and suppressed its own
"finish the check in your browser" line. Use
process_identity.is_desktop_owned_backend(), which also needs the
per-spawn credential.
2026-10-06 23:14:10 +05:30
kshitijk4poor
e955022eb1 fix(proxy): never wait on a free-tier challenge under the Nous adapter lock
NousPortalAdapter._get_credential resolves credentials while holding
self._lock, which every proxied request queues on. With the challenge
gate on, a 428 parked that lock (and a default-executor worker per
queued request) for up to 120 s. Resolve as a background caller: the
challenge is recorded/announced and the retryable error returns at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1507c8d0f7 fix(free-tier): a messaging gateway never waits on a browser challenge
The gateway resolves Nous credentials from async code with sync calls
(run_inbound._inbound_model_context_length -> runtime resolve, and
run_startup -> self_provision_relay -> resolve_nous_access_token). A 428
there parked the event loop for up to 120 s, and nobody sits at a
gateway's console to clear the check anyway. Treat the gateway surface
like a background caller: record/announce the challenge, raise the
retryable error at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
6dae8d059a fix(free-tier): keep the proof-of-work verdict for pow_* off a 428
The challenge commit dropped the `error == "pow_"` fallback in
_raise_for_anon_status. A pow_* error on a status with no wildcard row
(400/403/5xx) then became a retryable anon_server_error instead of the
terminal anon_pow_required, on every endpoint including sign-up and the
promotion paths the PR said were unchanged. Restore the fallback (kept on
purpose in 2a94ca80e7) and pin it on sign-up.
2026-10-06 23:14:10 +05:30
Robin Fernandes
90ff655c81 feat(free-tier): browser challenge primitive for the anonymous token exchange
NAS may answer POST /api/anonymous/token with 428 challenge_required and a
portal URL. Hermes implements the one `browser-v1` primitive: open the URL
(hidden where it can), poll /api/anonymous/challenge/status, exchange again.
Everything the page does (BotID, in-browser proof of work, an interactive
Turnstile fallback, its copy) belongs to the portal and changes without a
Hermes release. The mint stays authoritative throughout.

Python (hermes_cli/anon_challenge.py)
- the exchange reports surface (desktop / cli / gateway), version and the
  browser-v1 capability; a UA of the documented shape goes on every call
- the wait runs outside the auth-store and shared-store locks, under a
  per-profile work lock; only a caller someone is waiting on waits — the
  keepalive tick and a status paint announce the challenge and return
- 403 signin_required / access_denied and any 428 this version has no
  primitive for become a terminal sign-in error, scoped to the token
  exchange; sign-up and the promotion endpoints keep their previous verdicts
- status-poll 429 / 5xx / transport are blips; a 4xx is a verdict (the
  credential is gone); a second 428 starts a fail-fast cooldown so a boot
  burst of token readers does not each re-mint and reopen a window
- the pending challenge is published as the free_tier.challenge event and
  on free_tier.status for a client that connects later; the desktop reports
  how each window ended (free_tier.challenge_result) so the waiter re-mints
  promptly, and an attempt counter lets a fresh foreground attempt reopen a
  window the user closed without a status replay reopening it
- terminal: print the link, open a graphical browser once per ticket; a
  gateway process never opens one; control characters never reach the tty
- challenge URLs are only ever opened on the portal origin the exchange hit

Desktop (apps/desktop/electron/challenge-window.ts)
- the page loads in a hidden, sandboxed window of its own partition, pinned
  to the portal origin (will-navigate, will-redirect, off-origin commits end
  the run); a phase is read only from a main-frame portal URL fragment
- #interactive reveals the window (never for an optional challenge), #done
  and #failed close it; an HTTP error commit, a load failure and a crashed
  renderer are retryable, a closed window is not; timers are bounded
- the renderer relays the event and the status read to one window per
  attempt and reports the outcome once; a backend without the RPC is fine
2026-10-06 23:14:10 +05:30
Austin Pickett
80cf5183f8 chore: map contributor emails for #133991
Add contributors/emails/ mappings so check-attribution resolves the
author emails on this branch:
- GuerrillaMedia702@gmail.com -> AP3X-Dev
- zaclanzon@users.noreply.github.com -> zaclanzon
2026-10-06 13:23:15 -04:00
Austin Pickett
fd431bedbc test(dashboard): per-call model attribution regression for #71778 2026-10-06 13:23:15 -04:00
Zac Lanzon
0cc2bcdcee fix(dashboard): count distinct models over the merged row set
totals.distinct_models was COUNT(DISTINCT model) over sessions alone, so a
model reached only through auxiliary usage drew a card the header never
counted. Derive it from the same merged rows the cards come from (#89631).
2026-10-06 13:23:15 -04:00
AP3X
2c48f5ff6c fix(dashboard): fold aux usage into the model card it belongs to
/api/analytics/models appended one row per (model, task, provider) from
_aux_usage_rows next to the sessions-derived (model, provider) row, so a
model doing approval/compression/title/background_review work rendered as
four or five identical-looking cards, and their session counts summed past
totals.total_sessions.

Fold aux usage into the existing pair's row; only an aux-only pair (no
sessions-derived row) becomes a new row carrying its own session count,
so dedicated aux models stay visible (#23270).

Ported from #89707 (web_server.py -> web_routers/analytics.py after the
router decomposition). Same diagnosis as #77524.

Co-authored-by: Kumar Sarthak <kumarsarthak0712@gmail.com>
Co-authored-by: Zac Lanzon <zaclanzon@users.noreply.github.com>
2026-10-06 13:23:15 -04:00