Commit Graph

24 Commits

Author SHA1 Message Date
John Paul Soliva
9ad7156324 fix(dashboard): cron delivery targets, blueprints, recommended default, voice config, official skills and debug share answer an unknown ?profile= with 404
Each route's catch-all or fallback swallowed the profile scope's 404: the
GETs answered 200 with default or empty data (local-only targets, static
deliver options, an empty model, relay voice mode, no installed marks) and
POST /api/ops/debug-share answered 500. HTTPException now passes through
ahead of each catch-all, as in the rest of this change; for the hub skills
catalog the pass-through sits in _installed_hub_identifiers, so the search
and sources routes that share it keep their existing 404.

The unknown-profile test gains a row per GET route; the endpoint list in
web-dashboard.md names them.
2026-09-28 12:18:58 -07:00
beardthelion
9bdecbbf57 fix(dashboard): keep -p default in hub-action argv so actions hit the default profile
_profile_cli_args folded "default" into the no-override early return
alongside "current", so a hub action scoped to the default profile
spawned selector-less argv. The child hermes process then resolved the
ambient HERMES_HOME (the launch profile under a pooled -p X serve, or
the sticky active_profile) and installed/toggled into the wrong
profile; it also skipped the named-profile credential scrub.

"default" is a real named target everywhere else (profile list,
_resolve_profile_dir, -p default already emitted for multiplexed
restarts), so only "" and "current" now mean ambient.
2026-09-28 12:18:58 -07:00
beardthelion
2c3a75beaa fix(secret-scope): scoped misses fail closed under a foreign-home scope
get_secret returned os.environ on a scoped miss whenever multiplex was
off, but non-multiplex hosts serve foreign homes too (dashboard/desktop
backend, per-profile cron, MCP owner scopes, kanban spawn-env builds),
where os.environ is the launch profile's. Bound scopes now carry the
home they were built for; serves_routed_profile detects a foreign scope
even when the binder deliberately skips the HERMES_HOME override, and
the miss returns the caller's default. Every production binder stamps
its home; own-home scopes keep the deliberate env overlay.
2026-09-23 06:46:02 -07:00
teknium1
2e497bead8 fix(dashboard): close the indirection holes and the unnamed-profile 400s
Round 2 of the REST profile-scope pass. A call-graph audit (AST over every
router plus their imported helpers, following functools.partial bindings,
closures and callbacks handed to to_thread/_spawn_job) found the writes a
signature grep cannot see, and the SPA/Desktop callers that never named a
profile at all.

Handlers that reached a config write through indirection:
* PUT /api/dashboard/plugin-providers wrote memory.provider + context.engine
  through functools.partial(_write_config_value, ...) — the SAME key
  PUT /api/memory/provider scopes — into the launch profile's config.yaml.
* POST /api/local-models/quickstart is `activate` plus a download; only
  `activate` had been scoped, so _set_runtime_enabled/_assign_default landed
  in the launch profile.
* POST /api/local-models/runtime/install regenerates launch presets under
  get_hermes_home().
* GET /api/model/recommended-default lazily PERSISTS discovered custom-provider
  models via build_models_payload -> _save_discovered_models_to_config.

Policy gaps:
* POST /api/ops/hooks is now gated like DELETE: writing an arbitrary command
  into `hooks:` and, with approve, into the consent allowlist is strictly more
  privileged than removing one.
* POST /api/sessions/prune (non-dry-run), DELETE /api/sessions/empty and
  POST /api/sessions/bulk-delete join the same destructive class.
* PUT /api/memory/provider ran its readiness check OUTSIDE the scope, so it
  judged the launch profile and could write a broken setting into another.

Callers:
* /api/credentials was missing from the SPA's PROFILE_SCOPED_PREFIXES, so the
  credential-pool delete button 400'd unconditionally; so were
  /api/dashboard/plugin-providers, /api/local-models and the model route above.
* The management scope was empty until the switcher resolved, so every
  destructive route 400'd in working UI on any host with a second profile
  directory. The backend now injects the profile it itself serves
  (__HERMES_DASHBOARD_PROFILE__) — and only when that name provably resolves
  back to its own home, so the fallback can never retarget another profile.
