bcdb49ac7bb274eb71c38fd2f2a80ccf3358974c
4695 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b8c01f691 |
fix(multiplex): key tool-side and agent-side memos by profile home
Camofox VNC one-shot, computer-use aux-vision verdict, tirith binary path, MCP discovery lock path, remote-backend probe text, learned image token costs, auxiliary per-task semaphores and the custom-endpoint /models memo all held one profile's config-derived value for the whole process. The skill-sync debounce Timer ran with empty ContextVars, so a secondary's write pushed as the launch profile (and cancelled its pending push). Each memo is now keyed by hermes_home_key() (or credential fingerprint for the per-key catalog) under an override; the timer is per home and runs its callback inside the scheduling turn's copied context. Unscoped slots are unchanged. |
||
|
|
2cc6d88c1d |
fix(multiplex): build Bedrock and Entra credential clients from the routed profile
boto3 and azure-identity freeze the credential chain into the client at construction, and the process env under a multiplexed turn belongs to the launch profile. A region-only (bedrock) / config-only (lru_cache) slot therefore signed a served profile's calls with the launch profile's keys and served its account's model list to everyone. Under a HERMES_HOME override the clients are built from the profile's secret scope (AWS_* / AZURE_* from its .env) and cached per (home, service, region) / (home, config); the discovery cache key carries the home. The unscoped path keeps the region slot and the maxsize=1 lru byte-for-byte. |
||
|
|
4f1966edac |
feat(desktop): connector cards that wait for the sign-in, and a guided first launch that holds together (#108292)
* feat(desktop): give Button a loading prop that swaps label for spinner without layout shift The label stays in the box, invisible, and the spinner is absolutely centred over it, so a Connect or Approve button keeps its width while it works instead of collapsing to a spinner. The approval bar had the same thrash and moves onto it. * refactor(desktop): one consent card for connectors and MCP setup McpSetupTool rendered its own copy of the connector card's markup. It now renders ConnectorCard for the pending question and ConnectorSummary once settled, and the card gains what MCP needed: keyboard accelerators, a source line, a question heading. The card also gets an avatar variant (40px mark in the left gutter, text and buttons on one column) and a collapseWhenSettled switch so a connector can stay a full card with a green Connected pill in the action slot while MCP keeps its one-line summary. Brand marks for Gmail, Calendar, Drive, Discord, Telegram and Spotify; Slack via Tabler because simple-icons dropped the mark. * feat(desktop): connector card drives the agent through manage_connections wait The offer used to end in a Continue in chat button, and the agent, seeing an unconnected status, would improvise around the app. Now the card does what the TUI does. Clicking Connect opens the browser and sends one hidden line telling the agent to park in manage_connections action=wait for that slug and to never call connect again (a second link cancels the one being signed into). Not now sends its own line. A hidden request that lands while the turn is busy steers it, or queues if the turn just ended. Which call owns the live card changes too: consecutive calls naming the same apps are one exchange (connect, the wait, the status that follows), and the first of the last exchange is the card, so the agent's wait no longer demotes the card mid-authorization and mints a fresh one below it. A targeted ask renders one or two bare cards; only a real catalog gets the header, search and refresh. * feat(desktop): onboarding connects apps in chat and keeps tasks finishable without them The welcome chat knew connectors only as preferences to pick and wire up later, so asked to connect Gmail it invented a Settings page that does not exist. Both scripts now carry one rule set: status once, one batched connect for every app named, the card is the ask so write a line and end the turn, never route around a declined app with another client or credential. The build handoff checks real connection status instead of asserting none are connected, and the first task must be finishable, not free of, the apps they picked. The connectors card explains what connecting means and reports the count on its Continue button. * fix(tools): resolve the Nous identity for share_auth profiles in the connector gate A profile created with share_auth has no auth.json of its own and signs in through the root store. Every other credential reader falls back to the global root; the connector gate read HERMES_HOME/auth.json directly, saw nothing, and stripped manage_connections from the profile's tool list, so the welcome chat's agent truthfully reported the tool missing. The gate now goes through get_provider_auth_state. * fix(agent): name a provider retry backoff on the live status line The retry status is buffered and replays only when every retry fails, so during a 60s backoff after a 5xx the user saw a bare spinner. Right after a connector sign-in landed this read as the agent going silent. The backoff now also rewrites the live wait notice, which the desktop already renders in the thread status row; it is transient and clears on recovery. * test(desktop): connector rehearsal launcher and flagged connector spec connector-rehearsal.mjs starts the real desktop and backend under a fresh HERMES_HOME with no copied credentials, a fixed Vite port and CDP on 9344, so the onboarding connector flow can be driven end to end by hand or from outside. The Playwright spec covers the flagged connector step. * fix(desktop): send the agent back into wait when the user keeps waiting after a timeout The card's Keep waiting re-entered the poll but the agent's own wait had timed out too and nothing told it to go back in, so it would start talking mid-authorization. keepWaiting now fires onWaiting like connect does. Tests also pin that an expired or revoked grant asks the gateway for reconnect, not connect. * style(desktop): blank lines in connector-flow test per lint * feat(desktop): HERMES_SKIP_INTRO=1 / --skip-intro skips the first-run film The intro is a one-time reveal, so anyone rehearsing the guided chat behind it sits through it on every fresh HERMES_HOME. The flag rides the existing launch-flags path (main → preload → renderer) next to guestOnboarding and only gates isIntroRevealEnabled; the backend never sees it. The rehearsal launcher sets it. * fix(desktop): onboarding card Continue stays Done after the transcript rebuilds The card kept its Done flag in component state. The hidden submit and the turn-end hydrate both rebuild the message list, so the card remounted with the flag false and Continue came back live, letting a step be answered twice. The committed steps now live with the other onboarding answers, keyed by step, and the first-build chip pick rides the same store. remember_onboarding projects by key, so the new field never reaches USER.md. * fix(desktop): no provider picker or free-tier chip over the guided first launch Two sign-in surfaces leaked into the guide. A credential probe on the setup profile (a free-tier token mid refresh, a session before its runtime settled) hit requestDesktopOnboarding and dropped the provider picker over the chat the user was in; and the statusbar free-tier chip sat there offering a second sign-in the whole time. Both now yield while the gate phase is cinematic, guided or handoff. The free tier is the provider for those phases, and the guide offers sign-in on its own ready screen. * fix(desktop): onboarding connector picks are real catalog slugs The picker offered Spotify, GitHub and Stripe, none of which the deployed connector catalog carries, and spelled Calendar and Drive with hyphens the gateway does not use. A pick the build chat could not honour ended as "Spotify isn't in the connector list" after the user had been told to expect it. The list is now twelve slugs from the live status catalog, spelled as the gateway spells them; GitHub is out (the terminal has git and gh), chat channels stay on Messaging. Marks for the new entries; the Google marks answer both spellings. The build runbook offers the picked connections in its first turn rather than after the work is underway. * fix(desktop): the free-tier ready screen never interrupts the guided chat A readiness round fires when the layout pick assembles the window, and it raised the free-tier ready screen over the conversation: the user was dropped into the main app, dismissed it, and came back to a card they had already answered. The guide is the introduction. The ready screen now yields while the gate is cinematic, guided or handoff, and the notice is acked the moment the guided chat takes the screen, not only when the film does, so a skipped film no longer leaves it pending. * feat(desktop): tour options that lead to building, and a fork that follows the tour "Just the basics" and "Show me around" read as a click-through with no exit; "I'll figure it out" read as declining help. Now Quick tour, Show me everything, and Skip, let's build something. The script also folds the fork into the same turn as the tour, so when the user closes the overlay the next ask is already waiting instead of a transcript that ends on the tour call. * feat(desktop): the onboarding connector picker reads the live catalog A hardcoded list, however carefully copied from today's catalog, is the next drift. The picker now asks connectors.list through the same session-owned RPC the connector cards use and offers exactly what the gateway carries: a curated lead order puts the everyday apps first, chat channels stay on Messaging, everything else is reachable by search. The picks are gateway slugs, handed straight to manage_connections. No catalog (toolset off, gateway unreachable) ends the step honestly with Skip instead of inventing apps. * test(desktop): the guided first launch never forces a sign-in The acceptance criterion the guided onboarding was built to, as a test: while the gate is cinematic, guided or handoff, the provider picker does not open and a credential warning is dropped rather than deferred to the next send. Outside the guide the picker opens as before. Red against the tree before the guards landed (6 of 9). * fix(desktop): a relaunch mid-guide resumes the guide, in the guide's shape Closing the app during the guided first launch and reopening it booted the normal shell around the persisted solo layout: the connecting splash, the stock composer and model picker, a small window whose sidebars would not open, while the gate still read guided. The gate now queues a kickoff for the guided phase too (the kickoff adopts the existing guide chat by title), takes the solo shape before the gateway opens rather than after, and the connecting overlay yields to the guide's own opening. A typed reply in the composer now closes an ask card and the first-build chips the same way a click does; the layout card's Continue comes back Done. * style(desktop): one answeredAfter helper for the ask card and first-build chips * fix(desktop): the guide takes its shape on the tick the film ends, not after the window shows Between the film and the greeting the full-size shell painted for a beat: finishIntroReveal showed the main window, then the kickoff shrank it once the setup profile answered. The listener on the intro's hidden edge now takes the guide's shape (solo layout + small centred window) synchronously, so the window is already the guide when it is shown. One takeGuideShape owns the pair; kickoff and the boot gate call it idempotently. * style(desktop): the 'nothing connects yet' line reads first on the connectors card |
||
|
|
9c9e7ab6e5 |
fix(multiplex): a served profile's turn sees its own cwd, approvals, redaction and tool policy
Under gateway.multiplex_profiles a secondary profile's turn ran with the LAUNCH profile's working directory, command allowlist, redact_secrets switch, credential file mounts, browser engine/headed flags, LSP service, auxiliary-provider health marks and MCP stderr log, and several TERMINAL_ENV consumers read the process env instead of the routed profile's terminal scope. A standalone `hermes -p X gateway run` never behaved that way. - tools/terminal_scope.py: resolve the terminal.cwd placeholder inside the profile scope with the same rule gateway/run.py applies at import (local -> $HOME, sandbox default otherwise) so the system prompt, context files and the terminal of a routed turn start where the profile's standalone gateway would. - tools/image_source.py, credential_files.py, image_generation_tool.py, skills_tool.py, delegate_tool_progress.py, agent/tool_executor.py: read TERMINAL_ENV / TERMINAL_CWD through the terminal scope. - tools/approval.py (+ approval_floors.py): one permanent allowlist per routed profile home; the unscoped module set stays for single-profile processes. - agent/redact.py: `_redact_enabled()` resolves security.redact_secrets for the routed profile (scope .env, then config); launch snapshot kept when unscoped. - tools/credential_files.py, agent/auxiliary_health.py, agent/lsp/__init__.py, tools/browser_tool_cloud.py, tools/mcp_tool_config.py, tools/tool_result_storage.py: key process caches by profile home (or bypass the slot under an override). Tests: tests/tools/test_multiplex_turn_parity.py (4, red on base). Docs: multi-profile-gateways.md isolation table. |
||
|
|
f28c52845a |
fix(models): resolve routed OpenRouter ids in models.dev catalog lookups
Extends the routing-variant fix to the sibling catalog paths, so the whole bug class is covered rather than just context length (#97820). _find_model_entry(), lookup_models_dev_context(), get_model_info(), and get_model_capabilities() all keyed on the full suffixed id, so a routed id such as z-ai/glm-5.3-flash:floor missed models.dev and fell through to the generic "glm" family default (202,752 instead of 1,310,720). The retry with the base id runs LAST — after exact and case-insensitive matching — so a real catalog SKU always wins over its base. :free and :batch are deliberately NOT stripped. They are real catalog SKUs, not routing modifiers: OpenRouter's /models currently lists 18 :free and 65 :batch entries, and 13 of those carry a context window different from their base (z-ai/glm-5.2:free is 256K vs the base's 1.05M). Stripping them would report a window LARGER than the model has, so the request fails at the API instead of merely compacting early — a worse failure than the under-report this fixes. An absent SKU must also miss rather than inherit the base, so model_overrides _default fill-gap semantics keep working. The suffix set is shared from hermes_constants, so validation, context resolution, and catalog lookup all agree on one definition. |
||
|
|
284ee03df1 |
fix(models): resolve context length for OpenRouter :nitro/:floor routing variants
`:nitro`, `:floor`, `:exacto`, and `:online` are request-time routing
modifiers, not catalog models — OpenRouter's /models lists only the base
id, and a variant runs the same model with the same context window.
`get_model_context_length()` keyed every lookup on the full suffixed id,
so each one missed and the resolver fell through to a generic family
default or the 256K fallback:
openai/gpt-5.5:nitro -> 256K (real 1.05M)
x-ai/grok-4.6:nitro -> 131K (generic "grok" catch-all)
anthropic/claude-opus-4.6:nitro-> 200K (generic "claude" catch-all)
The window silently shrank, triggering early compression and a wrong
/usage readout.
|
||
|
|
819517fbac |
fix(gateway): routed profiles get their own max_turns, fallback chain, hooks, aux auth and media policy
One multiplexed gateway process serves every profile, but several per-turn reads still went through state frozen from the LAUNCH profile: - `_current_max_iterations` re-bridged `agent.max_turns`/`sessions.*` from the module constant `_hermes_home` into one process-wide HERMES_MAX_ITERATIONS, so every secondary ran with the default profile's turn budget. A routed turn (HERMES_HOME override) now resolves `agent.max_turns` from its own config. - `_refresh_fallback_model` read `_hermes_home/config.yaml` into one runner-wide slot, so secondaries fell back through the default's provider/model with their own keys. It now reads the active gateway home and keeps a last-known-good chain per home. - `_load_prefill_messages` resolved relative paths against the launch home. - `agent/auxiliary_client._AUTH_JSON_PATH` was an import-time constant, so a secondary's compression/title/vision calls authenticated to Nous with the default profile's token when it had no pool entry. Resolved per call via `hermes_cli.auth._auth_file_path()` (patched constant still wins in tests). - `gateway/hooks.HOOKS_DIR` was frozen at import and one `HookRegistry` was loaded outside any profile scope, so secondaries' `hooks/` never ran and the default profile's handlers received every profile's messages, responses and user ids. `HOOKS_DIR` now resolves per call (salvaged from #56508) and the runner holds one registry per served home, picked from the active scope at emit time and front-loaded under each secondary's startup scope. - Shell-hook subprocesses inherited the launch `os.environ` (default HERMES_HOME and the default profile's secrets). They now get the routed HERMES_HOME via `build_subprocess_env`, scrubbed under multiplexing, and the stdin payload carries `profile` so one script can tell which profile fired it. - Media-delivery policy (`gateway.strict`, `media_delivery_allow_dirs`, `trust_recent_files*`) was bridged once into env at startup and read from env per delivery; under a HERMES_HOME override the validator now reads the routed profile's config. Single-profile runs keep the env-bridge contract. Audit: /tmp/mux_audit F3, F4, F6 (auth.json half), F7, F12 (media). Live repro (temp HERMES_HOME A with profiles/B): before, B saw max_iterations 7, fallback A/fallback, TOKEN_A, A's hooks, strict=A; after, all B's values. |
||
|
|
a9838c2100 |
fix(multiplex): tool and memory-provider env reads stay inside the routed profile
Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's .env; a
secondary profile's values exist only in the per-turn secret scope. Every reader
below still read os.environ/os.getenv at call time, so a secondary profile's turn
silently used the default profile's value.
Credentials (F6): FIRECRAWL_API_KEY (read_file hosted OCR), OPENVIKING_API_KEY,
mem0-OSS OPENAI_API_KEY, MODAL_TOKEN_ID/SECRET and BROWSER_USE_API_KEY presence
gates, and the xAI video plugin's os.getenv("XAI_API_KEY") fallback AFTER the
scoped resolver had already missed — the exact fallback-after-miss shape
gateway/AGENTS.md forbids. Deleted, not re-scoped: the resolver is the scope.
Identity / tenant (F7): MEM0_USER_ID/AGENT_ID/HOST/MODE, SUPERMEMORY_CONTAINER_TAG,
RETAINDB_PROJECT, OPENVIKING_ACCOUNT/USER/AGENT (and the whole layered() env
read), HINDSIGHT_BANK_ID/MODE/retain shaping, HERMES_HONCHO_HOST. A raw read
put a secondary profile's memories into the default profile's account/bank/
project/tenant and recalled them back into the default's turns. Each now uses
get_secret with the provider's own per-profile default on a miss.
Endpoints (F8): OPENAI_BASE_URL (aux custom runtime + direct-alias expansion),
XAI_BASE_URL/HERMES_XAI_BASE_URL (aux OAuth), NOUS_INFERENCE_BASE_URL (#65941,
both the aux builder and hermes_cli.auth_nous._nous_inference_env_override),
GATEWAY_PROXY_URL (same UnscopedSecretError-only fallback shape as
GATEWAY_PROXY_KEY three lines below), FIRECRAWL_API_URL, BROWSERBASE_BASE_URL,
SUPERMEMORY/RETAINDB/HONCHO/HINDSIGHT URLs. The keys beside them were already
scoped, so a secondary's key was sent to the default profile's proxy or host.
Targets / display (F11): WEIXIN_HOME_CHANNEL (message posted into the default's
chat), HERMES_LANGUAGE, and agent/i18n's process-wide lru_cache of
display.language — now keyed by HERMES_HOME.
Outbound webhooks: hooks.outbound[].secret_env resolved from os.environ while
the gateway registers each profile's targets inside that profile's scope, so a
secondary's deliveries were signed with the default's secret or left unsigned.
Agent-cache eviction: _spawn_release_thread started a bare threading.Thread, so
commit_memory_session -> provider on_session_end ran with an EMPTY context. The
thread now runs copy_context() and, for the unscoped housekeeping sweep, enters
the owning profile's _profile_runtime_scope resolved from the session key
(agent:<profile>:...). The pressure batch does the same per key.
session_search (#82903): agent/inline_tool_executors.py::_session_search
forwarded every schema argument except `profile`, so a gateway agent could
never select a named profile's store. Forwarded; the ownership-scoping design
in #87779/#87847 is a separate design call and is not attempted here.
Live repro (/tmp/mux_audit/fix-tool-memory-reads/repro.py): 28 FAIL on
origin/main -> 0 FAIL with this change; 10 new invariant tests red on base.
Fixes #82903
Fixes #65941
Fixes #99121
Addresses #87779
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Michael Versluis (Berry) <michael@wve.nl>
|
||
|
|
759024bdff |
fix(deepseek): honor Flash 1M window leftovers and native vision
Users on native DeepSeek were told to pin model.context_length and model.supports_vision in config.yaml. That is the wrong layer: the 1M window is already in DEFAULT_CONTEXT_LENGTHS, and a global supports_vision pin would also mark text-only deepseek-v4-pro as multimodal. Two catalog gaps still produced the reported symptoms: - A leftover context_length_cache.yaml entry of 128K (the old ``deepseek`` catch-all) outlived the 1M catalog keys because deepseek-flash was missing from _PRE_CATALOG_STALE_KEYS. - When models.dev is empty/cold, Flash has no capability record, so image routing falls through to lossy text. Vendor docs (2026-09-10) mark deepseek-flash as vision-capable and deepseek-v4-pro as not. Discard those 128K leftovers, fill Flash (and retired Flash aliases) via _BUILTIN_MODEL_METADATA, and leave Pro catalog-only. |
||
|
|
0dcadf6f41 |
revert: remove Collective Wisdom V1 (#94266)
Reverts the in-tree org skill-marketplace: hermes_wisdom package, three model tools, CLI/gateway/desktop/dashboard/Telegram/Slack surfaces. Later non-Wisdom work on shared files (guest onboarding i18n, dashboard startup schema, Slack adapter, tui_gateway) is kept; Wisdom-only call sites and config were stripped from those files. |
||
|
|
8226c2f6a2 |
Merge pull request #107708 from dagardner-nv/feat/propagate-request-metadata-to-relay
feat(relay): pass API request metadata to Relay tracking |
||
|
|
754ecff466 |
fix(state): pin corrupt-session recovery guidance to the failing profile
The recovery commands rendered on structural corruption — the turn explainer's `session_persistence_failed`/corrupt body, the gateway's home-channel state.db warning, and hermes_state_repair._persistent_repair_exhausted_error — already interpolate the active profile's state.db path, but every `hermes ...` verb in them was bare. A bare `hermes` follows the sticky `active_profile` file, so an operator running the pasted `hermes doctor --fix` (or `hermes sessions recover` with a relative source) from a named-profile incident could inspect or repair a different profile's database (#105887). hermes_constants.profile_cli_selector() renders `-p <name> ` for a named profile home (default home and custom roots outside the profile tree render nothing: the default is what a bare `hermes` already means, and a custom root is only reachable via HERMES_HOME). Every command in the three guidance sites now carries it, and the new `fts_index` guidance inherits the same interpolation. Live check with HERMES_HOME=<root>/profiles/research and active_profile=other: before `1. Run \`hermes doctor --fix\`` (targets "other"); after `1. Run \`hermes -p research doctor --fix\`` and `hermes -p research sessions recover --source <root>/profiles/research/state.db --inspect-only`. Refs #105887 Reported-by: Cuttingwater |
||
|
|
38adfe90a4 |
fix(state): classify FTS-scoped corruption as fts_index, never whole-file damage
classify_persistence_error bucketed every _DB_CORRUPTION_MARKERS hit as "corrupt", so an error SQLite itself scoped to the FTS5 index layer (SQLITE_CORRUPT_VTAB, or an `fts5: corrupt structure record for table "messages_fts"` report) that escaped the write path — the detach in _enter_fts_fail_open refused (generation/lock check), or a read/search path with no fail-open at all — reached the turn boundary and the gateway startup notice as structural corruption: the turn ended with `.recover` / restore-backup advice on a file whose canonical tables were provably healthy. One provenance rule, hermes_state_errors.is_fts_scoped_corruption_error, now feeds both the write-repair gate (SessionDB._is_fts_write_corruption_error delegates to it, so the gateway transcript retry inherits it) and the classifier: a known result code outranks prose (only SQLITE_CORRUPT_VTAB is FTS-scoped; bare SQLITE_CORRUPT/NOTADB and any contradictory code fail closed), and without a code the text must both carry a corruption marker and name a messages_fts* object. The new "fts_index" cause renders index-scoped guidance (doctor --fix / restart, do not run recovery) in the turn explainer and the home-channel notice. The structural fail-close is untouched: bare malformed / not-a-database still quarantine and still classify "corrupt". Salvaged from PR #97843 (SulthanZahran1), trimmed: the quick_check-backed "corrupt_unconfirmed" tier is dropped — on a live handle that just observed an unscoped SQLITE_CORRUPT, PRAGMA quick_check on a damaged shadow b-tree raises rather than reports on 3.53.1, so the probe could never downgrade the exact shape it was built for, and a verdict that softens quarantine guidance on prose alone weakens the fail-close. #97841 (Finn763) reached the same fts_index cause via text markers only; its LIKE-degradation intent already lives in _search_messages_impl (_fts_stale). Fixes #97794 Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com> |
||
|
|
496eb13bd7 |
fix(state): one corrupt timestamp row no longer kills sessions list, export or insights
SQLite dynamic typing lets a TEXT cell ('not-a-timestamp'), inf/nan or a
garbage double (8.4e252 salvaged from a damaged page) sit in a REAL
timestamp column. Every reader called datetime.fromtimestamp()/float
arithmetic on the raw cell, so ONE bad row raised TypeError/OverflowError
out of the row loop and took down the whole `hermes sessions list`/browse
table (#102399), all three exporters — JSONL/MD, QMD, HTML (#102352) —
and `hermes insights` (#99959).
Fix the class with ONE helper, hermes_cli.timefmt.coerce_epoch(): a
stored cell becomes float epoch seconds inside a sane 1970..2103 window
or None after a WARNING that names the session id. Every reader routes
through it — relative_time (list/browse/resume picker), format_epoch
(prune/candidates tables), the three exporters' timestamp formatters,
insights' _get_sessions/_day/period range — so a bad row renders as
'?'/'N/A'/raw text for that one cell and the command completes.
Write side: hermes_state_messages._coerce_timestamp (append_message,
append_messages_batch, import) and the import path's started_at now use
the same window, so a new out-of-range timestamp falls back to now()
instead of being persisted — new bad rows cannot be written by Hermes.
Reported-by: #102399, #102352, #99959 reporters; kokhlo's insights
analysis pointed at every reporting site, not just line 860.
|
||
|
|
16e4496d90 |
fix(agent): corrupt-session recovery guidance names the session's own state.db
The corruption explainer filled `{db_path}` from `_default_db_path()`, the
process default. A Desktop `serve` backend launched on the root home hosts
named-profile sessions whose SessionDB is `profiles/<name>/state.db`, so the
operator was told to inspect/repair a different profile's database. Pass the
agent's own `_session_db.db_path` from the turn finalizer; the process
default remains the fallback for agents without a bound store.
Reported in #105887.
|
||
|
|
031d3760d0 |
fix(state): distinguish retired WAL recovery guidance
Classify deleted WAL generations separately from main-file replacement and point operators at the captured-generation manifest and mode-aware recovery path. Co-authored-by: crazyief <8566250+crazyief@users.noreply.github.com> |
||
|
|
fc3d60af09 |
feat(compression): provider-scoped model_thresholds keys ("<provider>:<substr>")
A bare `astra: 0.85` in compression.model_thresholds was written for the Codex OAuth route, where Astra is capped at 272K and 50% would compact at ~136K. The key is substring-matched on the model name alone, so it also fired on openai/gpt-6-astra via OpenRouter and Nous, where the window is 1.1M: the user's 0.5 global threshold was silently replaced by 0.85 and the session sat at 620K (~59%) without compacting. Keys may now carry a provider prefix: `"openai-codex:astra": 0.85` applies only when the session's provider is openai-codex; bare keys keep their route-agnostic behaviour. Ranking is by model-substring length with scope as the tie-break, so `astra-900k` still outranks `openai-codex:astra` for the 900K picker. The provider flows through ContextCompressor (ctor + update_model), the ContextEngine base class and the TUI hot-reload path, so a /model switch between routes re-scopes the override. |
||
|
|
a6ee31f55a |
feat(wisdom): add Hermes Collective Wisdom Agent V1 (#94266)
* feat(wisdom): add trusted publish and install foundation
* feat(wisdom): add private contribution loop
* feat(wisdom): add managed consumption workflows
* fix(wisdom): close cross-repository safety gaps
* fix(wisdom): align local package and lifecycle policy
* fix(wisdom): require explicit profile setup
* docs(wisdom): repin reconciled gateway head
* fix(wisdom): fence content downloads and approval receipts
* docs(wisdom): record generation-fenced downloads
* docs(wisdom): record unified delivery PR
* fix(ci): stop passing invalid classifier inputs
* docs(wisdom): remove internal requirements ledger
* feat(wisdom): localize dashboard and desktop copy
* feat(wisdom): complete local contribution and consumption UX
* style(wisdom): satisfy desktop lint
* chore(wisdom): refresh requirements pin
* test(dashboard): allow formatted profile copy
* test(wisdom): stabilize desktop interaction coverage
* fix(wisdom): surface dashboard action failures
* fix(wisdom): add repeatable Portal demo login
* feat(wisdom): add actionable skill notifications
* feat(wisdom): add notification install and update actions
* fix(wisdom): make Telegram skill alerts actionable
* fix(wisdom): always refresh demo Agent login
* feat(wisdom): embed Telegram notification actions
* fix(wisdom): preserve Telegram notifications after actions
* fix(wisdom): keep Telegram notification cards readable
* feat(wisdom): add Telegram candidate approval flow
* feat(wisdom): explain Telegram qualification reasons
* fix(wisdom): reconcile cross-surface candidate actions
* feat(telegram): add Collective Wisdom management command
* chore(wisdom): refresh Gateway contract pin
* chore(wisdom): advance Gateway contract pin
* feat(wisdom): align command UX across clients
* feat(slack): add Collective Wisdom management parity
* feat(wisdom): add security and professionalism reviews
* feat(wisdom): add first-time qualification guidance
* feat(wisdom): simplify qualification sharing choices
* feat(skills): add optional editorial metadata
* feat(wisdom): enrich legacy skill presentation
* fix(wisdom): harden review and update boundaries
* fix(wisdom): emit canonical review timestamps
* fix(wisdom): align with merged gateway and main
* wisdom: add agent-led sharing core (policy, evidence, schemas, templates, delivery, weekly job, share/install flows)
- hermes_wisdom/agent_led/: policy resolution (server > local > defaults),
7-day evidence builder that excludes bundled/hub/managed skills and
dismissed/handled/recently-suggested content hashes, strict pydantic
schemas for agent output with repair-or-reject, fixed copy templates
(Share / Teammate / Published / Update / Mute), idempotent retried
delivery ledger with stale-action resolution, weekly review job,
resumable Share and Install flows.
- prompts/: candidate review, recipient recommendation, share packaging.
- tests/wisdom/test_agent_led.py: 30 tests.
* wisdom: agent-led renderers and button action dispatcher
- render.py: Telegram HTML, Slack blocks, Desktop payload; editorial name
is the emphasized line, product label stays separate.
- actions.py: resolve opaque wa:<action>:<dedup> targets via the delivery
ledger; Not now -> dismissal, Mute -> fixed options, Share -> resumable
packaging flow, Install/Update -> plan command. Never publishes/installs.
* wisdom: CLI verbs, agent_led config default, conversational catalog skill
- hermes wisdom browse/review-week/act/share/dismiss/mute (all --json).
- wisdom.agent_led config block, default enabled.
- SKILL.md rewritten so natural-language catalog questions map to the CLI
verbs, share/install flows and fixed notification templates.
* wisdom: wire agent-led weekly review into gateway tick and Telegram buttons
- gateway housekeeping tick calls maybe_run_weekly_review with a home
channel sender when a Telegram adapter is available.
- Telegram: wa: callbacks resolved through the ledger (stale-safe), mute
duration keyboard, send_wisdom_agent_recommendation rich card + fallback.
* fix(wisdom): integrate local mediation and harden model and setup boundaries
* fix(wisdom): honor authoritative recommendation policy and defer on failure
* fix(wisdom): synchronize opaque suppression and recheck delivery preferences
* feat(wisdom): route weekly selection through the session-owned assessment queue
* fix(wisdom): prepare and submit the reviewed generated share package
* feat(wisdom): separate native Share preparation from publication consent
* feat(wisdom): sync native mute choices through a leased preference outbox
* feat(wisdom): bind native mute controls to durable preference choices
* feat(wisdom): add scoped desktop and dashboard notification settings
* fix(wisdom): revalidate feed recommendations before assessment and delivery
* fix(wisdom): persist validated delivery receipts before completing notices
* feat(wisdom): add private notification claim and receipt client
* Persist Wisdom send reservations and recover delivery acknowledgements
* Route legacy Wisdom controls through current native review
* Add typed private Wisdom operation outcome client
* fix(wisdom): make agent-led advice usable in the local demo
* fix(wisdom): keep requested consent outside proactive limits
* fix(wisdom): distinguish unavailable assessments and preserve digest text
* fix(wisdom): assess ongoing usefulness beyond the current task
* fix(wisdom): restore immediate qualification sharing controls
* fix(wisdom): separate qualification review from installation advice
* fix(wisdom): collapse review checklists and simplify sharing copy
* fix(wisdom): show compact sharing progress and publication receipts
* fix(wisdom): require credential prefixes rather than matching skill names
* fix(wisdom): finish package checks before presenting sharing consent
* fix(wisdom): scan local skills before qualification cards
* fix(wisdom): update moderation results on existing sharing cards
* fix(wisdom): keep sharing review accessible from receipt cards
* fix(wisdom): align mediated review cards and collapsible checks
* fix(wisdom): clarify clean security summary wording
* fix(wisdom): normalize consent plans and add explicit recheck
* fix(wisdom): keep install and update receipts concise
* fix(wisdom): collapse assessments and deduplicate operation cards
* fix(wisdom): restore private Portal review from native cards
* fix(wisdom): sync Portal publication to original consent card
* fix(wisdom): show local skill version on sharing cards
* fix(wisdom): skip agent recommendations for self-published versions
* fix(wisdom): simplify candidate notices and local-edit recovery copy
* feat(wisdom): submit locally reviewed packages with one confirmation
* feat(wisdom): expose safe receipt and outcome sync recovery
* wisdom: onboarding notice says detect and share, names the user's own skill
Copy review from the product owner on the first and returning
qualification notices (fixed delivery mode):
- the feature blurb now says the org enabled detection *and sharing*
- both notices say the detected skill is one the user created
- both close with an exclamation mark
Applied identically to hermes_wisdom.notice, the desktop and web i18n
strings, and the tests that assert the sentences.
* wisdom: one opener, no approval line, ask to share after the skill is shown
Product owner review of the candidate card.
- The Hermes written card now opens with the same sentence as the fixed card
("Your organisation has enabled Collective Wisdom, a feature designed to
automatically detect and share useful skills across all team members.")
instead of its own blurb, so there is one first time message.
- "Nothing is shared without your approval." removed from Telegram, Slack
and Desktop. The buttons already make the permission explicit.
- "Would you like to share?" no longer appears before the skill is named.
It is now the last line, after the skill name, description, why suggested
and the checks, and reads "Would you like to share it?" (matching the
agent led template wording).
Tests updated for the new order; proposalNotice removed from all desktop locales.
* wisdom: American spelling, organization
Product owner decision: user facing copy uses American spelling.
Changes "Your organisation" to "Your organization" in the chat notice,
the Hermes written card opener, the desktop and web strings, and the
tests that assert them. Identifiers such as nas_organisation:* and the
German and French locales are untouched.
* wisdom: candidate card copy round 4 (owner review)
Apply the product owner's round 4 copy decisions to the Hermes Collective
Wisdom candidate card on Telegram, Slack, Desktop and the shared views:
1. Hermes-written cards are titled "Hermes Collective Wisdom" instead of
the bare "Collective Wisdom".
2. The "Reusable skill ready to review" line is gone from the candidate
card (Telegram rich card and plain fallback, legacy agent-led share
template).
3. The skill name and description are labelled: "Skill name: <name>" and
"What it does: <description>" (Telegram, Slack, Desktop).
4. "Why suggested:" is now "Why others might benefit:".
5. A passing professionalism review reads "Safe to share at work ✓ (no
inappropriate content found)" with no per-check bullets and no "Pass";
a failed review reads "Needs a look before sharing at work (possible
inappropriate content)" and lists only the checks that flagged
something. Pending/unavailable wording is unchanged.
6. Telegram button toasts: "Will ask later...", "Preparing more
details...", "Sharing...".
7. Qualification reasons: "You used this skill consistently across many
days." and "You've really refined this skill."
8. prompts/wisdom_candidate_review.md asks for a compelling
editorial_name, a simple one_line_description and a compelling
why_coworkers_benefit under 300 characters; "Be concise and
convincing." becomes "Be concise and compelling: the goal is that the
user wants to share it."
Tests updated for the new strings; review_text() gains direct coverage.
* wisdom: re-apply owner copy after rebase
- Native share cards (advice_view/interaction_view): drop the approval line, ask "Would you like to share it?" as the last line after the checks
- Hermes-written completion card titled "Hermes Collective Wisdom"
- Qualification reasons use the owner wording (consistently across many days / really refined)
- American spelling (organization) in remaining English copy
- Desktop test asserts the current Share button; web test matches the returning notice
* fix(wisdom): pin reconciled Gateway and verify Unicode hash vectors
Pin Gateway 60cd2d6b613ae3cd4a6e65155d1142006d907e78 and byte-identical producer artifacts. Verify every content-order case and package-manifest binding. Validation: 186 focused Python tests, Ruff and contract verifier.
* fix(wisdom): reconcile optional SDK tests and frontend lint
* fix(wisdom): default to agent-written notification summaries
* fix(wisdom): restore deferred install review and browse controls
* feat(wisdom): inspect installed setup with exact package provenance
* feat(wisdom): run native-approved installed setup steps with durable evidence
* fix(wisdom): recover interrupted setup with explicit native consent
* feat(wisdom): hand native installs into guided setup review
* fix(wisdom): continue requested setup with fixed notification copy
* fix(wisdom): preserve setup while waiting for a session model
* fix(wisdom): expose canonical setup review controls on desktop
* fix(wisdom): resume setup after recorded automatic updates
* fix(wisdom): make missing setup prerequisites recheckable
* chore(wisdom): align Agent with verified Gateway contract
* fix(wisdom): stop guessing team slugs in portal links
* fix(wisdom): retire pending advice on account sign-out
* fix(wisdom): cancel advice after terminal account revocation
* fix(wisdom): fence feed responses across account sign-out
* fix(wisdom): checkpoint signed-out feed before reactivation
* fix(wisdom): link proactive advice to scoped notification settings
* fix(wisdom): coalesce queued publication recommendations by version
* fix(wisdom): keep package review navigation local and deferable
* fix(wisdom): reflect installed state in discovery controls
* fix(wisdom): show exact checks before command confirmation
* chore(wisdom): pin bounded analytics privacy contract
* chore(wisdom): pin retired legacy notification contract
* feat(wisdom): review publisher usage with exact sharing copy
* fix(wisdom): align discovery and review check summaries
* fix(wisdom): show expired consent before confirmation
* fix(wisdom): require fresh review for legacy install controls
* fix(wisdom): preserve review expiry across check toggles
* fix(wisdom): retain update policy in native install reviews
* fix(wisdom): surface failed native card edits
* fix(wisdom): persist local command approval reviews
* fix(wisdom): use saved approvals for messaging commands
* test(wisdom): provide scan result in setup handoff fixture
* test(wisdom): exercise Telegram approvals with saved review state
* fix(wisdom): retain suppression policy for offline deferral
* fix(wisdom): reconsider candidates after deferred suppression expires
* fix(wisdom): bind review checks and report verified readiness separately
* fix(wisdom): persist accepted publication intent and recover exact outcomes
* fix(sync): pin UTF-8 tree ordering across writers
* chore(wisdom): pin organisation-scoped Gateway authorization
* fix(wisdom): restrict consent delivery to user-facing sessions
* chore(wisdom): refresh reviewed Gateway contract pin
* fix(wisdom): preserve kept tools in Blank Slate exclusions
* test(auth): reset anonymous fixture with a profile-scoped cache
* fix(wisdom): gate local surfaces and work on current profile entitlement
* fix(wisdom): invalidate quiet tool cache on entitlement changes
* test(wisdom): authorize local consent gateway fixtures
* fix(wisdom): keep entitlement decoding free of native crypto imports
* test(wisdom): provide local entitlement to demo CLI subprocess
* ci: leave upstream workflow unchanged in Wisdom PR
* fix(wisdom): ship package and contracts in Nix wheels
---------
Co-authored-by: hbizi <36184542+hbizi@users.noreply.github.com>
|
||
|
|
f8456248d9 |
fix: Bedrock Guardrails enforced on the Claude route and blocks surface as refusals
bedrock.guardrail was only attached on the Converse route (guardrailConfig in the body). Claude on Bedrock goes through the AnthropicBedrock SDK, i.e. InvokeModel, whose body has no guardrailConfig, so the default Claude route ran with no guardrail at all (#52179; live-verified by JiaDe-Wu: the blocked word came back through Hermes). Bedrock reads the guardrail for InvokeModel from X-Amzn-Bedrock-GuardrailIdentifier / -GuardrailVersion / -Trace headers. Attach them as default_headers in build_anthropic_bedrock_client so every AnthropicBedrock client Hermes builds (primary init, /model switch, fallback, per-request rebuild, auxiliary) enforces the same guardrail, with prompt caching / thinking / 1M context kept (the reason Claude is not routed through Converse). InvokeModel blocks do NOT change stop_reason (stays end_turn) and return the guardrail's canned text as an ordinary assistant reply, flagged only by amazon-bedrock-guardrailAction=INTERVENED in the body (SDK: response.model_extra). AnthropicTransport.response_finish_reason maps that to content_filter so the loop runs its refusal handling instead of reasoning over the canned text; _derive_finish_reason uses it for the anthropic_messages branch. Mantle (openai.gpt-5.x) is documented by AWS as not supporting Guardrails on the Responses endpoint; the docs now say so instead of promising "all model invocations". Header mechanism proposed in #52312 by @JoaoMarcos44 (stale base, 7-file conflict, detection keyed on a Converse-only stopReason); reimplemented on current main. Live probe (local sink, SigV4 fake creds): before, no X-Amzn-Bedrock-* header on the InvokeModel request; after, headers present, SigV4 intact, INTERVENED → content_filter. |
||
|
|
036a20b3ca |
test(anthropic): one wire_headers probe; the bearer mirror lives only in the portal wire test
The SDK-private _build_headers probe was spelled three times across two files; the api-key shard also re-asserted the bearer invariant the portal test owns. dict(headers or {}) → dict(headers): every caller passes _beta_header's dict.
|
||
|
|
17b3cc2023 |
fix(anthropic): bearer-only clients omit x-api-key the same copy-safe way
The api-key branch now uses an Omit() default header because an attribute clear does not survive with_options(); the mirror bearer branch still relied on `client.api_key = None`. Probe on anthropic 0.87.0: a with_options() copy of a bearer client re-read ANTHROPIC_API_KEY and sent x-api-key alongside the Bearer (#26970's dual-auth leak, on copies). Both branches now set one Omit() header, built once from the beta headers. Wire test asserts the Authorization header is absent outright, not merely sentinel-free. |
||
|
|
9ee217c063 |
refactor(anthropic): Omit() guard reads the sentinel straight off the pinned SDK
The pinned anthropic==0.87.0 exports `Omit` at package level, so the import-probe plus httpx header-stripping fallback added for "SDK too old" never runs; drop both and set the default header directly. Tests trimmed to two invariants: the header-builder contract on the client and a with_options() copy, and the wire-level end-to-end through build_anthropic_client. The bearer-style mirror case stays covered. |
||
|
|
56dfa091f9 |
fix(anthropic): fail closed when the SDK Omit sentinel is unavailable
The api-key credential-isolation guard in this PR only removes the env-derived Bearer when `anthropic._types.Omit` can be imported. When the sentinel is unavailable (older/exotic SDK layout), `_new_sdk_client` fell through to a bare api-key client whose SDK env fallback re-reads ANTHROPIC_AUTH_TOKEN and ships `Authorization: Bearer ***` to third-party endpoints — the compatibility path failed open (P2, reported by @egilewski). Fail closed instead: when Omit is unavailable, build the client with a copy-safe httpx request hook that strips Authorization on every request. `http_client` propagates through `with_options()`/`copy()`, so the original client and every copy are covered without depending on the SDK's header-omission internals — the same custom-client shape already used by `_build_anthropic_client_with_bearer_hook`. Adds a real-loopback e2e test that simulates Omit unavailable and asserts no Bearer leak on the original client and on a `with_options()` copy, while x-api-key and request success are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
42cd1ab2d8 | fix(anthropic): make the api-key Bearer guard copy-safe via Omit() default header | ||
|
|
174eff3f4c |
fix(anthropic): clear SDK env-derived auth_token on api-key-style clients
Anthropic SDK fills an unset auth_token from ANTHROPIC_AUTH_TOKEN and sends it as a Bearer credential alongside x-api-key to third-party Anthropic-compatible endpoints. Mirror the existing bearer-side guard in _new_sdk_client so api-key-style construction clears auth_token after construction, and cover it with regression tests (client attributes, header shape, and an end-to-end local-server capture). |
||
|
|
2b4deeb32b |
fix(auth): key sibling per-process credential memos by profile home under multiplex
Same class as the resolve_nous_access_token memo: three more process-wide memos carried a credential resolved under one profile's HERMES_HOME override into another profile's turn for their TTL. - hermes_cli/nous_billing.py::_token_cache (30s (token, base) memo for the charge poll loop) was a single unkeyed slot -> dict keyed by hermes_home_key(); invalidate_cached_token() clears the dict. - agent/moa_loop.py::_runtime_cache carried api_key/base_url/api_mode keyed only (provider, model) for 5 min -> (hermes_home_key(), provider, model). - agent/auxiliary_client.py::_client_cache_key had no profile component, so callers that omit api_key (pool / Nous auth.json paths) could be handed a client built with another profile's bearer -> hermes_home_key() leads the key. WHY hermes_home_key(): it reads the per-turn HERMES_HOME override the multiplex gateway sets (falling back to the env var), and it is symlink-stable, so the memo key is exactly the credential home the resolution itself read from. Profiles stay independent islands; the default-profile process env never leaks into a secondary's turn. Tests: one invariant per site, proven red on origin/main. |
||
|
|
942973ae90 |
fix: every Bedrock client rebuild lands on the startup wire (Claude SDK, Converse region, guardrails)
Follow-up to
|
||
|
|
4bdd64b334 |
The free tier is created in one place, at boot, only behind HERMES_GUEST_ONBOARDING=1 (NS-847) (#107697)
* fix(auth): close the free tier's gaps against the gateway's welcome-tier contract The inference gateway's welcome tier (NousResearch/api DOCS/anon-tier/plan.md) serves an anonymous account exactly one model on its own host, refuses everything else with a structured 429, cross-refuses a request on the wrong host with a 400 (403 while the tier is dark), and tells a signed-in account that still asks for `nous/welcome` what to switch to in an `x-nous-model-switch` header. Four client-side gaps against that contract: - Auxiliary calls were refused on every session. The auxiliary client asked the welcome host for the Portal's recommended compaction/vision model, a guaranteed 429 `model_not_free` before each fallback. On the welcome host it now uses `nous/welcome` (its backing model covers auxiliary work) and skips Nous for vision, which the welcome model does not take. - The structured 429 body was never read. The classifier now parses `reason` / `retry_after` / `alternates` / `upgrade_url`: `model_not_free` and `feature_not_free` are non-retryable gates that fall back; `at_capacity`, `admission_closed` and `rate_limited` are rate limits that honour `retry_after` and never rotate the free tier's only credential. The wrong-host 400 and the dark-tier 403 are deterministic, so they abort this route and fall back instead of retrying or re-exchanging. The terminal paths say what happened and name the sign-in (`/login` in a chat, `hermes auth upgrade` in a terminal). - The `x-nous-model-switch` header was ignored. The chat-completions transport records it beside the rate-limit and credits headers; the next call moves the session, and the config default when it still names `nous/welcome`, to the backing model the gateway named. - A guest fell back to the paid host. With `inference_base_url` absent from the exchange or outside the host allowlist, routing defaulted to inference-api, where every request is a 400. A guest now defaults to the welcome literal at the exchange, in the shared store's shape, and in effective routing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit fc758aad7efceff6223fc144a9b5c69f13e41bd8) * feat(auth): the free tier is set up on request; nous.guest_setup decides whether also on first use A caller that names nous/welcome on a Nous route with no Nous identity in reach — the guided setup's session (provider=nous, which skips the resolver's nothing-configured rung), the free-tier picker row, a bare --provider nous pointed at it — is asking for the free tier. The OAuth runtime rung now sets it up there instead of failing "not logged in", so the guided chat no longer races the root profile's first-run mint. nous.guest_setup is the policy seam: "auto" (default) keeps today's first-use setup wherever nothing else is configured; "on-request" mints only when the free tier is asked for by name (nous/welcome, /login, hermes auth upgrade, replacing a retired identity). Implicit callers — the resolver's last rung, the first-run check, free_tier.status, the CLI's background setup, the connector token path — still adopt what the shared store holds, so every profile follows the one identity the guided setup created, but never create one on their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit ae915ddc65ecdb81b81e29b604671d15cd49233c) (cherry picked from commit 62ad1ff3ab200ea064975a32c502041b25910165) * feat(auth): the guided setup provisions the free tier explicitly; nous.guest_setup is auto | explicit Two questions govern the free tier: may it exist (nous.guest) and who may CREATE the identity (nous.guest_setup). "auto" (default) keeps today's first-use setup wherever nothing else is configured. "explicit" means Hermes never creates one on its own: the only creator is the new provision_free_tier() primitive, exposed as the free_tier.provision RPC, which the guided setup on Hermes Desktop calls as its first step — on the root gateway, before the setup profile and before the guided chat exists — so the identity lands in the root store every profile reads through and is there before any session asks for nous/welcome. That closes the race against the backend's own setup, and makes "only when the setup-bot flow is used" literally true. The earlier "on-request" tier is replaced: it minted whenever any caller named nous/welcome (the hermes model row, --provider nous), which treated a model name as intent and was broader than the guided setup. Under "explicit" a nous/welcome request with no identity fails "not logged in" as before the free tier existed, and /login or hermes auth upgrade report nothing to sign in from. Implicit callers still adopt an identity the shared store holds, and a retired credential is replaced (a continuation, not a creation). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit c63d2c935c1e59016164fdfb90cf70b4094466a0) * fix(auth): remove the nous.guest_setup knob; the free tier is created on first use `nous.guest_setup: auto | explicit` decided who may CREATE the free-tier identity. Under its default every line it added was inert (`may_mint` always true), nothing in tree set `explicit`, unknown values read as `auto`, and under `explicit` a CLI-only install could never get an identity, which contradicts the first-run contract (first command mints, then chats). The mint race the knob accompanied is already benign: every caller takes the profile lock then the shared-store lock, and the loser adopts what the winner wrote. What makes the guided setup win deterministically is `provision_free_tier()` behind the `free_tier.provision` RPC, which stays. `nous.guest` remains the only free-tier policy. Removed: `guest_setup_policy()` and its constants, the `explicit=` / `may_mint=` threading through `ensure_portal_identity` and `_reconcile_and_provision`, the flag at the three replacement call sites (now no-ops), the config default, the docs section, and the four `guest_setup` test-config entries. The three policy tests that hold regardless of the knob are kept under `TestExplicitProvision`; the two that only tested the knob are deleted. (cherry picked from commit d8a50526d93c374c0067dd935b5a65055e0af261) * fix(gateway): a server-driven model switch off nous/welcome does not evict the cached agent When a signed-in account still asks the paid host for `nous/welcome`, the inference gateway serves the current backing model and names it in `x-nous-model-switch`. `apply_model_switch` moves the live session to that model and moves `config.yaml`'s default off the alias in the same step. The messaging gateway's fallback-eviction check compares the agent's model with the config default and evicts on any mismatch that is not a /model override, so when the config write did not land (unreadable config, lock) the cached agent was evicted once per turn, and prompt caching with it. `apply_model_switch` now stamps the alias it moved the session off on the agent, and `_is_intentional_model_switch` treats "agent moved off the alias the config still carries" as deliberate, beside the existing /model override case. The check takes the agent and the config model instead of a bare model string; its one caller in `_run_agent_evict_on_fallback` passes them. (cherry picked from commit 696d1ec86b69db28bf002c841e9389b85178a954) * fix(auth): the free tier outranks implicit host credentials in provider resolution On a fresh install with a leftover ~/.aws profile, resolve_provider("auto") reached the Bedrock rung before the free-tier rung, so the first turn ran on Bedrock and failed 403 while the free tier was still being minted in the background at agent setup (NS-829). Live on a Mac with ~/.aws present: 28 s, three retries, no answer; the next process then switched to nous/welcome. The free-tier rung now sits directly above the Bedrock chain: when nous.guest is on, an existing free-tier identity answers, else a blocking mint runs, and only then does the boto chain get a say. Everything above is unchanged and still wins: CLI creds, config.yaml model.provider, env keys, the OpenRouter pool, a logged-in active_provider. nous.guest: false skips the rung, and a failed mint still falls through to Bedrock and the no-provider guidance. Tests: six precedence cases (identity present, fresh mint, free tier off, env key still wins, sign-in still wins, failed mint falls through). The opt-out test now neutralizes the AWS chain like the precedence tests do; on a machine with ~/.aws it was failing for the same reason as the bug. Live after the fix, same Mac, AWS credentials visible, isolated shared store: identity minted 2 s in, turn on model=nous/welcome provider=nous, answer in 11 s. (cherry picked from commit a04b05260cd334dd7199ad9b6cd5b2538364c75a) * fix(auth): review follow-ups for the free-tier rung (NS-829) - tests/agent/test_bedrock_integration.py: the Bedrock auto-detect test switches the free tier off; its contract is the boto chain, and the free tier now sits above it. - gateway/run_notifications.py: the free-tier startup line reads auth.json before consulting the resolver, so a gateway boot on a machine with AWS credentials never mints or refreshes over the network. - hermes_cli/anon_auth.py: module docstring says where the free tier sits in the ladder instead of "the ladder is untouched". - tests/hermes_cli/test_provider_precedence.py: two invariant tests instead of six (parametrized ladder cases; a failed mint that returns None or raises falls through to Bedrock). scripts/run_tests.sh on the five affected files: 147 passed, 0 failed. (cherry picked from commit 10790d148c60ada11b9ecdde2cd2c836c6a82a11) * feat(auth): HERMES_GUEST_ONBOARDING=1 is the one launch gate for the free tier; HERMES_FORCE_GUEST is gone The free tier is pre-GA. Until GA it must not exist for anyone who did not ask for it: no identity minted, no portal traffic, no free-tier copy on any surface. One environment variable now decides that, and one function reads it. `guest_enabled()` returns False unless `HERMES_GUEST_ONBOARDING` is exactly "1"; only then does `nous.guest` (the user's off switch) get consulted. Every free-tier site already funnels through `guest_enabled()`, so the gate closes minting, routing, connector entitlement, status lines and the picker row in one place. With the variable unset, `resolve_provider("auto")` on a fresh install raises `no_provider_configured` exactly as upstream does. `HERMES_FORCE_GUEST` and `force_guest_mode()` are removed. They inverted the gate (forced the tier ON over `nous.guest: false`), their "new" value re-minted identities as a side effect of provider resolution, and `_has_any_provider_ configured` read them ahead of every other check, making the CLI a second reader of a flag that must have exactly one. `_forced_new_done` and the `force` parameter of `_reconcile_and_provision` go with them. Supersedes the dev lever introduced in fcf9d11679 (rung 1) and hardened in b5c162c3ec. Ruling: NS-845 Q1.1 (recorded on NS-847). Not a user preference: the variable is never written to config.yaml or .env and never shown in setup. It is deleted at GA together with its comment in anon_auth.py. This is a deliberate, temporary exception to the "no new HERMES_* env vars for non-secret config" rule. Tests: fixtures set the gate instead of deleting the old lever; one new invariant (`test_launch_gate_off_means_no_free_tier_at_all`) proves that "", "0", "true" and "new" all leave the tier off with zero portal calls, red on the previous commit. The `HERMES_FORCE_GUEST=new` re-mint test is deleted with the feature. * feat(auth): the free-tier identity is created in one place, at boot; every other site is a read Before this commit eight sites could create a Nous free-tier identity as a side effect of something else: resolving a provider, the CLI's first-run check, the CLI's session setup (in the background beside an own key), a connector bearer read, the desktop polling `free_tier.status`, the sign-in precondition, the desktop's `free_tier.provision`, and the dead-credential re-mint. A poll could mint. Provider resolution could hit the network. Two of them raced each other on a fresh install. Now `hermes_cli/free_tier_bootstrap.py::run_bootstrap` is the only creator. `hermes serve` runs it on a daemon thread from `_lifespan` beside the other background boots; `cmd_chat` runs it synchronously before the first-run guard. It inventories credentials first (`resolve_provider("auto", skip_free_tier=True)`: what would carry inference if the free tier did not exist), creates the identity only when `guest_enabled()`, resolves inference, records a `SetupRecord` in process memory and broadcasts ONE `setup.ready` event. It runs on every boot; only the mint is gated. `ensure_portal_identity` now requires `explicit=True` and raises otherwise. Its callers are the bootstrap, the desktop's `free_tier.provision` (the explicit retry when the boot could not create the identity) and the two dead-credential replacements (`auth_nous.resolve_nous_runtime_credentials`, `managed_tool_gateway._replace_dead_guest_token`). The background thread path and `provision_free_tier` are deleted with their last callers. Reads that used to mint and now only read: `auth.py::resolve_provider` rung 7 (an existing identity still outranks the Bedrock chain, NS-829 ordering kept), `main.py::_has_any_provider_configured`, `cli_agent_setup_mixin._ensure_runtime_credentials`, `managed_tool_gateway.read_nous_access_token` (no identity -> None), `anon_sign_in.run_sign_in` (no identity -> Unavailable), `methods_free_tier` `free_tier.status`. `setup.status` answers from the record for the launch profile, blocking up to 8 s while the bootstrap is in flight so a client's first poll lands after the identity exists rather than racing it; a named profile, or a process that never ran the bootstrap, keeps today's live probe. The record's fields ride along additively (`ready`, `free_tier`, `other_providers`, `inference_provider`). Identity and inference are decoupled (NS-845 Q1.3): the mint sets `active_provider="nous"` only when the inventory found nothing else usable (`_mint_locked(carries_inference=)`); an adopted account always does. A token refresh no longer re-elects the provider it refreshed (`_save_provider_state_to_source` writes credentials, not the user's choice) — that write was how an own-key install ended up on the free tier after the first connector call. Supersedes the mint sites in fcf9d11679, a42d0748fc (first-run check), bbbaa8935a (CLI background setup), 0179efc989 (`free_tier.status` mint), 62ad1ff3ab / c63d2c935c / d8a50526d9 (the `nous.guest_setup` knob and `provision_free_tier`), and a04b05260c (blocking mint in the resolver). Ruling: NS-845 Q1.2 + Q1.3, recorded on NS-847. Tests: `TestBootstrapIsTheOneCreator` (one mint per process; own key keeps inference; reads never reach the portal; a refused mint is memoised), `free_tier.status` fails loudly if it ever calls the creator, the resolver stub fails loudly if resolution ever mints, `setup.status` reads the record, `skip_free_tier` proves the inventory question. The three sign-in tests for the deleted pre-mint collapse into one (`no identity -> Unavailable, zero portal calls`). Live: real `_lifespan` boot with a fake portal, gate on and off (/tmp/ns847-recon/evidence/e2e-rung5-c2-serve-boot.txt), and the CLI matrix incl. an own-key cell (e2e-rung5-c2-bootstrap.txt), 20/20. * fix(credits): the welcome host is free-tier evidence, so a free-tier identity never sees "run /topup" A free-tier identity carries $0 by design, so the portal seed reports `paid_access=False` for it. `is_free_tier_model` did not know the welcome host, read that as a depleted account, and every free-tier turn ended with the credits-depleted notice telling the user to top up an account they do not have. Rule (4) in `is_free_tier_model`: a `base_url` on the Nous welcome host (`anon_auth.route_is_welcome_host`) is the free tier. The host is the evidence, not the model name: the paid inference host can serve `nous/welcome` to a named account and that account's depletion is real, so `("nous/welcome", <inference host>)` stays False. Local data only, like the three rules above it. Restores the two contracts dropped by hermes-magic 674e11d1eaa (the prototype line ran without unit tests): the welcome host is free without any pricing evidence; the model name alone is not. The first is red without this fix. * fix(copy): free-tier text stops promising a connector transfer and never names the config key Sign-in copy on every surface said "Sign in to keep your connectors" and ended with "Your connectors are kept." The transfer registry that would make that true is empty (NS-821): nothing carries over today. The copy now says what signing in does give ("unlock more models and tools") and the completion line names the account, not a transfer. The docs page loses the "connectors carry over" paragraph for the same reason. The picker's off-state line exposed `nous.guest: false` and the word "guest"; user copy names the free tier only (R-USR-1). The docs page gains the pre-rollout note: until GA nothing on it happens without `HERMES_GUEST_ONBOARDING=1`. Its "first command mints" and "replaced on next use" sentences now describe the boot bootstrap. zh is a strict locale: the `freeTier` block was English placeholder text copied from `en`; it is now Chinese. `connectorsKept` is renamed `completedBody` since it no longer talks about connectors. * feat(desktop): the free-tier launch flag is decided once in Electron and stamped onto every backend spawn The Python backend reads `HERMES_GUEST_ONBOARDING` and treats exactly "1" as on. Until now nothing in the desktop set it, so a packaged app could never turn the free tier on, and a backend spawned by the app could disagree with the app about whether the tier was live. `electron/guest-onboarding.ts` owns the decision: `guestOnboardingEnabled` is true when the launch env has `HERMES_GUEST_ONBOARDING=1` or argv has `--guest-onboarding` (the packaged-app spelling). It is read ONCE at launch into a module constant. `desktopBackendSpawnEnv` wraps every backend env as the outermost call and writes the flag LAST, as "1" or an explicit "0", so no earlier spread (`process.env`, `backend.env`) can resurrect a stray value from the parent shell. Stamped onto all three spawn sites: the primary `serve` spawn, the pooled per-profile spawn, and the remote SSH `exec env ...` command (which gains ` HERMES_GUEST_ONBOARDING=1` only when on). The embedded terminal PTY and the backend probes are not backend spawns and do not get it: a `hermes --tui` typed in the pane must not mint. The renderer learns the same fact read-only through the existing `hermes:launch-flags` sync IPC (`guestOnboarding`) and preload (`window.hermesDesktop.guestOnboardingEnabled`). Ruling: NS-845 Q1.1 / Q2 (env var is the contract, `--guest-onboarding` maps to it in main). Two invariant tests on the pure helpers: only "1" or the argv flag enables; the spawn env carries "1"/"0" as the last word and preserves every other key. * feat(desktop): the renderer learns free-tier readiness from one `setup.ready` push, not a 60 s poll The backend's boot bootstrap now announces `setup.ready` once, after it has created (or refused) the free-tier identity and resolved the inference route. The renderer used to discover both by polling `setup.status`, `setup.runtime_check` and `free_tier.status` every 60 s from `useStatusSnapshot`; a fresh install's chip, notice strip and onboarding overlay could sit stale for up to a minute after boot, and three RPCs a minute per window kept asking a question whose answer changes only at boundaries the backend already announces. `handleLifecycleEvent` routes `setup.ready` (active source only, like `skin.changed`) to `notifySetupReady()`, a one-shot tick atom in `live-sync.ts` beside the other change ticks. `useStatusSnapshot` listens to it and runs one readiness round at once (`setup.status` + `setup.runtime_check` + `free_tier.status`). The readiness legs also run once on open and on return from another app, as today. The 60 s tick keeps only `getStatus()`. `SetupStatusSnapshot` types the record's additive fields (`ready`, `free_tier`, `other_providers`, `inference_provider`); readiness semantics are unchanged and still key on `provider_configured` + `runtime_check`. Ruling: NS-845 Q1.2 (renderer half). Tests: the lifecycle branch fires one refresh from the active source and none from another; the snapshot hook's contract is three legs on open, one leg on the tick. * fix(cli): the banner names the free tier's model instead of "no model configured" The welcome banner prints before credentials resolve, so on a fresh install `model` is empty and the banner said, in red, "no model configured — run /model or hermes setup". Under the free tier that is false: the route is already known from local state (identity on disk, tier on), and the first message will run on `nous/welcome`. `_banner_left_lines` now asks the route the same question when `model` is empty (`guest_carries_inference()`, a local read) and shows `welcome · Nous Research`. When nothing resolves the red line stays. Ruling: NS-845 ("the banner's 'no model configured' line reads the resolved route"). Live: fresh HERMES_HOME + fake portal, gate on -> `welcome · Nous Research`; gate off -> the red line, zero portal calls. * fix(aux): vision on the free tier uses nous/welcome too The text-only modality on the gateway's `nous/welcome` row is DeepSeek V4 Flash's, the backing model until the repoint; `z-ai/glm-5.3-flash` is natively multimodal and the repoint declares the welcome row `text+image->text`. Skipping Nous for vision on the welcome host would have sent every image step past the free tier for no reason, so the auxiliary client pins the route's one model for every lane. A backing model that takes no images answers with the upstream's own error, which the ladder handles as it always has. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 7456e028faba55480db43015dc2c8df3e393a415) * fix(gateway): hermes gateway run is a boot owner of the free tier too Rung 5 made every demand-time free-tier site a read: resolve_provider, the connector token, the /login precondition. That is only correct if every process that can reach those sites ran the bootstrap first. The CLI (cmd_chat) and hermes serve (_lifespan) did; the standalone messaging gateway did not. A fresh HERMES_HOME with the gate on and `hermes gateway run` reached provider resolution with no identity to consume, and /login returned Unavailable. Reported by @andrexibiza on #107697 (P1). GatewayRunner.start now runs `free_tier_bootstrap.run_bootstrap` on an executor thread right after startup recovery and BEFORE any adapter connects, so a fast first DM cannot arrive with nothing to resolve. It is its own step, not part of the turn-machinery warm-up: the warm-up is an optimisation with an off switch (HERMES_STARTUP_WARMUP_TIMEOUT<=0); the bootstrap is correctness and must always run. With the gate unset it is a local inventory and no network. Live, real GatewayRunner.start against a fake portal in a fresh home: gate on -> 1 create, identity persisted, resolve_runtime_provider=nous, /login precondition sees the identity gate off -> 0 portal calls, no identity, no_provider_configured Before the fix the gate-on row was identical to the gate-off row. Test: the bootstrap seam runs before _start_prefilter_platforms and delegates to the one creator. Red on 5554eb6993 (no seam), green here. --------- Co-authored-by: Robin Fernandes <robin@soal.org> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
cbcf7b72f7 |
feat(gateway): sign in with a Nous account from a chat (/login), one shared sign-in flow (#105261)
* refactor(auth): one sign-in flow behind SignInState, rendered by the CLI and the desktop * feat(gateway): /signin signs the free tier into a Nous account from a DM * feat(cli): chat surfaces name /signin as the sign-in verb * fix(auth): review follow-ups for the shared sign-in flow and /signin * fix(i18n): carry the /status free-tier line in every locale catalog * refactor(cli): the chat sign-in command is /login * fix(auth): durable override cleanup in the /login sweep, and the sign-in flow in its own modules |
||
|
|
a2db110ccc |
feat(auth): Nous free tier: free inference and connectors out of the box, one command to sign in (#105258)
* feat(auth): Nous free tier core: anonymous identity minted on first use, welcome inference, shared-store scoping A fresh install with no provider sets up a free Nous identity (anonymous auth method of the nous provider) instead of forcing the setup wizard. The identity is persisted through the same path a real login uses, so the resolver ladder is unchanged. Two seams differ: token acquisition re-exchanges the anon credential (no refresh token), and routing pins the welcome host's single model nous/welcome. One identity per shared store; nous.guest: false turns the free tier off. * test(auth): free tier core contracts: lifecycle, resolver precedence, exchange seam, model pin * docs(user-guide): free tier and signing in New page explaining what a fresh install gets before any key or sign-in (free inference on nous/welcome plus connectors), how the free tier coexists with a user's own API key, how to sign in with hermes auth upgrade and keep connectors, how to turn the free tier off with nous.guest, what hermes logout does in each state, a troubleshooting table, and a plain privacy note. Wired into the Using Hermes sidebar. * fix(auth): logout leaves the free tier alone and clears the shared store for a real Nous account Logging out of the free tier is a no-op: it is not a login, so nothing is cleared and the user is told they were never signed in. Logging out of a real Nous account now also clears the cross-profile store, so a profile logout is not silently re-adopted on the next boot. * fix(model): switching off the free tier points at signing in, never hops providers * Name the free tier in the gateway startup notice and tell explicit-provider installs about it once * Render the Nous free tier as free tier on auth status, auth list, hermes status and portal info, short-circuit billing copy for it, and skip the keepalive when there is no refresh token * fix(auth): free tier is set up where nothing is configured: resolver last rung and first-run check Both the provider resolver's terminal rung and the CLI first-run check now try to set up the free tier before declaring nothing configured. On a fresh install the first command lands in chat on nous/welcome; a failed setup still falls through to the existing guidance. * Add hermes auth upgrade: sign the free tier into a Nous account while keeping its connectors The device-code flow runs as usual, with a promotion intent registered on the portal between the code request and the token poll so the account that approves the code inherits the free tier's connectors. The promotion status decides the outcome: only a completed one is followed by the token grant, which is persisted over the free-tier singleton and the shared store. Declined, superseded, retired and busy outcomes each print their own plain copy, and a retired identity is cleared so the next use sets up a fresh one. User-facing text never names the free tier's internals. * Show the Nous free tier as one picker row with nous/welcome and hide it when nous.guest is off * fix(auth): upgrade opens the consent page for this sign-in; one mint attempt per process; forced free tier wins the first-run check The browser leg of hermes auth upgrade now prints and opens the promotion claim URL with the claim code, not the generic device page. A failed mint is attempted once per process so several bootstrap sites cannot hit a closed gate or a 429 twice; a retired credential resets that so re-minting still happens. HERMES_FORCE_GUEST is honoured ahead of the first-run provider check. * fix(auth): pin the welcome model on the selected route, not on profile state; background setup retries after a failure A credential-pool entry can select a paid Nous key while the profile singleton is still the free tier. The model pin now keys on the resolved endpoint (welcome host) in agent init and /model, and the pin in model normalization is removed since it had no route to look at. A failed background identity setup releases its latch so a later attempt in the same process can try again. * fix(auth): decide the Nous model together with the route on every credential-pool swap The credential pool can move a Nous agent between the welcome host and the portal host after init. One helper, pin_model_for_route, now runs at init and inside every pool swap, so the welcome host always carries nous/welcome and a paid endpoint always keeps the caller's model. * fix(auth): apply the route model policy on every wire mode during a pool swap; release the setup latch if the thread cannot start * fix(auth): free-tier lifecycle takes profile then shared lock, reconciles with the shared store, persists the mint before exchanging, and clears only the identity that died The shared store is the identity of record for a Hermes root: a profile holding a stale free-tier identity adopts a sibling's newer sign-in instead of keeping the guest, and never overwrites the shared account. Locks are taken in the documented order (profile, then shared). A minted credential is stored as soon as create succeeds, so a rate-limited or timed-out exchange does not lose it and trigger a second mint. Retiring a dead credential removes only that credential from both stores. Guest exchange uses the resolver's canonical portal URL. * fix(auth): a credential rotation never rewrites the conversation model; connectors honour the off switch and replace a retired free-tier credential The welcome host serves one model, so a rotation onto it is refused for any conversation on another model instead of silently switching that conversation to nous/welcome (the model pin applies only when a route is first chosen). The connector token path now treats the free tier as absent when nous.guest is false, including cached tokens, and shares the one dead-credential rule with inference: a retired identity is replaced once rather than returning its stale token. * fix(auth): plain login never imports the free tier as OAuth credentials; the gateway startup line reads persisted state only A free-tier identity in the shared store is not an OAuth credential to offer for import; a real sign-in replaces it. The gateway's startup notice now answers provider precedence from persisted state (no token refresh at boot), so an expired free-tier token cannot stall the online message. |
||
|
|
2ddeba9e17 |
Merge pull request #99523 from somewheresy/justin/e-1047-route-hermes-actual-provider-through-chat-completions-with
fix(providers): use chat completions for all Actual routes |
||
|
|
6ee218602d |
feat(relay): pass API request metadata to Relay tracking
Signed-off-by: David Gardner <dagardner@nvidia.com> |
||
|
|
564aef2946 |
fix: /model onto Bedrock Mantle keeps SigV4 auth instead of 401ing
Startup (agent_init._init_openai_client) ran configure_bedrock_openai_client_kwargs,
so the aws-sdk sentinel became a SigV4-signing httpx client. Every later client
rebuild — switch_model, fallback restore, credential rotation, request-scoped
clients — went through create_openai_client with bare {api_key, base_url} kwargs,
so the OpenAI SDK sent "Authorization: Bearer aws-sdk" and Mantle answered 401
"Invalid bearer token". Symptom: `hermes --provider bedrock --model
openai.gpt-5.6-terra` works, `/model openai.gpt-5.6-terra` inside the CLI/TUI fails.
Install SigV4 in create_openai_client itself (the single chokepoint every primary
OpenAI-wire client passes through) whenever the base_url is a Mantle host, so all
rebuild paths inherit the fix rather than each remembering to call the adapter.
A real AWS_BEARER_TOKEN_BEDROCK key is left alone (the adapter only rewrites the
aws-sdk / no-key-required placeholders).
Live repro (local HTTP sink capturing the Authorization header after switch_model):
before "Bearer aws-sdk", after "AWS4-HMAC-SHA256 Credential=...".
|
||
|
|
8a6b5b67a7 | fix(providers): block Actual at Responses send sites (E-1047) | ||
|
|
4135933ec9 | fix(providers): preserve Actual routing during startup and key reload (E-1047) | ||
|
|
8b2b83a906 | fix(providers): pin all Actual routes to chat completions (E-1047) | ||
|
|
d7b0a72c2a | fix(providers): route Actual through chat completions (E-1047) | ||
|
|
f98cb00a8b |
fix(vault): 2FA review follow-ups — per-digit fill only for an unmistakable maxlength=1 widget; honour otpauth digits/period/algorithm
Reviewer findings on #107585: - build_otp_fills split any >=4 code-like controls into digits. A page with promo/zip/referral 'code' inputs next to the real OTP box would have had a digit sprayed across unrelated fields. Split now requires exactly len(code) controls that are all maxlength=1, same form, adjacent in DOM order (inspection JS exports maxLength); anything else fills ONE field, the best-scoring one. Verified on the real Browser Use stack: 6-box widget gets one digit each; scattered page fills only the one-time-code input. - normalize_otp_secret dropped digits/period/algorithm from otpauth:// URIs, so an 8-digit or SHA-256 authenticator would get wrong codes. Non-default parameters are now stored as seed|digits|period|algo and honoured (RFC 6238 SHA-256 8-digit vector added); hotp:// is rejected explicitly. - rebase on main (prompts.ts conflict) + prettier. |
||
|
|
d9ca9c974d |
feat(vault): two-factor codes — automatic from a saved authenticator key, otherwise asked for in the user's UI
Follow-up to #106480. Sites that ask for a code after the password stopped the agent cold: the login classifier excludes one-time-code fields on purpose (a password must never land in an OTP box) and there was no tool for the second step, so the only move was to ask in chat. browser_vault_enter_code Fills the one-time code the current page asks for. Two sources, same invariant as passwords (the code goes to the page over the supervisor socket and never enters model context): - a TOTP seed on the login: local vault `otp_secret` (RFC 6238, stdlib, verified against the RFC test vectors), 1Password `op item get --otp`, Bitwarden `bw get totp`. Nobody is asked. - no seed: the surface prompts "Verification code for {site}"; the user types what their phone/email/app shows. Enter on empty / Skip declines and the tool returns code_declined ("do not ask again this turn"). no_code_field tells the model the site wants a passkey / hardware key / app approval: hand it to the user's device and wait for navigation. Per-digit OTP boxes (maxlength=1 pattern) get one digit each in DOM order. Surfaces CLI: sudo-style panel, code shown as typed (not a secret worth masking, typos must be visible), Enter submits, ESC/empty skips. Desktop: "Verification code for {site}" card via vault.code.request / vault.code.respond (gateway), owner-routed like the other vault prompts. Settings → Passwords & Logins: optional "Authenticator key" field on the add form (base32 or otpauth:// link); items with one show a "2FA auto" badge. `hermes vault add` asks for the same optional key. browser_vault_fill's result now says what to do next ("if the site asks for a verification code, call browser_vault_enter_code with this handle"). Six locales. Verified live (real model, local 2FA site that checks the TOTP; CLI PTY): A. login saved with authenticator key → signed in through 2FA, zero prompts, code/password absent from the transcript B. login without key → code panel → user types code → signed in C. panel dismissed → agent stops and explains, never asks in chat Unit: RFC 6238 vectors, seed normalisation, mint-without-asking, per-digit spread, decline, no-code-field; Desktop card test (owner routing, trim, Skip). |
||
|
|
a42388b3a9 |
feat(agent): a2a_key names a bot author's turns
Dropped from the #103888 salvage for lack of a consumer; the honcho a2a session key is that consumer. |
||
|
|
98d11c95f4 |
feat(vault): zero-setup UX — save a login on the page that needs it, managers auto-detected, one "Passwords & Logins" surface
Nobody should have to learn `hermes vault add` or find a toggle before "log into GitHub" works. - browser_vault_save_login: when the agent reaches a sign-in page with no saved login it asks the user on THEIR surface (CLI two-step panel on the sudo modal: identifier shown, password masked; Desktop card with labelled Email/username + Password fields). The answer goes to the encrypted vault bound to the page origin and is filled at once; the model gets back only the handle and identifier. Declining returns save_declined; headless sessions get prompt_unavailable. Never a password in chat. - Vault tools ride with the browser toolset (check_browser_requirements) instead of appearing only once the vault has items — an empty vault is exactly when save_login is needed. browser_vault_list hints at it when empty. - 1Password / Bitwarden are login sources as soon as their CLI is installed; `vault.<name>.enabled` is opt-OUT only. Settings shows Detected/Locked/Unlocked/Off/Not detected with a switch only for installed managers; `hermes vault sources` reports detection, `--disable`/`--enable` flip the opt-out. - Desktop nav/page renamed "Passwords & Logins"; empty state tells the user they do not need to add anything; all five locales updated. Docs rewritten from "how it works" to "say log into X". - New per-thread SaveLoginPrompt callback (agent/vault_backends/unlock.py) installed beside the unlock prompt on every CLI site and the gateway bridge (vault.save_login.request/respond/expire), propagated to worker threads via tools.thread_context. Live: CLI PTY (real model, packaged Chromium, local login server) — panel shown, identifier + masked password typed, server received the correct password, password absent from terminal transcript and from every file under HERMES_HOME outside vault/. Native Electron (headless, isolated HOME/HERMES_HOME, own Vite + CDP port) — card shown, "Save & sign in", server received the password, Settings lists the saved item, password absent from the rendered UI. |
||
|
|
c8998c3957 |
feat(vault): fill payment cards and addresses at checkout, cards behind a confirm prompt
payment and address items could be stored (CLI wizard, Desktop dialog) but nothing could fill them: a dead surface holding real card numbers. browser_vault_fill now handles all three kinds through the same origin-bound, supervisor-only, redacted path: - classify_checkout_control / select_checkout_fills map WHATWG autocomplete tokens (cc-number, cc-exp[-month|-year], cc-csc, address-line1/2, address-level1/2, postal-code, country-name) with label/name heuristics as backup; a combined "MM/YY" control gets exp_month+exp_year and suppresses the split fills; inspection now covers <select> (country, state, expiry month) and the fill script picks an option by value or text. - Every payment fill goes through request_elicitation_consent (gateway button round-trip or CLI panel) before a byte is written; declined → payment_declined, headless sessions are refused. A prompt injection that reaches a checkout can ask, not spend. Card values join the redaction registry like passwords; the result lists targeted field tokens only. - Origin is now required for every kind (CLI wizard asks; Desktop dialog always shows the field) because a card without a bound origin is unfillable. - The tool descriptions, docs and CLI copy drop "Phase 1 / login only". Live (evals/vault_fill_live_e2e.py, real browser_exec + packaged Chromium): decline writes nothing; accept fills card/expiry/CVC on the /checkout tab, leaves the email box and the country <select> untouched, and neither the card number nor the CVC appears in any result. browser_vault_tool also: focuses the tab on the bound origin holding the right form before the origin pre-check (focus_page from the previous commit); tool descriptions say "the browser's input tool" (rewritten per session by model_tools); _check_vault_available is registered uncached because its answer is per profile (vault dir + config) and the probe is a file stat. |
||
|
|
27ca97e469 |
fix(vault): cross-process store lock + fsync; profile-scoped, bounded redaction registry
Two review findings on #96988/#106480 that were still open: - VaultStore serialized read-modify-write with a threading.Lock only. The Desktop gateway, a CLI `hermes vault add` and a TUI slash worker are separate processes writing the same vault.json.enc, so two adds could drop each other's items, and the temp file was renamed into place without fsync (a crash between rename and the next sync loses the vault). Writes now take an flock/msvcrt lock on <vault>/.vault.lock and fsync file + directory. - The vault redaction registry was one process-global, unbounded set. Under gateway multiplexing profile A's passwords scrubbed profile B's browser output (and confirmed to B that those bytes exist). It is now keyed by profile home, capped at the 64 most recent values per profile, and clearable (clear_vault_redaction_values). Also canonicalizes payment/address payloads (PAYMENT_FIELDS / ADDRESS_FIELDS, required fields enforced, stray keys dropped) so the checkout fill in the next commit never has to guess a user's ad-hoc field names; the Desktop dialog already wrote these names. |
||
|
|
dedc99ec6c |
fix(vault): ownership and race findings from the second independent review
Manager tokens: lock generation fence (a Lock acknowledged while `bw unlock` / `op signin` is still running discards the late token); tokens record the unlocking gateway session and are released when THAT session ends, not when any sibling session in the profile is torn down. 1Password: OP_CONNECT_HOST/TOKEN come from the profile's scoped secret store like the service token (Connect outranks a service token inside op), never from the launch environment. Vault RPCs bind params.profile (home + secret scope) so a shared remote backend serving several profiles locks/lists/unlocks the requested one; unknown profile → RPC error, not a crash. Fill target: inspection stamps are `<nonce>:<index>`; a fill resolves only its own inspection's stamps, so an interleaved second inspection can no longer redirect A's password into a newly mounted field (real Chrome: 0 filled, both fields empty). Desktop Settings: every RPC goes through the owner profile's socket (requestGatewayForProfile), query keys carry (connection, profile), an owner change closes dialogs and wipes drafts (a master password typed for A is never submitted to B; a late list from A never paints under B), and vault.add secrets travel in a ref consumed by the mutationFn instead of mutation variables. Three owner-routing invariant tests on the real component. Docs/PR body: session-scoped release, lock-race semantics, bw --passwordenv. |
||
|
|
5aa9e7f033 |
fix(vault): independent-review findings — vendor contracts, profile scope, transport, target binding
Bitwarden unlock now uses the CLI's documented non-interactive channel: `bw unlock --raw --nointeraction --passwordenv VAR`, VAR set on the child environment only (bw 2026.x rejects a piped password with "Master password is required"). Verified against the real published binary. Manager session tokens are keyed by (profile home, backend): a Desktop gateway hosting several profiles can no longer reuse or lock another profile's session. Status probes (`vault.sources`, is_unlocked) no longer refresh the idle TTL; only real manager calls do. Gateway session teardown locks the profile's managers (a per-session unlock ends with the session). 1Password service-account token comes from the profile-scoped secret store (get_secret), not ambient os.environ. `vault.source.set` no longer references a module constant (bind_module rebinding dropped it → NameError on every Settings toggle). Fill target binding: inspection stamps each input with a per-inspection slot attribute; the fill resolves by stamp and requires type=password, then strips every stamp. A DOM reflow between inspect and fill can no longer redirect the password into a text field (reproduced in real Chrome before, 0 filled after). Redaction boundary: no 4-char floor, CR/LF-normalized form registered (what a text input actually stores), JSON object KEYS scrubbed in both browser redactors; longest value first. Docs now state the real trust model: accidental-disclosure protection, not an execution sandbox. Desktop: the mid-turn card sends the master password through the owning session's socket (requestForOwnedSession), never the ambient foreground gateway; `vault.unlock.expire` clears a stale card; Settings keeps the master password out of react-query mutation variables (ref consumed by the mutationFn). One renderer invariant test for the routing. |
||
|
|
e9f7d44f3f |
fix(vault): carry the unlock prompt onto tool worker threads; honour binary_path in install checks
Live Desktop repro: the fixture model called browser_vault_unlock and got unlock_unavailable although the renderer was interactive. tool_executor runs handlers on a propagated worker thread; thread_context only copied the approval and sudo thread-local callbacks, so the unlock prompt registered by _wire_callbacks was invisible there and can_prompt_here() said nobody could answer. The callback table in thread_context now lists every per-thread prompt (approval, sudo, vault unlock) so a new one cannot silently drop off worker threads again. After the fix the same turn shows the masked card and completes. vault.sources / `hermes vault sources` report a manager as installed when its configured binary_path exists, not only when it is on PATH. |
||
|
|
8e7e9be217 |
feat(vault): sign in with 1Password or Bitwarden logins, unlocked per session
The browser vault now draws from three login sources behind one handle
shape: the local encrypted vault (vault_…), 1Password Login items (op:…)
and Bitwarden Password Manager logins (bw:…). browser_vault_list aggregates
metadata across them; browser_vault_fill routes by prefix and resolves the
password at fill time only, through the manager CLI.
External managers are locked until the user unlocks them for the current
session. The new browser_vault_unlock tool (and the fill path, implicitly)
asks the surface to show a masked master-password prompt — CLI panel
(reuses the sudo panel state), TUI/Desktop via a vault.unlock.request
blocking card. The password goes to `op signin --raw` / `bw unlock --raw`
on stdin, never argv or env; only the session token is kept, in memory,
with a 30-minute idle TTL, cleared on session close or `vault.lock`.
Headless contexts (cron, webhook, api_server, -q) can never prompt: the
manager is reported as locked with unlock=unavailable_in_this_session and
fill refuses — the same posture approvals take where nobody can answer.
Config: vault.onepassword / vault.bitwarden {enabled, binary_path, …};
a 1Password service-account token skips the prompt for headless use.
RPC: vault.sources, vault.source.set, vault.unlock, vault.lock for Settings.
Tests (2, real subprocess against a fake bw; each proven red by sabotage):
headless never prompts or spawns; unlock feeds stdin only, token never
enters os.environ, fill routes by prefix and the password only reaches the
fill script.
|
||
|
|
cde0ad0edd |
feat: agent signs into sites from an encrypted local vault (CLI, browser fill, Desktop Settings)
Consolidated re-apply of #96988 onto current main. Ported from Merit-Systems/OpenInstinct (MIT) opaque-handle autofill design: the model sees vault handles + login metadata, the password is resolved and filled server-side over the supervised CDP socket, and filled values are scrubbed from every browser tool result by an unconditional redaction registry. Rebase adaptations to the Sep-2026 facade/sibling layout: - toolsets: one _HERMES_CORE_TOOLS entry (the browser toolset derives from it) - hermes_cli/main.py: vault parser registered via the subcommand owner table - file_safety: vault/ joins the _READ_DENIED_DIRS credential-dir table - redact: registry scrub runs before the redact_secrets early-return - browser_vault_tool: _run_browser_command now lives in browser_tool_session |
||
|
|
94f77dfa0d |
fix(agent): thread turn_author through conversation_loop.run_conversation
The facade forwarded turn_author= to the loop's public entry point, which did not declare it: every real AIAgent.run_conversation() turn raised TypeError (16 CI failures across provider, sidecar, cron and finite-chat suites). The PR's tests only exercised build_turn_context directly, so the missing hop was invisible. Adds one facade-through-loop test that goes red when the kwarg is dropped. |
||
|
|
ff90eb28ff |
test(bot-mode): trim the author and loop-guard suites to their invariants
Keep one or two behaviour tests per seam (author reset on a cached agent, forged _turn_author refused, guard trips and cools, single charge on the busy path) and drop the parser/setting enumerations. a2a_key goes with them: nothing in this PR reads it; the honcho follow-up that does can bring it back with its consumer. |