Commit Graph

14667 Commits

Author SHA1 Message Date
BearHuddleston 8513c5984c [verified] fix(gateway): fence orphan timers and reconnect interrupt claims
Re-scope #98106 onto the activity-based orphan policy from #100504. Keep timer ownership across callbacks and continuations, serialize reconnect paths against interrupt claims, and avoid recursive eager-resume locking. Leave cleanup polling armed when concurrent cold reuse is rejected.
2026-09-06 14:14:29 +05:30
kshitijk4poor 9a84bee265 refactor(ssh): drop probe-only bookkeeping the probe never reads
- probe_only is consumed inside __init__ only; no instance attribute.
- _sync_manager is assigned only on the probe branch, after the connection is
  up, so a normal SSHEnvironment whose constructor fails still behaves exactly
  as before (its __del__ cleanup does not reach the shared socket teardown).
- _remote_home has no reader on the probe path; not assigned.
- prompt_builder passes probe_only=True unconditionally: which backends honor
  it is the builder table's decision, not a caller-side env_type check.
- Tests trimmed to the probe_only contract: the cleanup-raises case was green
  on main (pre-existing try/except), the socket-name length and double-cleanup
  assertions covered pre-existing behaviour.
2026-09-06 13:32:51 +05:30
Ruichen Zhou 50b54bb18f fix(ssh): isolate prompt backend probes from file sync
The prompt-time backend probe built a normal SSHEnvironment just to run a
one-line `uname`. That constructor detects the remote home, creates the
~/.hermes tree, force-uploads every sync file and snapshots a login session;
when the throwaway object was later garbage-collected, __del__ -> cleanup()
ran sync_back() and `ssh -O exit` against the ControlMaster socket the
agent's real environment shares (keyed by user@host:port).

Add an internal probe_only construction path for SSH: an isolated,
same-length ControlMaster socket (keyed by the instance's session id), no
remote dir setup, no FileSyncManager, no session snapshot. The probe now
tears its own connection down explicitly on success, non-zero exit and
exception, without replacing the probe result when cleanup fails. Normal
SSH callers and non-SSH backends are unchanged.

Salvaged from #77933 onto the facade/sibling layout (the probe body moved to
_run_backend_probe, _create_environment to tools/terminal_tool_backends.py).
2026-09-06 13:32:51 +05:30
Muno459 3f88055035 test(mcp): integration regression for colliding static toolset name via _register_server_tools 2026-09-05 17:00:18 -07:00
Muno 3bc1780f2d fix(toolsets): merge MCP tools when alias collides with static toolset
If an MCP server registers itself under the same name as a built-in
toolset (e.g. an MCP "homeassistant" running alongside the built-in
`homeassistant` toolset), `get_toolset()` returned the static definition
early and the MCP tools were silently shadowed — they got registered
into `mcp-homeassistant` but never surfaced when the agent looked up
`homeassistant`.

Detect the collision via the registry alias (`alias_target` starting
with `mcp-`) and merge the two tool lists. The description is annotated
so the source is visible in `/toolsets info`.
2026-09-05 17:00:18 -07:00
Teknium 4dc9988f78 feat(skills): reddit-reading and rss-feeds bundled skills, multi-platform sweep guidance
Two zero-install research skills plus routing guidance, ported as ideas
(not code) from the Agent Reach skill's per-platform backend routing.

reddit-reading (skills/social-media): subreddit listings, site/subreddit
search, threads with comments, user pages. Live-verified from a datacentre
IP: www .json, api.reddit.com, old.reddit, r.jina.ai and the browser tool
all return 403 / an empty shell / a humanity check; the Atom .rss endpoints
are the only anonymous path and are throttled to ~1 request/min/IP. The
script waits out the x-ratelimit-reset window once and retries, and
switches to the OAuth API (scores, nested comments, ~100 req/min) when
REDDIT_CLIENT_ID/SECRET are present. `doctor` reports the active backend.