* authedFetch went around the scope entirely; the Desktop /api/ops callers
  (doctor, security-audit, backup, debug-share) sent no profile while Electron
  pins that family to the shared primary backend.
2026-09-21 09:59:11 -07:00
teknium1
363b8a6fcb fix(desktop): custom endpoints pin an API mode and keep /v1/models alias metadata
Settings > Custom Endpoints assumed Chat Completions: the form, its types, the
update payload and _write_custom_endpoint carried no api_mode, so a
Responses-only (or Anthropic-compatible) host validated fine on /models and
then 404'd on every POST /chat/completions. Validation also flattened each
/v1/models row to a bare id, so a reasoning alias like gpt-5.6-sol-high
(canonical_model + reasoning_effort) was saved as a literal upstream model.

- Desktop form: API Mode segmented control (Auto-detect / Chat Completions /
  Responses API / Anthropic Messages — the same set `hermes model` offers);
  threaded through toPayload, hydrated from GET read-back.
- CustomEndpointUpdate.api_mode (Literal) persisted as providers.<id>.api_mode,
  the key the CLI writes and the runtime reads; None (older UI) leaves a
  hand-written mode alone, "" clears it. GET rows report api_mode.
- validate returns model_details (id / canonical_model / reasoning_effort)
  next to the unchanged string[] models; _parse_model_ids is now a projection
  of _parse_model_entries.
- Save keeps the alias metadata in providers.<id>.models and, when the picked
  default is an alias, persists the canonical model and pins its effort under
  agent.reasoning_overrides (the resolve_reasoning_config chokepoint).

Fixes #93622
Supersedes #69824 (@SacrEllfarch), #82148 (@JackLee992), #93693 (@fangliquanflq)
2026-09-19 00:17:19 -07:00
teknium1
4590ef8b58 fix(profiles): polled profile lists never walk skill trees; vanished skill dirs no longer abort enumeration
GET /api/profiles, the profiles.list RPC and the /api/profiles/projects/tree
fan-out are polled by the Desktop every few seconds (roster tick, focus,
gateway-open). Each call ran list_profiles() -> _count_skills() ->
Path.rglob("SKILL.md") over EVERY profile once the 30 s TTL expired: ~4 fs
calls per skill, 2.4-4.7 s per walk on 7-84 profile installs, ~half a core
at idle, and on Linux enough to starve the renderer's 60 s API timeout. A
skill dir removed mid-walk raised FileNotFoundError out of rglob and aborted
the whole profile list.

- list_profiles(lazy_skill_count=True): skill_count is the last known value;
  a missing/aged entry schedules ONE background _count_skills per profile per
  60 s recheck window, so the request thread does zero skill-tree I/O and the
  refresh cadence is decoupled from the poll rate. The two polled callers and
  the REST fallback entry use it; the sync default (CLI, detail views) is
  unchanged.
- _walk_skill_count: the repo walker (agent.skill_utils.iter_skill_index_files,
  os.walk with excluded/support dirs pruned) instead of rglob — half the
  fs calls and best-effort on subtrees that vanish mid-walk. profiles.describe
  uses the same walker.
- _profile_targets always uses profiles_to_serve (pure directory read):
  projects/tree and sessions/pull-requests only ever consumed name/path.
- Skill-count TTL 30 s -> 600 s (signature invalidation still catches
  skill add/remove immediately on the next refresh).

Live repro (5 profiles x 200 SKILL.md, temp HERMES_HOME, py3.11):
  before: GET /api/profiles 2175 stat + 2015 scandir per cold call,
          profiles.list 2189 + 2015, projects/tree 2177 + 2015;
          a skill dir removed mid-walk -> FileNotFoundError from list_profiles()
  after:  GET /api/profiles 0 skill-tree stats/scandir on the request thread
          (counts land from the background refresh by the next poll),
          profiles.list 0, projects/tree 0; _count_skills (detail/control)
          still reports 200 with 203 scandir; the vanished-dir case returns
          199 and list_profiles() enumerates all 5 profiles.

