Commit Graph

22221 Commits

Author SHA1 Message Date
Teknium e0bb71cb73 fix: track secret source registration origin 2026-08-12 16:25:10 -07:00
Bartok9 2e29de2296 fix(plugins): delegate secret-source enablement to is_enabled contract (#64177)
Address teknium1 review on #64189:
- Re-pull gate now delegates to each source's is_enabled(cfg) via the
  registry contract, so a plugin source with custom activation logic is
  honored (previously only secrets.<name>.enabled was checked).
- Add BUILTIN_SOURCE_NAMES to the registry so plugin-vs-bundled is a
  single source of truth instead of a hard-coded set at the call site.
- Reconcile docs: rewrite the timing :::note to describe both the
  post-discovery re-pull and the remaining import-time limitation, and
  cross-link the first-process bootstrap section.
- Tests: real SecretSource subclasses, custom is_enabled activation
  (positive + negative), is_enabled-raises skip, builtin-only no-op,
  and a discovery-registration end-to-end re-pull check.
2026-08-12 16:25:10 -07:00
Bartok9 7a7e73d310 fix(plugins): re-pull plugin secret sources after discovery (#64177)
After plugins register SecretSource backends, reset the env-loader cache
and re-run load_hermes_dotenv when an enabled plugin secret source is
configured. Closes the first-process bootstrap gap where import-time env
load stale-outs plugin vaults (tommck / Community ask). Fail-open, no-op
without plugin sources.

Docs: first-process bootstrap timing on secret-source plugin guide.
Tests: unit coverage for noop / enabled re-pull / discover hook.
Part of #64182 plugin-interface expansion.
2026-08-12 16:25:10 -07:00
hans 299996f7e0 docs(rfcs): plugin-architecture research spike — Pi and OpenCode lessons
Code-level analysis of Pi (earendil-works/pi @ eb79351) and OpenCode
(anomalyco/opencode @ c69abee) plugin architectures across the six
dimensions #64180 specifies, with a 13-row adopt/adapt/avoid table
mapped to #64164/#64161/#64162/#64165/#64229/#64230. Key findings:
neither system has hook timeouts (both shipped hang-class bugs),
OpenCode's permission.ask is typed-but-dead (hook wire-up drift),
Pi treats prompt-cache stability as API contract, and both systems
lack ADRs. Fixes #64180.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 16:25:01 -07:00
teknium1 2a7ed25db8 feat(skills): cover all Hermes browser pathways in har-derived-api-client
Adds scripts/har_capture_cdp.py for browsers reached over CDP -- cloud
backends (Browserbase, Browser-Use, Firecrawl), Camofox-with-CDP, and any
/browser connect endpoint. record_har_path only works on a locally-owned
Playwright context, so the CDP capturer attaches via connect_over_cdp() and
assembles the HAR from page request/response events instead, leaving the
attached browser open (it doesn't own it).

- SKILL.md: pathway->capturer routing table, CDP prerequisites, pitfalls for
  wrong-capturer/empty-HAR, headless-UA weakness, and no-close-on-attach
- Validated live: attached to an external CDP Chrome, drove DuckDuckGo
  autocomplete, derived the /ac/ endpoint, replayed it browserless
- tests: assert CDP capturer attaches (not launches) and that the skill
  documents every browser backend
2026-08-12 16:11:51 -07:00
teknium1 c558e3520a feat(skills): add har-derived-api-client optional skill
Record a site's XHR into a HAR with Playwright, derive its private JSON API,
and call it directly over plain HTTP instead of browser-controlling the page
every time. Credit: trick by Jared Longster, popularized by Dax (thdxr).

- scripts/har_capture.py: Playwright HAR recorder with scripted --action steps
  and embedded response bodies
- scripts/har_to_client.py: distills the HAR to endpoints (method/path template
  /params/body/response) plus User-Agent+cookie+auth replay hints
- Validated live: derived + replayed the Algolia HN-search POST API and the
  Wikipedia rest.php search-title GET, both browserless
- tests exercise the real derivation logic on a synthetic HAR fixture

optional-skills placement: heavy Playwright dependency, niche use case.
2026-08-12 16:11:51 -07:00
MagMueller 49ac259215 Disable Browser Use telemetry by default 2026-08-12 16:10:25 -07:00
Teknium 7dad8f6a51 docs(github-auth): document headless gh auth login --with-token hang + hosts.yml fallback
On keyring-less headless Linux (VPS, containers, no dbus session),
'gh auth login --with-token' can block indefinitely waiting on a
secret-service keyring -- even with --insecure-storage, with no output.
Hit live on a headless x86_64 VPS (gh 2.97.0): the documented device
flow succeeded up to the token, then --with-token hung twice.

- Add a timeout guard to the device-flow polling loop so the hang is
  detected instead of silently stalling the login.
- Document the proven fallback: write ~/.config/gh/hosts.yml directly
  (chmod 600) and run 'gh auth setup-git' -- both read the file store
  without touching the keyring.
2026-08-12 16:08:36 -07:00
brooklyn! d2c6af3aa2
Merge pull request #84852 from NousResearch/bb/atomic-replace-windows-sharing
fix(utils): recover Windows renames contended by an open handle
2026-08-12 17:31:30 -05:00
Hermes Agent f514d546be test(utils): cover contended Windows renames on the Windows CI lane
The real-handle tests use @pytest.mark.windows_only rather than a bare
pytest.skip(os.name != 'nt'). scripts/ci/list_os_marked_tests.py greps for
the marker NAME to decide which files the Windows lane imports, so a plain
skipif leaves a Windows test running on no host at all — green over zero
coverage, which is what AGENTS.md warns about.

Cross-platform tests simulate winerror 5 (what a held target actually
raises) rather than 32, and pin the state machine: retry-then-rewrite for
each contention code, an in-budget retry keeping the write atomic with no
fallback, a genuine denial propagating after the budget with its temp file
intact, a retry that turns into EXDEV switching to the copy fallback,
ENOSPC and POSIX EACCES propagating with no retry at all, the rewrite
never exposing a truncated file, and the #16743 symlink invariant holding
on the contended path.

On native Windows the lane exercises real held handles end to end, and
pins the winerror-5 premise so a CPython change surfaces here instead of
silently reintroducing the bug.

Co-authored-by: LewfKrad <lEWFkRAD@users.noreply.github.com>
Co-authored-by: ruochu88s <ruochu88s@users.noreply.github.com>
Co-authored-by: lost9999 <lost9999@users.noreply.github.com>
Co-authored-by: guanla-zz <guanla-zz@users.noreply.github.com>
Co-authored-by: zapabob <zapabob@users.noreply.github.com>
2026-08-12 17:25:30 -05:00
Hermes Agent dcbe175423 fix(utils): recover Windows renames contended by an open handle
os.replace onto a file that any other handle holds open is denied on
Windows — CPython opens files without FILE_SHARE_DELETE. atomic_replace
only fell back for EXDEV/EBUSY, so the exception propagated and, because
most callers swallow it, the write was silently dropped. gateway_state.json
loses status updates at every turn boundary while status readers poll it;
auth.json surfaces the same race to the user as 'agent init failed'.

Classify winerror 5/32/33 as contention candidates and retry the rename
with jittered backoff; a retry that wins keeps the write fully atomic.
Only a handle that outlives the budget falls through to a rewrite.

Measured on Windows 11 build 26200 / CPython 3.11: a held *target* handle
reports winerror 5, not 32 — 32 is what a held *source* reports. Keying
recovery on 32 alone misses every real occurrence of this bug.

The codes are ambiguous (a genuine ACL denial is also 5) and cannot be
told apart up front: os.replace needs delete-child rights on the parent
directory, so probing the target with os.access reports a directory-level
denial as writable. Rather than guess, both cases take the same bounded
path and a genuine denial is re-raised unchanged with its pending temp
file intact.

The last-resort rewrite writes through the existing file instead of
shutil.copyfile: a copy truncates the target to zero first, and a
concurrent reader can observe an empty auth.json mid-write. Writing
through the target also preserves its ACL, which os.replace does not.

Co-authored-by: LewfKrad <lEWFkRAD@users.noreply.github.com>
Co-authored-by: ruochu88s <ruochu88s@users.noreply.github.com>
Co-authored-by: lost9999 <lost9999@users.noreply.github.com>
Co-authored-by: guanla-zz <guanla-zz@users.noreply.github.com>
Co-authored-by: zapabob <zapabob@users.noreply.github.com>
2026-08-12 17:25:25 -05:00
rob-maron 66a4161620
add grok 4.6 (#84837) 2026-08-12 21:52:31 +00:00
kshitij 1d3d021282 chore: map 1759158233@qq.com -> wanquanY for contributor attribution 2026-08-13 01:34:00 +05:30
wanquanyang 0763e77bc4 fix(search): keep grep fallback root searchable 2026-08-13 01:34:00 +05:30
kshitij 068f58740c refactor(error_classifier): guard grammar check + extract shared constant
Follow-up to #84632:
- Guard _llama_cpp_grammar_hit inside status_code == 400 to restore
  short-circuit behavior on non-400 errors (minor efficiency)
- Extract _NO_USER_QUERY_SIGNAL constant for the duplicated string
  between _INVALID_MESSAGE_BODY_PATTERNS and the llama.cpp exclusion
  guard, preventing silent drift if the phrase is ever changed
2026-08-13 01:17:02 +05:30
xxxigm 847cc47cdb test(error_classifier): cover Qwen applyPromptTemplate vs llama.cpp grammar
Add regressions for the Discord-shaped applyPromptTemplate 400 that embeds
"No user query found", bare no-user-query on a large session, and the genuine
llama.cpp unable-to-generate-parser grammar path that must stay recoverable.
2026-08-13 01:17:02 +05:30
xxxigm a768a6c1a2 fix(error_classifier): stop misrouting Qwen "no user query" as llama.cpp grammar
Local engines wrap Qwen template raise_exception("No user query found…") as
applyPromptTemplate / "Unable to generate parser for this template". That used
to match llama_cpp_grammar_pattern, strip tool schema keywords, and retry while
the real cause was a poisoned/oversized transcript after failed compression.
Classify as format_error so recovery fails fast toward /new instead.
2026-08-13 01:17:02 +05:30
kshitij 169f207a86 fix: update kimi auxiliary test for medium→high mapping 2026-08-13 01:05:49 +05:30
kshitij d8e3b4f516 fix: map Hermes reasoning efforts onto K3's low/high/max vocabulary
K3 only recognizes low/high/max. Previously the Kimi provider only
forwarded low/medium/high verbatim and dropped every other level
(xhigh/max/ultra/minimal) to the thinking toggle, silently ignoring
the user's requested effort.

Now maps the full Hermes vocabulary onto K3's set, matching K3's own
server-side mapping:
  low, minimal       → low
  medium, high       → high
  xhigh, max, ultra  → max

ref: https://www.kimi.com/code/docs/en/kimi-code/models.html
2026-08-13 01:05:49 +05:30
rob-maron 7113a69248
fix(models_dev): map meta-ai provider to models.dev 'meta' id (salvage #81418) (#84679)
* fix(models_dev): map meta-ai provider to models.dev 'meta' id

Muse Spark models (muse-spark-1.1/1.2/-contributor) are served via the Meta
Model API and reverse-map from api.meta.ai to the Hermes provider id 'meta-ai'.
models.dev keys the same models under the provider id 'meta'.

lookup_models_dev_context() / _get_provider_models() resolve the models.dev id
strictly via PROVIDER_TO_MODELS_DEV.get(provider) (no raw-id fallback, unlike
get_model_info()), so an unmapped 'meta-ai' missed entirely and context fell
back to the generic 256K default instead of the true ~1M window. Add the
meta-ai->meta mapping (plus a defensive meta->meta) so context and pricing
resolve from models.dev: 1.1=1,000,000; 1.2 & -contributor=1,048,576.

* add contributor email

---------

Co-authored-by: Beto de Paola <betodepaola@meta.com>
2026-08-12 14:42:00 -04:00
brooklyn! 9eab7a4473
fix(ci): stop running uv lock --check on PRs that can't touch the lockfile (#84675) 2026-08-12 13:30:01 -05:00
brooklyn! 6aaa181f0e
docs: present /export and /import as the second way to share a profile (#84668)
The distributions guide framed export/import as local backup only, so the
new slash commands read as a competing path instead of the lightweight
half of one story. Give profile-distributions.md a comparison table up
front (git repo vs single file: updates, versioning, setup cost, what
each carries), rewrite the Not-a-fit bullets that mislabeled export, and
add a full Export/import section covering the CLI, TUI, and desktop
entry points, the desktop.json overlay, and what an archive actually
contains — including that it can carry memories and sessions, which a
distribution never does.

Also register /export and /import in the slash-command reference (they
shipped undocumented), point the profile-command entries at their chat
and desktop doors, and cover the desktop Export/Import UI on the desktop
page.
2026-08-12 13:10:03 -05:00
Teknium f525772725
fix(desktop): keep config/structured code blocks fenced instead of unwrapping to prose (#84664)
* fix(desktop): keep config/structured code blocks fenced instead of unwrapping to prose

The desktop markdown preprocessor has a "prose fence" heuristic that
strips the fence off blocks it thinks are wrapped prose. Its
`proseLines >= 3 && codeSignals === 0` rule fires on ANY 3+ line
plaintext block with no JS/SQL tokens -- which is exactly what an SSH
config, a .env dump, or any INI/key-value listing looks like. The result
was that a fenced ```-block of SSH config rendered as a flat paragraph
instead of a code block.

Add isLikelyStructuredText() and use it as a veto in both
isLikelyProseFence() and isLikelyProseCodeBlock(): a block is treated as
structured (and kept fenced) when it has indented continuation lines, or
when it has no sentence-ending punctuation and a majority of lines are
`Key value` / `Key: value` directives. Real wrapped prose has
sentence-shaped lines and no per-line indentation, so it still unwraps as
before. The bullet-prose case in isLikelyProseCodeBlock is checked first
so markdown bullet lists remain prose.

Tests: markdown-code.test.ts gains SSH-config / flat-config / .env
regression cases for both functions, plus direct isLikelyStructuredText
coverage, and re-asserts that genuine paragraph prose still unwraps.

* fix(desktop): tighten config-line detection to not match punctuation-less prose

The first CONFIG_LINE_RE matched any 'word word' line, so a wrapped prose
fragment with no sentence punctuation (e.g. 'the quick brown fox jumps')
was misread as a config directive and its fence kept. Split into an
explicit-separator form (Key: value / Key = value) plus a short 2-3 token
'Key value' directive form; a real sentence line has more tokens, so
punctuation-less prose is no longer treated as config.
2026-08-12 11:08:55 -07:00
Teknium 62a9c0f0e9
fix(file-safety): approval-gate ~/.ssh/config writes instead of hard-denying (#84663)
The write_file / patch file tools hard-denied ~/.ssh/config as a
"protected system/credential file", while the terminal tool only
*asked* for approval on ~/.ssh writes. That inconsistency meant a write
to ~/.ssh/config was refused via write_file but succeeded via terminal
after an approval prompt -- the same operation flip-flopping between
denied and OK depending on which tool ran it.

The SSH client config carries no private-key material, and editing it
(host aliases, ProxyJump, VS Code Remote-SSH targets) is a routine,
user-initiated task. It CAN carry ProxyCommand / Match exec directives
that run commands, so a free write is still inappropriate -- approval,
not a flat refusal, is the right policy, matching what the terminal tool
already does.

Changes:
- agent/file_safety.py: remove ~/.ssh/config from the flat credential
  deny; add build_write_approval_paths() + is_write_approval_required(),
  and short-circuit it out of the ~/.ssh/ prefix deny so the file is
  allowed at the classifier layer. Private keys, authorized_keys, and
  everything else under ~/.ssh/ stay hard-denied.
- tools/file_tools.py: _check_approval_required_write() routes ssh config
  writes through the shared _run_approval_gate (once/session/always,
  honors --yolo, fail-closed with no human), wired into write_file_tool
  and patch_tool right after the protected-instruction gate.
- Non-interactive consumers fail closed: the ACP file bridge
  (copilot_acp_client) rejects approval-required paths outright, and the
  TTS output-path picker refuses them as before.
- Docs + tests updated (security.md exception note;
  TestSshConfigApprovalGate covers config approval-gated, keys still
  hard-denied).
2026-08-12 11:04:45 -07:00
Brooklyn Nicholson 8a6deaaacf docs: present /export and /import as the second way to share a profile
The distributions guide framed export/import as local backup only, so the
new slash commands read as a competing path instead of the lightweight
half of one story. Give profile-distributions.md a comparison table up
front (git repo vs single file: updates, versioning, setup cost, what
each carries), rewrite the Not-a-fit bullets that mislabeled export, and
add a full Export/import section covering the CLI, TUI, and desktop
entry points, the desktop.json overlay, and what an archive actually
contains — including that it can carry memories and sessions, which a
distribution never does.

Also register /export and /import in the slash-command reference (they
shipped undocumented), point the profile-command entries at their chat
and desktop doors, and cover the desktop Export/Import UI on the desktop
page.
2026-08-12 12:57:18 -05:00
rob-maron 3e09adb109
add grok 4.6 (#84661) 2026-08-12 13:37:04 -04:00
kshitij a871948d8d fix: correct Lightpanda fallback docs — remove nonexistent PDF/upload/clipboard actions
Hermes has no browser PDF, file upload, or clipboard tools. The fallback
mechanism only covers commands in _FALLBACK_ELIGIBLE (open, snapshot,
screenshot, eval, click, fill, scroll, back, press, console, errors).
The original docs described Lightpanda's general limitations, not
Hermes's actual behavior.
2026-08-12 20:19:26 +05:30
Katie Brown 0a81935a9d docs(browser): document Lightpanda local engine 2026-08-12 20:19:26 +05:30
kshitij 222465d847 refactor(tools): unify probe caches and dedupe the exclusion log
/simplify-code findings on the full PR diff:

- _is_usable_python had the same sticky-failure bug the previous commit
  fixed in _python_environment_prefix: lru_cache pinned a transient
  probe failure (fork pressure, timeout) as False forever, silently
  locking project mode to sys.executable. Both probes now share a
  success-only bounded dict cache via _cache_probe_result() with FIFO
  eviction at _PROBE_CACHE_MAX (the old < cap guard stopped caching new
  entries instead of evicting, re-probing entry 33+ on every call).
- The hermes-root-omitted logger.info fired on every external-env call
  in project mode; now deduped once per interpreter path per process
  (matching the tirith/mcp warn-once convention).
- Regression test: _is_usable_python probe failures are retried, not
  cached (mutation-verified).
2026-08-12 17:42:34 +05:30
kshitij 89556c63ac fix(tools): harden interpreter-environment probe for the strict-mode default
Follow-up to the salvaged #81201 commits:

- Short-circuit _uses_hermes_python_environment when the child IS the
  running interpreter (path or realpath match). The default strict-mode
  path no longer spawns a probe subprocess at all, and a flaky probe of
  sys.executable can never drop the hermes root from PYTHONPATH
  (protects the test_repo_root_modules_are_importable invariant). The
  realpath leg also covers uv-style venvs whose bin/python resolves to
  the same binary.
- Stop caching failed probes: _python_environment_prefix now uses a
  success-only dict cache instead of lru_cache, so one transient
  timeout under load no longer sticks for the process lifetime.
- Deduplicate the subprocess probe scaffolding shared with
  _is_usable_python into _probe_python().
- Log once when the hermes root is omitted so import-behavior changes
  are diagnosable from user reports.
- Tests: fail the composition tests loudly if execute_code never
  reaches Popen (was vacuously passing on exceptions); assert the
  staging dir is literally first in PYTHONPATH (was truthiness only);
  add guards for probe-failure retry and the no-probe short-circuit.
2026-08-12 17:42:34 +05:30
Elisa Martinez Abad ec884fc655 feat(tests): add tests to cover external-venv PYTHONPATH isolation 2026-08-12 17:42:34 +05:30
Elisa Martinez Abad 76961b61bd fix(tools): isolate external project environments 2026-08-12 17:42:34 +05:30
kshitij a3bcb2c232 fix(tools): mirror misplaced-arg recovery on the terminal side
Whole-bug-class sibling of the execute_code fix: terminal(code=...) —
the reverse confusion — fell through to command=None and failed with
'Invalid command: expected string, got NoneType', naming neither the
stray 'code' argument nor execute_code as the right tool. Mirror the
guard in _handle_terminal (verified live: the opaque NoneType error
reproduces on main). Mutation-checked: removing the guard fails the
new regression test.
2026-08-12 15:36:23 +05:30
kshitij c5e2bff6c1 fix(tools): redirect non-string code payloads in execute_code handler
Review follow-up on the salvaged handler: a non-string 'code' (int,
dict, list) reached code.strip() and surfaced as a generic
'Tool execution failed: AttributeError' — the same unrecoverable shape
the salvage exists to eliminate. Add an isinstance guard beside the
'command' check that names the received type and shows the correct
call form; narrow the docstring to what the handler actually does.
Regression test drives int/dict/list through registry.dispatch and
asserts no AttributeError leaks (mutation-checked: removing the guard
fails 3 subtests).
2026-08-12 15:36:23 +05:30
Elisa Martinez Abad e8607417d7 feat(tests): add tests for execute_code error mesages 2026-08-12 15:36:23 +05:30
Elisa Martinez Abad b50d8f6917 fix(tools): improve error message when wrong args 2026-08-12 15:36:23 +05:30
Teknium f20d16fbf1
fix(windows): SSH ControlMaster gating + stop hijacking the user's python (#84452)
* fix(windows): SSH ControlMaster gating + stop hijacking the user's python

Two Windows environment-integrity fixes:

1. tools/environments/ssh.py (#73927): Windows OpenSSH has no
   Unix-domain-socket ControlMaster support, so unconditionally passing
   ControlPath/ControlMaster/ControlPersist failed EVERY tool call on a
   Windows-hosted ssh terminal backend with 'getsockname failed: Not a
   socket'. Gate the three multiplexing options behind a module-level
   _SSH_MULTIPLEX = (os.name != 'nt'); the scp upload path is gated the
   same way. On Windows the backend now works without connection pooling
   (each command a fresh connection); POSIX behavior is unchanged. The
   teardown 'ssh -O exit' is naturally inert because the socket never
   exists on Windows.

2. scripts/install.ps1 (#83797): the installer put the whole
   venv\Scripts directory on the user PATH, which contains python.exe /
   pythonw.exe / pip.exe and so silently hijacked the 'python' command in
   every terminal on the machine — unrelated projects started resolving
   python to Hermes' runtime interpreter. Now copy only the launchers
   (hermes.exe, hermes-acp.exe) into a dedicated $InstallDir\bin and put
   THAT on PATH. Existing installs are migrated: the legacy venv\Scripts
   entry is stripped from the user PATH on the next install/update. The
   new bin dir is under $InstallDir (…\hermes-agent), which the uninstall
   PATH sweep already matches via its \hermes-agent marker.

Updated the stale hermes_cli/update_cmd.py docstring that described the
old venv\Scripts-on-PATH layout.

Tests: SSH ControlMaster gating pinned both directions (multiplex on →
flags present; off → absent but BatchMode/StrictHostKeyChecking retained).
install.ps1 parses clean via the PowerShell AST parser.

* docs: update windows-native install docs for the bin\ launcher layout

CI (test_windows_native_docs) pins the docs and installer to the same
PATH layout. The #83797 fix moved the PATH entry from venv\Scripts to a
dedicated $InstallDir\bin holding only the hermes launchers, so update
the Windows-native guide to match: PATH-after-install section, the
install-steps list, the directory-layout table, the Get-Command
verification line, and the 'command not found' pitfall. Test now asserts
the bin\ layout and guards against a regression back to venv\Scripts on
PATH.

* fix: keep install.ps1 pure ASCII (PowerShell 5.1 codepage safety)

The two comments I added in the #83797 PATH-hijack fix used em-dashes,
tripping tests/test_install_ps1_ascii_only.py — Windows PowerShell 5.1
reads a BOM-less .ps1 in the system ANSI codepage (not UTF-8), so a
non-ASCII byte can misdecode into a stray quote and desync the parser
(issues #66994/#67000). Replace the em-dashes with ASCII '--'.
2026-08-12 02:56:33 -07:00
kshitij 8d0d908bef fix(tools): skip degenerate identical hunks in V4A validation
The apply phase already skips a hunk whose -/+ lines are identical
(patch_parser.py '(search_lines == replace_lines): continue'), but the
validation phase lacked the guard: such a hunk reached
fuzzy_find_and_replace, whose identical-strings error names
old_string/new_string — parameters that don't exist in patch mode — and
failed the whole atomic patch that apply would have accepted. Mirror
the apply-phase skip in validation; regression test drives a mixed
degenerate+live patch end-to-end (short text dodges the
is_already_applied >=8-char rescue).
2026-08-12 15:21:47 +05:30
kshitij 6061377bbf fix(tools): mirror must-differ guidance in skill_manage new_string schema
skill_manage's patch action uses the same fuzzy_find_and_replace engine
as the file patch tool and surfaces the identical-strings error verbatim
— and unlike the file path it has NO is_already_applied no-op rescue, so
identical old/new ALWAYS errors there. Mirror the new_string description
so the schema warns before the error fires (sibling-site parity with
tools/file_tools.py PATCH_SCHEMA).
2026-08-12 15:21:47 +05:30
kshitij 48db2011b9 refactor(tools): extract IDENTICAL_STRINGS_ERROR constant
The 3-sentence identical-edit message was snapshot-asserted verbatim in
two tests. House style avoids exact-string change-detector assertions;
both tests now import the constant from tools/fuzzy_match so rewording
the message can't silently break them.
2026-08-12 15:21:47 +05:30
Elisa Martinez Abad 9c541de91a fix(tools): improve patch tool parameter description 2026-08-12 15:21:47 +05:30
Elisa Martinez Abad 31a04db465 fix(tools): clarify identical old and new string error 2026-08-12 15:21:47 +05:30
Teknium 4a2198bf51
fix: Windows MCP PATHEXT resolution + python3 -> python in cross-platform skills (#84429)
Two Windows agent-loop friction fixes:

1. tools/mcp_tool.py (#56536): shutil.which(cmd, path=env_path) reads
   executable extensions from the PARENT process PATHEXT, not the MCP
   subprocess env — a stdio MCP config supplying both PATH and PATHEXT
   could fail to resolve a command its own env can locate, and startup
   then got a bare command name. On Windows, when the first which() call
   misses and the config env carries PATHEXT (any key casing), retry the
   resolution with the config's PATHEXT temporarily applied.

2. skills/ + optional-skills/ (#50606): 42 SKILL.md files that declare
   platforms: [.., windows] used python3 in their command examples.
   python3 does not exist on native Windows (the toolchain probe in the
   system prompt reports python3=missing), so every copy-pasted example
   burned a failed agent turn before self-correction. Replaced the
   command word python3 -> python (python3-config / python3.x version
   strings untouched). python is the spelling that exists in every
   Hermes-managed environment (Windows native, uv-managed venvs on all
   three OSes); agents on POSIX hosts additionally see the probed
   toolchain line and adapt either way.
2026-08-12 02:43:28 -07:00
Teknium e1caf88c6c
fix(security): approval system covers Windows destructive commands and paths (#84428)
Fixes #69472. On a Windows host every destructive native command passed
approval silently — DANGEROUS_PATTERNS were POSIX-shaped, and the
normalizer strips backslashes as shell escapes so no Windows path could
ever match a path rule. Probed live before the fix: 15 of 15 destructive
Windows commands (Remove-Item -Recurse -Force, del /s /q, iwr | iex,
taskkill /F, Format-Volume, diskpart, icacls /grant Everyone, vssadmin
delete shadows, bcdedit /set, reg delete, cipher /w, ...) sailed through
undetected.

Two changes:

1. Windows destructive tier in DANGEROUS_PATTERNS: PowerShell deletes
   (bare Remove-Item -Recurse/-Force), cmd builtins with /s|/q switches,
   iwr|iex remote execution (pipe and subexpression forms), taskkill /F /
   Stop-Process -Force, volume/disk destruction (Format-Volume,
   Clear-Disk, diskpart, format.com, cipher /w), icacls Everyone-grant /
   /reset, backup destruction (vssadmin delete shadows, wbadmin delete,
   bcdedit /set), reg delete / Remove-ItemProperty -Force, and service
   stop/delete (Stop-Service -Force, sc stop|delete). Each pattern
   requires the destructive flag so graceful/read-only usage (taskkill
   /IM without /F, reg query, icacls inspect, sc query, plain del file)
   does not prompt. Patterns live in the main list, not a win32-gated
   tier: a Linux-hosted Hermes can drive a Windows box over SSH.

2. Windows-path detection variant in _command_detection_variants: when
   the raw command contains a drive-letter/UNC backslash path, also
   yield a variant with backslashes flattened to forward slashes BEFORE
   normalization strips them, plus Windows spellings of the credential
   path rules (Users/<u>/.ssh, AppData/{Local,Roaming}/hermes .env).
   Gated on a real path shape so POSIX escape semantics are untouched.

Tests: tests/tools/test_approval_windows.py — 48 cases (27 destructive
flagged, 13 benign not flagged, 5 credential paths in both separator
spellings, 4 POSIX-escape non-regressions). The 8 pre-existing failures
under '-k approval' on this Windows host are identical on unmodified
main (ordering artifacts + known symlink cases) and unrelated.
2026-08-12 02:43:23 -07:00
Teknium 1156ba43bf
fix: steer agents off MSYS paths for native tools; pin line-ending preservation (#84426)
Two follow-ups from live Windows sessions:

1. agent/prompt_builder.py: extend the Windows shell hint with the
   native-binary path rule. Hermes disables MSYS path conversion for its
   bash, so agents passing /c/Users/... or /tmp/... to NATIVE programs
   (git -C, node, python, rg) hit 'cannot change to' / 'not found' while
   the same path works in bash builtins — observed repeatedly in a live
   session (git -C failures, git apply /tmp/x.patch failures). The hint
   now says: forward-slash native form (C:/Users/x) for native tools,
   $LOCALAPPDATA/Temp over /tmp for scratch files native tools read.
   (/tmp is pure model habit from Linux training data — nothing
   instructs it — so the hint is the right layer.)

2. tests: pin LF/CRLF preservation through write_file and patch_replace.
   A live session saw a repo-LF file come back full-CRLF after an edit
   (4699-line diff churn); not reproducible through current tool APIs,
   so pin the correct behavior — LF files stay LF, CRLF files stay CRLF,
   no mixed endings — to catch any regression on the Windows write path.
2026-08-12 02:43:17 -07:00
Teknium ee472a7fdb
fix: Windows agent-loop papercuts — path splitting, hashing, autocomplete, screenshots, OS detection (#84419)
Sweep of open Windows issues affecting day-to-day agent operation
(explicitly excluding install/setup and locale classes):

- hermes_cli/_subprocess_compat.py: new split_command_line() — Windows-
  safe command-line tokenizer (posix=False + quote stripping) so
  backslash paths survive. POSIX behavior unchanged (plain shlex.split).

- hermes_cli/console_engine.py (#83934): console commands like
  'sessions export C:\Users\me\out.jsonl' no longer silently mangle the
  path into a relative filename in the cwd.

- agent/shell_hooks.py (#78293): hook commands with backslash paths now
  spawn, resolve their script path, and pass hooks doctor instead of
  reporting 'not executable'. All three shlex sites routed through the
  shared splitter.

- agent/prompt_builder.py (#51755): system prompt now reports
  Windows (11) on Windows 11 — platform.release() returns 10 for both;
  distinguish via sys.getwindowsversion().build >= 22000.

- hermes_cli/commands.py (#42016): @ autocomplete no longer crashes the
  prompt_toolkit event loop when rg emits a path on a different mount
  (device paths \.\nul, other drive letters) — relpath ValueError is
  skipped per-entry.

- tools/browser_use_cli.py (#83884): screenshot-path detection now
  matches Windows drive-letter paths (C:\... and C:/...) in addition to
  POSIX; Browser Use screenshots attach on Windows.

- tools/skills_hub.py + tools/skills_guard.py (#62310): the two 'MUST
  stay symmetric' skill content hashes actually agree on Windows now.
  Bundle keys are normalized to POSIX separators before hashing, and the
  disk digest sorts by rel-posix STRING (case-sensitive) instead of Path
  objects (case-insensitive on Windows). Fixes permanent false-positive
  update_available for every installed skill.

Tests: tests/tools/test_windows_agent_loop_papercuts.py — 16 cases
covering each fix, including a disk-vs-bundle hash symmetry check built
with native Windows separators and a mixed-case filename.
2026-08-12 01:45:18 -07:00
kshitij a5170af4a8 chore: map hermes-agent@nous.local commit identity to @C-EXCITE-STUDIO
Salvaged PR #83678's commit is authored under a generic local agent
identity with no linked GitHub account; map it to the PR opener for
release attribution (same pattern as hermes-agent@users.noreply.local).
2026-08-12 13:58:26 +05:30
kshitij 223f703012 fix: close provider-anthropic MiniMax proxy bypass + rework cache observability
Follow-up fixes on top of the salvaged #83678 commit:

1. Hoist the MiniMax-M3 marker exclusion ABOVE the native-Anthropic
   early return. provider="anthropic" pointed at a MiniMax /anthropic
   proxy is a supported override (_anthropic_base_url_override_ok), and
   the is_native_anthropic branch matched on provider alone — returning
   (True, True) before the M3 exclusion was reached. Two regression
   tests pin the proxy route (M3 off, M2.7 still on).

2. Reuse the existing _model_name_suggests_minimax_m3() helper from
   agent/model_metadata.py instead of a second inline substring copy.

3. Drop the debug kwarg on normalize_usage() — it had zero production
   callers and duplicated standard logging level gating. The
   cache-observability line is now a plain logger.debug scoped to
   MiniMax providers on the Anthropic wire only, so the "+128 floor"
   note can no longer appear for native Anthropic where it is false.
   Tests updated accordingly (MiniMax logs, native Anthropic does not).
2026-08-12 13:58:26 +05:30
Hermes Agent c1e2529ae2 fix(cache): opt M3 out of cache_control markers on Anthropic wire
MiniMax-M3 ships server-side automatic prefix caching on the
Anthropic-compatible endpoint (content-keyed, no marker needed —
see platform.minimax.io/docs/api-reference/text-prompt-caching).
cache_control markers are NOT on its explicit-cache support list
(which covers only M2.7/M2.5/M2.1/M2).

Emitting markers on M3:
  - wasted serialization overhead
  - risked perturbing the server-side prefix hash
  - gave users a false sense of explicit-cache savings (the
    cache_read_input_tokens field carries a +128 constant floor
    and cache_creation_input_tokens is always 0 for M3)

Also add an opt-in debug=True parameter to normalize_usage() that
emits a debug-level log line carrying the observable cache fields.
This is the only reliable cache signal for M3 — off by default,
debug-level, scoped to the anthropic_messages wire, so production
callers see no impact.

Pin both changes with 8 new tests:
  - 4 M3 tests covering provider, host, and custom-provider paths
  - 1 regression guard ensuring M2.x caching is unaffected
  - 3 observability tests (off-by-default, on-with-M3, on-with-Claude)

Verified end-to-end against api.minimaxi.com/anthropic/v1/messages
with MiniMax-M3[1m]: identical system prompt hit-rate with and
without markers; cache_read field is unreliable (128 floor),
input_tokens drop (8467 -> 1) is the real hit signal.
2026-08-12 13:58:26 +05:30
kshitij 33855f1b30 perf(tools): linear-time masking rebuild + last-opener early exit
Efficiency review (measured with timeit probes) found two unbounded
costs on adversarial inputs:

- The masked-range rebuild copied the whole string once per range
  (O(n*k)): 50k tiny heredocs took 1.7s. Replaced with a single-pass
  segment join over the (sorted, non-overlapping) ranges: 152ms, and
  newlines are now counted on the original command instead of
  re-slicing.
- After the last '<<' occurrence no opener can start, but the scanner
  still walked the remaining text per-char: one heredoc followed by a
  1MB tail cost ~150ms. An rfind bound breaks out of the unit loop
  once the scan passes it: 0.3ms.

Typical commands are unaffected (the '<<' fast path already returns
first). 30/30 guard tests pass; mutation check re-run on the final
stack (no-op mutation -> 11 tests fail, restore -> green).
2026-08-12 13:57:59 +05:30