rss-feeds (skills/research): RSS 2.0 / RSS 1.0 / Atom / JSON Feed parsing
with UTC-normalised dates, --since/--limit, and feed discovery behind a
page URL (<link rel=alternate>, then well-known paths). Stdlib only; the
optional blogwatcher skill remains the stateful many-feed reader and now
points at this one for one-off reads.

grounded-citations gains a "Multi-Platform Sweeps" section routing
"what are people saying about X" tasks across web, Reddit, feeds, video,
code and X with per-platform attribution and coverage-gap reporting;
competitor-news-monitor references the new sources.

Tests: two invariant tests per skill (format normalisation + discovery;
anonymous 429 handling + OAuth routing/flattening), no network. The
reddit test caught a real bug: the feed footer's "/u/author" leaked into
post bodies.
2026-09-05 14:01:34 -07:00
Teknium 463292351f fix: mid-turn user message no longer waits behind a foreground terminal command
A message typed while the agent runs (CLI busy_input_mode=interrupt, gateway
priority redirect, ACP redirect) goes through AIAgent.redirect(). During tool
execution redirect() degrades to steer(), whose delivery rides the tool result
— so a long foreground command (a `sleep 285` CI poller, a build) parked the
user's message until it exited. The UI printed "Redirected current turn" while
nothing happened for minutes.

redirect() now also asks the tool workers to YIELD (tools/interrupt.request_yield).
The local terminal backend's wait loop honours it: the drain thread is stopped, the
still-running Popen is adopted by the process registry as a notify_on_complete
background session (ProcessRegistry.adopt_local — output so far seeds the buffer,
the registry reader continues from the pipe), and the tool returns immediately with
status "yielded_to_background" + session_id. The command is never killed; the
completion notification arrives as usual and process(poll/wait/log/kill) work on it.