Fixes #114041
Co-authored-by: KoNit-K <konit.block@protonmail.com>
2026-09-18 10:18:40 -07:00
xxxigm
9c068fb3dc fix(dashboard): honor Podman in the Docker backend health probe
Desktop's preflight only looked for a `docker` binary, so podman-only
machines got "Terminal commands are unavailable" even when the agent
was already running containers via HERMES_DOCKER_BINARY or podman.
2026-09-18 21:47:13 +05:30
kokhlo
2af437b4fe fix(desktop): label Settings "Applies to" chips with the bot title / display name
The shared profile selector on the config-backed settings pages rendered the
canonical slug, so a renamed default profile or a titled Bot read as its
machine id while every sibling surface (sidebar switcher, profiles page, Bots
roster) showed the chosen identity.

Two coupled causes:

- the chip callsite read profile.name directly, never resolving through the
  display-name helper the other surfaces use;
- the REST /api/profiles row never carried the Bot Mode title
  (profile.yaml ui_meta['hermes-bots'].title), so no Settings-side fix could
  reach the name the roster shows.

The REST row now exposes bot_title alongside display_name (same precedence the
Bots roster resolves: bot title -> display_name -> canonical slug), and the
chip labels through it. Canonical name remains the identity everywhere —
selection, routing, and the override store keep keyed on the slug.
2026-09-16 16:44:53 -07:00
teknium1
8609743389 fix(serve): launch-profile scope decided at entry; send keeps scope authority; per-reset release
Three edges of the fail-closed multi-profile host (#111620 review, andrexibiza P1 + P2,
kvnloo finding 1):

- `send` under a routed profile's scope `update()`d the installed scope from raw `.env`,
  reversing build_profile_secret_scope's precedence (user .env, then external secret
  sources) for the rest of the request; a stale user value beat the secret-manager one.
  The installed scope is authoritative as-is; only the config.yaml setdefault bridge runs.
- The launch profile's body was scoped only when `is_multiplex_active()` was already true
  at entry, while get_secret consults that global on every read. A launch RPC / dashboard
  request entering single-profile and resuming after a concurrent first `?profile=B`
  activation raised UnscopedSecretError mid-request. The launch profile's secret scope
  (its .env + external sources over the launch env: live while single-profile, the frozen
  snapshot once multiplexing is active) is now bound for every launch-profile body, so the
  credential source is fixed at entry. The terminal policy overlay stays multiplex-only
  (standalone terminal execution keeps its os.environ bridge). _publish_env_value mirrors a
  same-request .env write into that scope AND os.environ for the launch profile, only into
  the scope for a routed one (serves_routed_profile).
- _release_profile_runtime_scope_tokens reset terminal → secret → home in sequence under
  one outer suppress; a failing terminal reset left the previous profile's secrets and
  HERMES_HOME installed for the next body in that context. Each reset is now independent;
  the first failure is re-raised after every scope is released.

tests/tui_gateway/test_multi_profile_hosting_transitions.py: manager-vs-dotenv precedence
through _load_hermes_env, TUI-RPC and dashboard barrier tests (launch enters single-profile,
B activates on another thread, launch resumes and still resolves its injected credential,
never B's), forced terminal-reset failure still releases secret + home. 4/4 red on base.
2026-09-16 00:35:00 -07:00
teknium1
5ca670b398 fix(dashboard): profile-routed routers run under the profile's secret scope; console send never writes the process env
_config_profile_scope bound only HERMES_HOME, so GET /api/config?profile=B
expanded B's `${VAR}` refs to the dashboard (DEFAULT) profile's plaintext
credentials, WS /api/console commands for B saw the default's keys wherever B
lacked one, and the audio speak-stream synthesis thread re-resolved the TTS key
unscoped. Console `send` for B went further: send_cmd._load_hermes_env copied
B's .env into the shared os.environ with override=True, so every later
default-profile read saw B's tokens.

- web_server_profiles._config_profile_scope binds home + hydrated secret scope
  for a named profile and flips the process to fail-closed multi-profile
  hosting (same activation as the tui_gateway); the dashboard's own profile then
  runs under its frozen-launch-env scope. A single-profile dashboard stays
  unscoped (systemd / op-run credential injection keeps working).
- GET /api/tools/toolsets computes the per-toolset "configured" flag inside
  the profile scope (it was read after the scope closed).
- web_routers/mcp._profile_secret_scope now just delegates (no second copy of
  the composition); audio `_produce` runs the whole synthesis body under the
  requesting profile's scope, not only the resolve step.
- send_cmd._load_hermes_env targets the installed secret scope when one is
  active (gateway.config._getenv reads the scope first), os.environ only for
  the standalone CLI.
2026-09-15 03:46:29 -07:00
Teknium
d0c90039a7 fix(messaging): keep profile status truthful without credential inheritance
Preserve the two contributor fixes, slim them to two behavioral invariants, and enter explicitly requested homes even inside a nested scope. Real native remote Desktop changes Disabled to gateway_stopped for default and named profiles; direct API controls preserve explicit disable and empty-profile isolation. Unit A/B and regression suites remain queued under the shared campaign lock.
2026-09-07 05:56:33 -07:00
vectorcontext
e24f07239f fix(dashboard): reclassify profile=default as current when it resolves to the process home
The desktop app always sends profile=default on GET /api/messaging/platforms.
_is_current_profile() recognized only None/""/"current" as the dashboard's
own profile — NOT the string "default" — so a single-profile install (the
standard `hermes gateway setup` flow: token in .env, no platforms: section in
config.yaml) entered the profile-scoped branch of _config_profile_scope().
That branch derives platform enablement from config.yaml only and never calls
load_gateway_config()'s env-override pass (which enables the platform when the
token is in the environment). Result: a platform connected via .env reported
enabled=false, state="disabled" while it was actually running. The unscoped
GET (no profile param) correctly reported enabled=true, state="connected".