Non-local backends and internal env.execute() consumers pass no yield_handler and
are unaffected; a stale yield bit is cleared with the interrupt bit per worker tid.
2026-09-05 13:51:06 -07:00
kshitijk4poor 9dd6634c56 test(docker): atexit drain joins the worker of an env already popped from the registry; finished workers do not accumulate
Both red on origin/main.
2026-09-05 21:07:14 +05:30
kshitijk4poor b800fdf9a8 test(docker): snap_compat drops only --init and no-new-privileges; key bridged on every config path 2026-09-05 21:00:19 +05:30
kshitijk4poor 431978d47b test(file_sync): backend can reopen the staging tar for writing; POSIX remote keys match on a Windows host (windows_only) 2026-09-05 20:49:29 +05:30
kshitijk4poor 16759c0ec6 test(serve): WS sessions counted at the ASGI boundary for any route; exit only after grace with no client and no running turn; only SSH-isolated backends arm it
Red on origin/main (module absent).
2026-09-05 20:44:51 +05:30
kshitijk4poor 006b1beb00 test(tools): dead environment surfaces as unavailable across read_file/read_file_raw/read_file_bytes/search; healthy env still reports missing
Trimmed from the contributor's suite to two parametrized contracts. Red on origin/main (6 cases).
2026-09-05 19:06:57 +05:30
kshitijk4poor 370ad21dea fix(gateway): the skipped-MEDIA warning says whether the file is missing or denied (#100074)
'Skipping unsafe MEDIA directive path' fired for every non-existent file too, sending
operators down a security-policy rabbit hole for what was a hallucinated or untranslated
path. The warning now names the reason: 'not found on this host' vs 'denied by the
delivery policy'.
2026-09-05 19:01:22 +05:30
kshitijk4poor 60ca93cebd fix(doctor): probe the Docker daemon with 'docker version', not 'docker info' (#72927)
docker info needs the /info endpoint, which socket proxies such as tecnativa block by
default, so doctor reported 'docker daemon not running' against a fully working
DOCKER_HOST. docker version hits /version and is what the backend itself probes with.
2026-09-05 19:01:22 +05:30
kshitijk4poor 337113c1b8 fix(agent): a parent directory the process cannot stat is not a crash while locating .git (#8751)
_find_git_root walked cwd's parents with Path.exists(); on hosts where a parent is
mode 700 for another user that raised PermissionError out of prompt construction.
Treat an unreadable ancestor as 'no .git here'.
2026-09-05 19:01:22 +05:30
kshitijk4poor bb504a2e14 fix(terminal): every Windows drive letter is a host cwd, not just C: (#60962)
_HOST_CWD_PREFIXES only listed C:\ and C:/, so a D:\ or E:/ working directory on a
Windows host reached docker run -w and the container started in a directory that does
not exist. The two prefix scans now go through one _is_host_cwd predicate that matches
any drive letter with either slash.
2026-09-05 19:01:22 +05:30
kshitijk4poor 7b8cf4c631 test(code-execution): a changed tool set spawns a fresh remote kernel with fresh stubs
Red on origin/main.
2026-09-05 18:46:53 +05:30
kshitijk4poor ef89a7f1c9 test(tools): every truncation footer renders the backend-visible cache path under docker; local keeps the host path
Red on origin/main.
2026-09-05 18:46:16 +05:30
kshitijk4poor deaebd9b17 test(tools): probe and execute_code hand _create_environment the same container_config as the terminal tool
Replaces the source-reading AST check with a behaviour contract. Red on origin/main.
2026-09-05 18:32:44 +05:30
kshitijk4poor d72cbbcf5a test(docker): lockdown + operator --network none emits one flag; contradictory network fails closed
Trimmed from the contributor's five tests to the two contracts. Red on origin/main.
2026-09-05 18:19:58 +05:30
kshitijk4poor 9e23028e2e test(approval): deny rule blocks in every isolated backend under yolo/off; non-denied commands still skip
Red on origin/main (10 parametrized cases).
2026-09-05 18:08:00 +05:30
kshitijk4poor c957432685 test(security): corrupt-token and docker_env warnings never echo the value
Red on origin/main.
2026-09-05 18:05:15 +05:30
kshitijk4poor bde6f6dce2 test(docker): reuse probe never emits the Docker-only {{.Label}} template; egress-off posture is a label filter
Red on origin/main.
2026-09-05 18:04:52 +05:30
rainbowgits 179e988493 fix(terminal): JSON-serialize list/dict values in profile terminal scope 2026-09-05 18:04:00 +05:30
kshitijk4poor 5035fbde1c test(tools): plugin container backend gets the host-cwd guard on the file-tools path; non-container plugin keeps it
Red on origin/main, green with the fix.
2026-09-05 18:03:05 +05:30
RelaxJonh c851ef4d0f fix(tools): file-tools env creation uses _is_container_backend() for the cwd guard (#101013)
_create_terminal_env_for_file_ops tested the static _CONTAINER_BACKENDS set while the
terminal tool's sibling sites already go through _is_container_backend(), which also
honours plugin backends declaring is_container. A host cwd override on such a plugin
backend therefore reached the sandbox as its workdir from the file-tools path only.

Salvaged from #101389 by @RelaxJonh.
2026-09-05 18:03:05 +05:30
kshitijk4poor 52cf39c908 refactor(auth): reuse _CLEAR_STATUS and the merge's None short-circuit in the reset path
Behaviour-preserving cleanup of the salvaged fix:

- reset_statuses goes back to replace(e, **_CLEAR_STATUS, ...) instead of
  re-spelling the six status fields; every other clear site uses the
  constant, so a seventh field would otherwise drift here. The
  count/new_entries/cleared_ids trio collapses to the original stale-list
  shape, and entry.failure_reason is read through the dataclass's
  __getattr__ like the rest of the module.
- Both bypass sites (write_credential_pool, _update_root_pool_rows) pass
  disk_entry=None to _merge_disk_cooldown_state for a cleared id instead of
  adding a parallel branch; the helper already returns the entry unchanged
  for a missing disk row.
- The cleared-id set in _update_root_pool_rows is built once, not per disk
  row (an Iterable argument would also have been drained on the first row).
- persist_pool_entries passes status_cleared_ids plainly; the kwargs splat
  existed only for one test fake, which now accepts the kwarg.
- Docstrings trimmed to the WHY.

Same 116 targeted tests green; reverting the two source files to main still
fails the two regression tests and passes the guard test.
2026-09-05 17:43:45 +05:30
rodrigogs 8378551311 fix(auth): make hermes auth reset actually clear a binding cooldown
Port onto the split layout: persist_pool_entries now forwards
status_cleared_ids to BOTH writers — write_credential_pool (hermes_cli/
auth.py) and the root-store row merge for single-use-refresh providers
(_update_root_pool_rows), which the original fix predates. Both skip the
disk recency merge for deliberately-cleared entries; the kwarg is only
sent when non-empty so upstream fakes with the old signature keep
working. reset_statuses clears failure_reason (lives in extra, replace()
cannot reach it) alongside the status fields.

tests/agent/test_credential_pool.py: 62 passed; refresh-race suite: 3
passed (mutation: the reset/binding tests fail with the fix stashed).
The dashboard-auth-gate / user-providers failures are pre-existing on
pristine upstream/main (reproduced with changes stashed).
2026-09-05 17:43:45 +05:30
Bergmann89 2e24e06e55 fix(env): scope terminal config re-bridge to the true process profile
In a multiplex dashboard (one process serving every profile on the host),
a secondary profile's terminal.backend could leak into the shared
os.environ and silently override the launch profile's terminal backend.
A profile configured for terminal.backend: ssh would find itself running
every command locally because a sibling profile with backend: local had
polluted the shared environment.

The tell is an asymmetric env state on the launch session:

    TERMINAL_ENV=local            # leaked from a sibling profile
    TERMINAL_SSH_HOST=10.10.0.103 # still the launch profile's, untouched

A restart does not help: as soon as the sibling profile is touched again
(a turn, a cron tick), the shared env is re-poisoned.

Root cause: env_loader._reapply_terminal_config_bridge() guards "only
re-bridge config into the shared os.environ when this load is for the
process's own profile" via _process_hermes_home(). That helper delegated
to get_hermes_home(), which follows the context-local
set_hermes_home_override a per-request task installs. When a per-turn
handler is scoped to a secondary profile and triggers a dotenv reload,
both sides of the comparison resolve to that secondary profile, the guard
passes, and the bridge writes the wrong profile's terminal.backend into
the shared environment. Because the secondary profile's config carries no
TERMINAL_SSH_* keys, only TERMINAL_ENV is overwritten, producing the
asymmetric state above.

The helper was introduced for the config-cache key (where following the
active home is correct) and later reused for the terminal bridge guard,
where the override-following semantics are wrong.

Fix: delegate _process_hermes_home() to get_process_hermes_home(), the
override-immune resolver built for exactly this. It reflects the scope the
process was launched under, ignoring per-task overrides. Both call sites
(the bridge guard and the secrets-cache reuse check) want the true process
home. Single-profile / CLI behaviour is unchanged: with no active
override the two resolvers are identical.

Complements the terminal_tool._active_environments session-key fix: that
scopes the per-session environment cache; this keeps the shared os.environ
the cache reads TERMINAL_ENV from uncontaminated in the first place.

Adds tests/hermes_cli/test_terminal_bridge_profile_scope.py covering both
the override-immune resolver and the no-leak reload behaviour.
2026-09-05 17:01:23 +05:30
kshitijk4poor 377118af86 test(cli): assert the short-terminal branch drops description rows entirely
The parametrized heights 5 and 8 exercise the renderer's zero-budget
branch (description too large to fit any row beside the reserved choice
rows), but the test never asserted what that branch draws. Pin it: no
description content and no omission notice, while the choices and key
hint stay visible.

Follow-up polish on #103350 by helix4u.
2026-09-05 16:45:59 +05:30
Gille 8f9d300ab9 fix(cli): keep radio choices visible below long descriptions 2026-09-05 16:45:59 +05:30
kshitijk4poor 07b161e241 refactor(context): fold simplify findings into oversized-file fallback
- Check text tokens BEFORE building the fenced block: an oversized file no
  longer allocates a second MB-scale string just to discard it, and the token
  estimate runs once instead of twice.
- Drop the companion warning: the fallback block alone carries the message,
  so the model no longer reads "too large to inline safely" twice (Context
  Warnings + Attached Context).
- Extract _on_disk_reference_block + _agent_visible_path shared by the binary
  and oversized blocks (stat/format_bytes/OSError fallback, container path
  translation, nudge text were copy-pasted).
2026-09-05 16:44:23 +05:30
Gille cfa1a90b8c fix(context): fall back for oversized file refs 2026-09-05 16:44:23 +05:30
杨瑾 d347fcaf97 test(desktop): cover live token in SPA index 2026-09-05 16:30:35 +05:30
thinkingsheep 1866921d33 fix(desktop): serve the current SSH session token 2026-09-05 16:30:35 +05:30
kshitijk4poor b499ab11fe fix(gateway): remote media fetch fails closed on unresolvable symlinks, skips strict mode, keys the sandbox by session id
readlink returning nothing no longer falls back to the unresolved name (a sandbox symlink to a
credential would otherwise be followed by [ -f ]). Strict delivery mode keeps its recency gate:
no fetch, since the copy lands in an allowlisted root. The active sandbox is looked up by the
HERMES_SESSION_ID the turn registered it under (per-session-isolated docker keys by task id, not
session key). MEDIA path normalisation (quotes/backticks/trailing punctuation) now matches the
host path; the denylist tables are built once at import.
2026-09-05 16:09:46 +05:30
kshitijk4poor 90db26ad55 test(gateway): sandbox MEDIA artifacts deliver; credentials and symlinks to them never leave the sandbox; fetch transport round-trips and caps size
Red on origin/main (module absent / paths dropped), green with the fix.
2026-09-05 16:09:46 +05:30
kshitijk4poor baf7ceeace refactor(environments): share the unset-prelude and client-env helpers; drop test-only shape from ssh
prepend_unset / client_env_with live beside resolve_passthrough_env so docker and ssh
post-process its output identically. The ssh tests build a real SSHEnvironment under
the existing connection stub instead of a half-initialised __new__ object, so the
production code no longer carries getattr fallbacks for it.
2026-09-05 15:54:39 +05:30
kshitijk4poor be6f0d296b test(ssh): passthrough goes out via SendEnv, values never in remote argv, provider keys held back
Red on origin/main (no SendEnv, no client env), green with the fix.
2026-09-05 15:54:39 +05:30
sgaofen 7456081d2a fix(ssh): forward skill/config env passthrough over SendEnv (#14091)
The SSH backend spawned `ssh … bash -c` with no env forwarding at all, so a
skill's `required_environment_variables` (and `terminal.env_passthrough`)
reached local and docker shells but never a remote host — the skill reported
ready while every remote command saw the variables unset.

Names travel as `-o SendEnv=NAME`; values ride the ssh client's own
environment, so no secret ever enters the remote `bash -c` argv. Hermes
provider credentials stay behind the same blocklist docker uses, `.env`
fills in variables unset in the shell, and profile-scoped names missing
from the active scope are unset remotely under multiplex. The remote sshd
must `AcceptEnv` the names (documented).

A ControlMaster only relays the SendEnv names it was started with, so
passthrough commands ride a per-name-set master socket (verified live
against a real sshd: the shared master silently dropped every variable).

Salvaged from #14414 by @sgaofen.
2026-09-05 15:54:39 +05:30
kshitijk4poor 03e1a9bbdf test(gateway): session stores + sidecars + named-board kanban.db are denied; neighbouring attachments still deliver
Red on origin/main (all nine paths delivered), green with the fix.
2026-09-05 15:54:34 +05:30
kshitijk4poor 9835b2fd69 chore: map contributor email for salvaged #41071 (pprism13) 2026-09-05 15:54:34 +05:30
kshitijk4poor 0af92098f2 test(agent): split the Kimi lookalike-host case into its own test 2026-09-05 15:54:14 +05:30
kshitijk4poor 562383ad31 test(agent): Kimi Code fallback lands on anthropic_messages; lookalike host stays on chat_completions
Red on origin/main (chat_completions), green with the fix.
2026-09-05 15:54:14 +05:30
Teknium d20a8e4475 feat(models): OpenRouter, Nous Portal, and Alibaba Token Plan pickers carry Qwen3.8-Max-0902
Alibaba shipped qwen3.8-max-0902 (alias qwen3.8-max-2026-09-02), the upgraded
snapshot of Qwen3.8-Max: 1M context / 131K output, $2 in / $6 out. Both
aggregators now serve it and dropped the bare slug from their catalogs
(verified 2026-09-02: OpenRouter /v1/models + a 200 completion echoing the
slug; Nous Portal /v1/models).

- OPENROUTER_MODELS (feeds the nous list too): qwen/qwen3.8-max -> qwen/qwen3.8-max-0902
- _ALIBABA_TOKEN_PLAN_MODELS: qwen3.8-max-preview -> qwen3.8-max-0902
- test_empty_model_fallback fixture follows the nous catalog slug
- model-catalog.json regenerated

No new metadata: DEFAULT_CONTEXT_LENGTHS substring-matches qwen3.8-max (1M),
reasoning floor prefix fires (180s), both aggregator routes bill via
official_models_api. alibaba/alibaba-cn/opencode-go/setup.py left unchanged
(out of scope).
2026-09-05 02:31:06 -07:00
Teknium 41e6b60249 test(update): fold the #97332 plan-kind cases into one invariant test 2026-09-05 00:24:40 -07:00
Teknium 1cce7c6dd8 fix(update): unmapped gateway stops no longer fail the fleet check with "no rows"
A gateway with no profile mapping (or one whose relaunch could not be armed) is
SIGTERMed and listed under "Restart manually" — by design it has no successor and
publishes no fleet-matrix row. It still counted in ``killed_pids`` and the
pre-restart snapshot, so ``_fleet_probe_expected_runtimes`` demanded rows that
could not exist and a fully successful update exited 1 with "Fleet version check
returned no rows even though gateway runtimes were expected", leaving the
fleet_restart_pending marker behind and every later CLI start warning about it.

Track the unmapped stops on the restart outcome and subtract them from the
row-predicting signals (``fleet_probe_signals``); relaunched/systemd gateways
still predict rows exactly as before.
2026-09-05 00:24:40 -07:00
liuhao1024 b8e3c5c700 fix(update): scope fleet-probe runtime expectation to gateway-kind plan records
A dashboard-only runtime plan (gateway never started) made
_fleet_probe_expected_runtimes() return True from the unfiltered
'plan.runtimes is non-empty' check. collect_fleet_versions() reports
gateway identities only, so the probe waited for rows that cannot exist,
printed the incomplete-verification warning, and exited 1 after a
successful update (#97332).

Key the plan-derived expectation on kind == 'gateway' records — the same
row-capability rule already applied to the Windows resume token (#93406)
— and update the two tests that pinned the old object() placeholder so
they pin the runtime-kind distinction. Restart-phase, killed-PID, and
pre-restart-PID signals still fail closed unchanged.
2026-09-05 00:24:40 -07:00
Teknium e17dd8b428 fix(update): protect the whole hermes_cli.update_* family from the stale-module purge
Widen the salvaged receipt protection to every updater-owned module: the
pre-update plan's ``RuntimeRecord`` dataclass (``update_inventory``) has the same
failure — the plan is built before the purge and the post-restart reconciliation
filters with ``isinstance``, so an evicted module means a fresh class, zero matches,
and an empty runtime-outcome report. One prefix rule instead of a name list that
has to be re-fixed per symptom.
2026-09-05 00:24:40 -07:00
WolveBrown fada877519 fix(update): preserve active receipt across module purge 2026-09-05 00:24:40 -07:00