Fix: classify by resolved path, not by string. After _is_current_profile()
fails, _config_profile_scope() now resolves the requested profile dir and
compares it against get_process_hermes_home().resolve() — the same comparison
_is_other_profile() already uses. When they match (profile=default on a
default-home process), yield None (no override), taking the unscoped path that
calls load_gateway_config(). A named-profile process (`-p worker`) has a
different HERMES_HOME, so its profile=default resolves to a different directory
and still scopes correctly — cross-profile secret isolation is preserved.

The scoped branch's config.yaml-only enablement is DELIBERATE:
load_gateway_config()'s env pass reads os.environ and would leak the root
install's tokens into a genuinely different profile's state. This fix only
reclassifies requests that name the process's OWN home; it does not touch the
scoped branch or gateway/config_env.py.

Refs #104614
2026-09-07 05:56:33 -07:00
Teknium
5f1feb5344 simplify(compat): web_server — drop 221 re-exports (config/status/shutil/run_in_threadpool, lifecycle, 13 web_server_<concern> blocks, 47 route-handler legacy re-exports); web_deps.late()/LateState() take an owning-module arg; concern modules import each other directly (62 lazy sites) 2026-09-03 14:21:32 -07:00
Teknium
7a33369e81 simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
2026-09-03 14:00:59 -07:00
Teknium
c93ace77c2 simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files 2026-09-03 14:00:17 -07:00
Teknium
14791b4d4e simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
2026-09-03 13:49:57 -07:00
Teknium
e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium
a29a970054 refactor(web-profiles/status): drop dead sidebar cache_clear hook, compact literals and ladders 2026-09-02 22:28:11 -07:00
Teknium
ba1df7dab1 refactor(web-profiles/status): fold db-open into read helper, data-drive sidebar slices, compact docstrings 2026-09-02 21:49:33 -07:00
Teknium
7e04ebda8e refactor(web-profiles/status): AST-neutral bracket packing/hugging 2026-09-02 21:17:14 -07:00
Teknium
976b008611 refactor(web-profiles/status): AST-neutral bracket hugging 2026-09-02 21:14:17 -07:00
Teknium
48751f0916 refactor(web-profiles/status): shared home scope, DB-read helper, get_status phase helpers, dedupe fallbacks 2026-09-02 21:11:59 -07:00
Teknium
8b34d23366 refactor(web_server): move skills-hub action helpers to web_server_profiles; trim fastapi import block to what web_server still uses 2026-09-02 16:19:10 -07:00
Teknium
755e83fb5c refactor(web_server): extract files policy, MCP, and profile-scope/catalog helpers to web_server_{files,mcp,profiles} 2026-09-02 16:02:07 -07:00