Commit Graph

1973 Commits

Author SHA1 Message Date
Teknium f01c193be4 refactor(schema): trim terminal and execute_code schema prose ~40%
Every tool schema ships on every API call. The terminal schema was
5,641 chars (~1,410 tokens) and execute_code 2,842 (~710) — the two
largest core tools, padded with repeated war stories and triple-stated
rules. Schema token audit across 88 tools: ~33k tokens total.

This trims prose while preserving every hard rule (each still stated
exactly once):
- terminal description 2,324 -> 1,233 chars: tool-redirect lines
  collapsed to one sentence; background/notify guidance deduplicated
  (was stated in desc + 2 params); PTY/pager rules merged.
- background/notify_on_complete/watch_patterns params 692/508/1,114 ->
  ~330/250/490 chars: kept the mutual-exclusion contracts, the
  rate-limit consequence, and the bounded-vs-long-lived distinction;
  dropped narrative repetition.
- execute_code description tightened (helper docs inlined to one line
  each; when-to-use kept).

Net: terminal schema 5,641 -> 3,386 chars, execute_code 2,842 -> 2,522
— ~700 tokens saved on EVERY request with the terminal+code toolsets.
One test updated (pinned a removed phrase; now pins the rule's new
phrasing).
2026-08-02 16:02:26 -07:00
Teknium 80631c4aea feat(terminal): recoverable truncation — spill full output + report pre-truncation size
Truncated terminal output was information LOSS: the middle was gone
and the only recovery was re-running the command (data: 1,394
truncation markers in a 250k-call window, with re-runs and grep
retries chained behind the big ones).

Truncation is now deferred retrieval (opencode/goose/qwen-code
pattern, codex's original_token_count idea):

- tools/environments/base.py: _BoundedOutputCollector gains an
  optional spill tee — when foreground output overflows the capture
  window, the FULL stream is teed to
  ~/.hermes/cache/terminal-output/out-*.log (lazy file creation with
  backlog backfill, 5MB hard cap, 7-day opportunistic cleanup,
  disk errors never break execution). All three _wait_for_process
  returns attach {output_total_chars, full_output_path} via a shared
  finalizer.
- tools/terminal_tool.py: redacts the spill with the same
  redact_terminal_output pass as the visible output (no secret
  persists unmasked), then surfaces output_total_chars,
  full_output_path, and a truncation_note pointing at
  search_files/read_file instead of a re-run.

Non-truncated results are byte-identical; internal unbounded
consumers (file-ops cat reads, RPC reads) are untouched (spill only
arms with bounded_capture=true).
2026-08-02 15:52:14 -07:00
Teknium 1c6d1a23c0 feat(patch): list match locations in ambiguous old_string errors
'Found N matches for old_string' (190+ occurrences in a 250k-window)
previously reported only the count, forcing a re-read of the file to
find the occurrences before retrying. The error now appends up to 5
'L<line>: <snippet>' rows (80-char cap per snippet, overflow noted),
so the model can disambiguate in ONE follow-up — add neighboring
context or choose replace_all — without the intermediate read.

Applies to both patch modes (replace + V4A) since they share
fuzzy_find_and_replace.
2026-08-02 15:51:43 -07:00
Teknium 7713d216f5 test(search): guard zero-match hint wiring on both engines
#77128 fixed the orphaned zero-match probe but shipped no tests, so the
same class of break can recur: an early return anywhere in the rg branch
silently makes the whole steering tier unreachable.

Asserts hint wiring per search engine with the probe stubbed to a
sentinel (the probe itself needs rg, so a real-text parity assertion
fails on the grep leg for an unrelated reason), plus the negative case
and the rg newline-warning skip that the early return originally
existed to preserve.

Sabotage-verified against 794d6c434e: restoring the early return turns
4 red; a naive fix that also drops the rg newline guard turns 1 red;
attaching the hint when matches exist turns 2 red.
2026-08-02 15:42:44 -07:00
Teknium eb62143006 feat(execute_code): recovery hints for known sandbox failure classes
The top execute_code failure shapes in production (state.db mining)
are sandbox-contract confusions, not logic bugs: importing tools that
aren't in the sandbox from hermes_tools (23x in one window, incl.
importing the built-in helpers json_parse/shell_quote/retry), importing
third-party packages absent from the sandbox interpreter (matplotlib
6x), and indexing tool-result dicts as strings. The stderr traceback
alone sends models into re-diagnosis loops.

Failed scripts (exit != 0) now carry one actionable 'hint' field:
- unavailable hermes_tools import -> lists the tools that ARE
  importable in this session + points to normal tool calls otherwise;
- built-in helper import -> 'no import needed, call it directly';
- ModuleNotFoundError -> 'sandbox has stdlib only; use terminal() with
  the project venv for third-party packages';
- string-indexing errors -> 'tool functions return dicts, do not
  json.loads them'.

Bounded 4KB stderr scan, first match wins, never raises; successful
scripts and unknown failures are untouched.
2026-08-02 15:13:24 -07:00
Teknium 1cefabc8af feat(search): auto-enable multiline mode for newline patterns
A regex \n (or a raw newline) in a search_files content pattern cannot
match in rg's default line-oriented mode. It previously either
hard-errored ('the literal "\n" is not allowed in a regex' — 17
occurrences in the production window) or, after the newline-warning
patch, returned 0 matches with an explanation — either way the model's
cross-line search intent required a manual workaround.

The rg engine now detects the pattern shape (_pattern_has_regex_newline,
already used by the warning path: odd-backslash \n escape or raw
newline; escaped \\n literals excluded) and enables -U/--multiline up
front, noting the mode switch in the result warning. Plain patterns are
untouched; the old line-oriented explanation is retained for the grep
fallback engine, which has no multiline mode.
2026-08-02 15:13:04 -07:00
Teknium 2a3a7e6f53 feat(skills): dedup repeat skill_view calls with an unchanged-content stub
skill_view re-sent full skill content on every call: ~286k tokens of
verbatim repeat views in a 400k-msg production window (one session
loaded the same skill 9 times), and a single repeat view of a large
skill costs ~25k tokens.

Mirrors read_file's proven unchanged-stub pattern: a per-task cache
keyed on (resolved name, file_path) with an mtime+size fingerprint of
the served file. On a repeat view of an UNCHANGED file, return a short
stub pointing at the earlier result. This does NOT violate the
skills-are-loaded-fully rule — the stub only ever replaces content
that is already fully present earlier in the same conversation, and:

- any on-disk change (patch, external edit) invalidates the entry;
- context compression clears the cache (wired next to
  reset_file_dedup in conversation_compression.py) so post-compression
  re-views return full content;
- setup-needed views are never deduped (readiness can change without
  the file changing);
- no task_id -> no dedup; caches are task-isolated; 200-entry cap.

Live E2E: repeat view of hermes-agent-dev 99,739 chars -> 374-char
stub.
2026-08-02 15:12:43 -07:00
Teknium 2c8a932f80 feat(file): verify write_file content on disk and say so (verified: true)
write_file confirmed only SIZE (wc -c) after writing — never content.
Models compensated by re-reading files immediately after writing them
(154 verify-reads in a 400k-msg production window), and a corrupted
write (truncated pipe, backend FS oddity) could silently pass.

The write path now compares the on-disk sha256 against the intended
content (one shell call). Three outcomes:
- match -> result carries verified: true; the schema tells the model
  an explicit contract: do NOT re-read to check the write landed.
- mismatch -> hard error ('The write did not persist correctly'),
  mirroring patch_replace's existing post-write verification.
- backend can't hash (no sha256sum) -> flag omitted, write unaffected.

Hashes the shim-adjusted content (after CRLF/BOM preservation) so
Windows-line-ending and BOM round-trips verify correctly; surrogatepass
encoding matches the rest of the codebase's hashing of model text.
2026-08-02 15:12:23 -07:00
Teknium 5d675a2ca7 feat(patch): whitespace-visualized diagnosis on residual no-match errors
When old_string survives all 9 fuzzy strategies without a match but
the closest candidate line matches after stripping whitespace, the
failure is whitespace-shaped (tabs vs spaces, indent depth). The
did-you-mean hint now appends a two-line diagnosis with leading
whitespace made visible:

  Whitespace difference detected (→ = tab, · = space):
    file has: →def start(self):
    you sent: ····def start(self):
  Use the exact whitespace shown in 'file has'.

Pattern ported from crush's diagnoseMismatch (agent-codebase survey) —
it converts the residual dead-end error into a one-turn fix. Only the
leading run is visualized (interior spacing stays readable); content-
shaped misses and raw-exact candidates are unchanged.
2026-08-02 15:12:02 -07:00
Teknium 6f5d6b1f5b feat(terminal): auto-save parser-limit-blocked payloads as runnable scripts
Follow-up to the recovery-recipe commit on this branch, per review:
instead of only TELLING the model to re-author the payload via
write_file (2 turns), materialize the blocked command to
~/.hermes/cache/blocked-scripts/blocked-*.sh and point the recovery
at it directly: 'saved to <path> - review it, then run
terminal(command="bash <path>")' (1 turn).

Safety posture is unchanged or better:
- Nothing is executed here; the file is only written.
- The bash <path> follow-up goes through the normal execution
  pipeline, including the referenced-script content guard, which
  inspects script files named in commands - the payload is MORE
  visible to policy than it was inline.
- Genuine hardline blocks (destructive ops) never save anything
  (test-asserted).
- Save failures fall back to the previous manual write_file recipe.
- 7-day opportunistic cleanup of saved payloads.
2026-08-02 15:11:21 -07:00
Teknium b1711c6f2e fix(terminal): blocked-command errors carry a concrete recovery recipe
Two block classes from production mining (250k-call window) that
models answered with blind rephrase-retries:

1. Parser-limit / malformed-payload hardline blocks (198x): these fire
   on oversized inline payloads (heredocs, giant one-liners), not on a
   forbidden operation - but the message read like a permanent ban.
   The block now appends: 'RECOVERY: ... write the script to a file
   with write_file, then run bash /path/script.sh - do not retry
   inline.' Genuine hardline blocks (destructive filesystem
   operations) are unchanged.

2. Backgrounding-wrapper blocks (200x): the guidance now spells out
   the exact corrected call shape - 're-send WITHOUT the wrapper as
   terminal(command="<cmd>", background=true,
   notify_on_complete=true)' - instead of describing the feature
   abstractly.
2026-08-02 15:11:21 -07:00
Teknium 0b149ca030 fix(process): wait timeout result reads as status, not failure
process(action='wait') hitting its window returned status='timeout'
with a terse note — models read it as an error and re-issued identical
waits (process is the #1 exact-duplicate tool call in production: 511
dupes in a 400k-msg window; wait is 57% of all process actions).

The timeout result now carries:
- process_running: true — machine-readable 'this is a status, not a
  failure'
- an explicit note: 'Wait window of Ns elapsed — the process is still
  running. This is not an error. Uptime: Ms.' plus the right next step:
  when notify_on_complete is set, 'you will be notified on exit — do
  more work instead of waiting again'; otherwise a pointer to
  notify_on_complete for next time.
- the clamp note (requested > max) now composes with the status note
  instead of replacing it.

Exited/interrupted results are unchanged.
2026-08-02 15:11:02 -07:00
Teknium e7aa06c3a6 feat(search): hidden/gitignored probe on zero-match results
Third zero-match steering tier: when a content search finds nothing in
visible files, probe once with rg --hidden --no-ignore --count-matches.
If the pattern exists only in dotdirs or gitignored files, the result
says so and tells the model to search the hidden path explicitly.

Found live by the benchmark battery: a task with a match inside
.hidden/ returned a bare 0 and the model missed the file entirely in
2/3 baseline runs.
2026-08-02 15:10:42 -07:00
Teknium 5797b50288 feat(search): zero-match probes and multi-path recovery
Two dead-turn classes from production mining (state.db, recent window):

1. 13.9% of 19.6k content searches return 0 matches with no steering.
   Now a 0-match content search runs one cheap rg -i --count-matches
   probe (plus an rg -F probe when the pattern has regex metachars) and
   attaches what it found: '0 exact matches, but N case-insensitive
   matches — casing may be wrong' / 'N literal matches — metacharacters
   need escaping'. True zero-match results stay clean (no noise).

2. 122 'Path not found' failures came from models passing several paths
   in ONE path string ('dir1 dir2 dir3', comma lists). Instead of
   failing wholesale, split the string, search every path that exists,
   merge results, and report skipped parts in a warning. Single-path
   misses keep the existing Similar-paths hint; all-missing multi-path
   strings still error.

Both probes are bounded (count-only rg, 30s timeout, max 2 invocations)
and wrapped so a probe failure can never break the search result.
2026-08-02 15:10:42 -07:00
Teknium a18a2f170c feat(terminal): echo cwd in result when a command changes the working directory
Production mining (state.db, 400k-msg window): 60.2% of 104k terminal
calls carry a defensive 'cd X && ' prefix (~925k tokens of pure prefix)
and 2,462 failed calls led with cd — the model cannot see cwd state, so
it re-asserts it on every call and runs pwd/ls diagnostics after
directory changes.

The result dict now includes a 'cwd' field whenever the session cwd
after the command differs from the cwd it started in (cd, pushd,
chained cd). Stable-cwd commands are unchanged (no field, no noise).
Per-command workdir overrides stay transient by contract and never
echo. Schema note added so models learn to trust session cwd instead
of prefixing. Pattern borrowed from crush's <cwd> injection.

realpath comparison avoids false echoes through symlinks; the echo is
wrapped defensively so a backend without .cwd can never break the
result path.
2026-08-02 15:10:13 -07:00
Teknium 99d6f55e38 feat(patch): detect already-applied edits and return success no-op
The #1 patch failure class in production (state.db mining, 250k-window)
is a re-send of an edit that already landed: 'old_string and new_string
are identical' (299 occurrences) plus a share of hunk-not-found errors
where the new text is already in the file. These errored, sending
models into re-read/re-patch loops.

New tools/fuzzy_match.is_already_applied(content, old, new) — a
conservative check requiring (1) non-trivial new_string (>=8 chars),
(2) EXACT presence of new_string, (3) old_string gone (unless
identical). Wired into three sites:

- patch_replace (replace mode): returns success + no_change: true +
  an explicit note instead of the identical-strings / no-match error.
- V4A validation phase: an already-applied hunk validates as a no-op
  so multi-hunk patches no longer fail wholesale when one hunk landed
  in a prior call.
- V4A apply phase: mirrors the same skip so the two phases agree.

Genuine no-matches (new text absent) and half-applied renames (old
text still present) keep their error behavior — covered by tests.
2026-08-02 15:09:53 -07:00
Teknium af27e60603 feat(file): raise read_file default limit from 500 to 2000 lines
Production mining (state.db, 28.5k read_file calls in the recent
window) shows 74.3% of reads truncated — nearly all by the 500-line
default, not the char budget (22,443 line-limit vs 13 char-budget
truncations). That churn produced 12,229 redundant re-reads, and after
a truncated result the most common next move was fleeing to terminal
cat/sed (4,608 times) — the pagination contract was not trusted.

Median truncated file is 2,422 total lines, so a 2000-line default
makes 44% of today's truncated first-reads complete in one call while
the unchanged ~100K-char budget still caps worst-case result size
(same ceiling as before: 500 lines x 2000-char line cap = the same
100K). Schema max was already 2000.

Touchpoints: DEFAULT_READ_LIMIT + both read_file signatures
(file_operations.py), read_file_tool + schema text (file_tools.py),
execute_code sandbox stub docs (code_execution_tool.py), 3 tests
pinning the old default.
2026-08-02 15:09:33 -07:00
Teknium 677473273e feat(terminal): output-pattern failure hints for common error classes
When a command exits non-zero, scan the first 4KB of output for
well-known failure shapes and attach one short, actionable recovery
hint to the tool result ('hint' field):

- gh 'Unknown JSON field' (9.2k occurrences in a 250k-call window)
- git merge conflicts (1.2k) — stop verbatim retries
- command not found (1.0k), incl. python->python3 and pip->pip3
- ModuleNotFoundError (739) — venv activation guidance
- 'already exists' (633), gh rate limits (133), permission denied
- exit-code-only tier: 124 timeout, 126 not-executable, 137 SIGKILL

Hints are suppressed when the existing exit_code_meaning tier already
explains the code (grep=1 etc). Pattern order = production frequency
from state.db mining; first match wins; pure function, no I/O.
2026-08-02 15:08:35 -07:00
xrazai e57a8f5cb9 fix(gateway): preserve delegates during session reaping 2026-08-02 14:02:08 -07:00
kshitij fb6446fc9e fix(cron): scope cron approval context per session
Replace the process-global HERMES_CRON_SESSION env var with a per-session
ContextVar so a cron tick in the gateway process cannot leak into unrelated
live gateway/API/TUI turns. The cron scheduler now sets the ContextVar
inside the job's try/finally scope and resets it on cleanup. Gateway, API
server, ACP adapter, and TUI gateway all pass cron_session='' to explicitly
mark their sessions as non-cron, masking any stale process env.

Co-authored-by: hinablue <hinablue@gmail.com>
Closes #37968
2026-08-03 00:25:20 +05:30
kshitij 4fa8d7bb67 fix(tools): staleness + tracking-parity fixes for the not-found cache
Review follow-ups on the #25387 salvage:

1. CRITICAL: a cached miss survived out-of-band file creation (terminal
   command, external process) for the full 60s TTL — breaking the common
   agent pattern 'check for file -> create it -> read it' (live-repro'd).
   Serve-side existence guard: one ~free stat before serving a cached
   miss; if the path now exists the entry is evicted and the real read
   runs. Also fixes the search-root variant (write under a cached-missing
   directory). Both mutation-checked.

2. notify_other_tool_call now clears the task's not_found entries too
   (belt: the dispatcher calls it for every non-read tool).

3. Tracking parity: the record sites no longer early-return. On upstream,
   error results flow through consecutive-loop detection and dedup
   bookkeeping; short-circuiting skipped that and broke
   TestDedupInvalidationTaskResolution when preceded by
   TestSilentFileMisplacementE2E (bisected: the early return at the
   read record site was the trigger). Recording is now
   side-effect-identical to upstream; serving from the cache remains the
   optimization. Also reuse the already-computed _resolved instead of
   resolving a second time.
2026-08-02 22:45:28 +05:30
Kent acfb40c9c7 perf(tools): negative-result cache for read_file + search misses
When read_file or search hits a non-existent path, ShellFileOperations
spawns a subprocess to stat the path and another to walk the parent
directory for "did you mean..." suggestions. A typo'd path retried 13
times (observed in the wild) costs 26 subprocess invocations + 13 ls
walks for a result we already know.

Add a per-task negative-result cache keyed by (op, resolved_path) with
a 60s TTL and a hard cap of 500 entries. On hit, return the cached
error JSON immediately and skip the subprocess + suggestion walk.

The cache is namespaced by operation ("read" vs "search") because the
two callers return different error JSON shapes ("File not found:" vs
"Path not found:"). Eviction:

  * TTL (60s) — short, so a path that appears later isn't masked.
  * write_file / patch on the same path — _invalidate_dedup_for_path
    now also drops the negative-cache entry so a freshly-written file
    is read from disk on the next call instead of returning a stale
    "not found" stub.

Tests in tests/tools/test_file_tools.py cover:

  * read cache hit skips the subprocess on retry
  * cache is per-task (no cross-task pollution)
  * successful reads do not poison the cache
  * search cache hit skips the subprocess on retry
  * read and search caches are namespaced (different error shapes)
  * write_file invalidates the read negative cache
  * TTL expiry evicts stale entries
2026-08-02 22:45:28 +05:30
Ray 062d44bba7 fix(xai): fail closed in xai_http.get_env_value — honor get_secret's verdict
Salvaged from #56982 (@rayjun): the live piece of the PR. The
hermes_cli/config.py get_env_value scope-honoring change and its
test_env_load_cache.py tests were already merged via ed1170cd8b
(#76462) and are dropped here.

tools/xai_http.py::get_env_value wrapped the scope-aware
hermes_cli.config.get_env_value in except Exception + a raw os.environ
fallback — swallowing UnscopedSecretError and borrowing the process
env, so a multiplexed xAI credential read could silently pick up
another profile's XAI_API_KEY. Narrow the except to ImportError (the
only legitimate degraded case) so get_secret's verdict propagates: an
unscoped multiplexed read fails closed, and a scoped miss returns the
default instead of the foreign environ value.

Co-authored-by: rayjun <rayjun0412@gmail.com>
2026-08-02 09:59:52 -07:00
kshitijk4poor 4983c576b1 fix(docker): gate the remaining every-boot chown walks (cron, pairing)
Whole-bug-class follow-up to the profiles/ gate: cron/, platforms/
pairing, and legacy pairing/ ran chown_hermes_tree unconditionally on
every boot with the identical warm-boot cost profile. Same
tree_has_non_hermes_owner gate; find evaluates the top directory first
and -quits on the first mismatch, so a mis-owned tree short-circuits in
O(1) while a clean tree pays one read-only walk instead of a full
chown -R inode rewrite.
2026-08-02 21:54:10 +05:30
LeonSGP43 f1da9d0d66 fix(docker): skip redundant stage2 chown walks 2026-08-02 21:54:10 +05:30
spfcraze 48e8254567 perf(tools): use load_config_readonly on the approval guard path
The terminal-command guard path loaded config 2-3x per invocation via
load_config(), which pays a defensive deepcopy of the entire config on
every call (~356us of the ~376us warm-cache cost measured on a real
config.yaml). All six swapped call sites were audited read-only — every
caller takes scalar reads or iterates the returned structures; none
mutate (the save path at save_permanent_allowlist keeps load_config) —
so they now use load_config_readonly(), the API built for exactly this
(precedent: #74211, #74322; the one unsafe-site lesson from #56085's
salvage is covered by the mutation audit and a cache-integrity test).

Measured (real config.yaml, warm cache): load_config 376.0us ->
load_config_readonly 19.9us (18.9x); full guard pass
check_all_command_guards('ls -la','local') 930.7us -> 241.8us (3.85x).

Tests: new test_approval_config_readonly.py drives the real functions
against a temp HERMES_HOME — readonly call counts per function, a
no-deepcopy pin for the full guard pass, and cache-identity/integrity
checks. Existing test mocks retargeted from load_config to
load_config_readonly (same injection intent). Note: 6
test_approval_mode_parity failures are pre-existing ordering flakes —
identical with the change stashed on clean main.
2026-08-02 21:17:49 +05:30
kshitijk4poor cd6585abf8 refactor(process-registry): fold kill_started_since into kill_all via exclude_ids
kill_started_since duplicated kill_all's collect-under-lock/kill-outside-lock
loop line for line; it is now a thin delegate through new kill_all kwargs
(exclude_ids, source, consume_output). Public signatures unchanged — existing
callers and test monkeypatch seams keep working. kill_process's docstring now
names the deliberate consume_output=True exception for abandoned-turn reaping
so the deviation isn't 'fixed' later.
2026-08-02 14:23:55 +05:30
joaomarcos 80e4fb5995 fix(gateway): reap only the background processes an abandoned turn created
An agent turn can spawn a long-running background subprocess (e.g.
`next build`) and later be abandoned via inactivity timeout, /stop,
/new, or a client disconnect. Before this fix the gateway interrupted
the agent loop but never touched the subprocess: it kept running
inside the gateway's cgroup, unbounded, until memory pressure starved
the event loop and made every platform/cron look hung (#76115).

The process registry already knew how to kill a process tree — the
missing piece was per-turn ownership: nothing distinguished a process
that predates the turn (must survive), a process the turn started and
finished successfully (must survive), and a process an abandoned turn
left running (must be reaped).

- tools/process_registry.py: snapshot_running_ids() captures a turn's
  starting baseline; kill_started_since() reaps only IDs created after
  it, scoped to one task_id.
- gateway/turn_context.py: TurnContext carries process_task_id +
  process_baseline so the timeout/interrupt paths can reach them.
- gateway/run.py: baseline is snapshotted right before the turn's
  executor task starts; the inactivity-timeout path and the explicit
  /stop|/new|disconnect interrupt path both reap via the same helper.
  A daemon-thread watchdog backs up the asyncio-based timeout poll,
  since a starved event loop is exactly the failure mode this bug
  causes. The turn's own worker clears its ownership markers the
  instant it finishes, closing a race where a /stop landing right
  after normal completion could reap a background process the turn
  deliberately left running.

Related but insufficient on their own: #37454 (cgroup ExecStopPost
reaper only fires on service restart) and #68915 (orphaned-pipe
grandchild detection, a registry bug not a turn-lifecycle gap).
Neither ties process cleanup to turn abandonment.
2026-08-02 14:23:55 +05:30
kshitijk4poor 0cd26ce9a5 refactor(cron): log the heartbeat ceiling stop + test it
- logger.warning when the 6h ceiling stops the heartbeat (matches the
  delegate_task stale-stop precedent) so the eventual watchdog reap is
  explainable from logs instead of silent
- new mutation-checked test: past the ceiling the heartbeat stops while
  the job still completes
- clearer assertion messages (surface res on failure)
2026-08-02 14:04:52 +05:30
kshitijk4poor 8fd1a68106 refactor(cron): harden the run heartbeat (review follow-ups)
- heartbeat loop continues past a raising activity callback instead of
  silently stopping (matches delegate_task / touch_activity_if_due
  swallow-and-continue semantics) — one transient error must not drop
  watchdog protection for the rest of a long job
- hard 6h elapsed ceiling so a wedged job under HERMES_CRON_TIMEOUT=0
  (unlimited child watchdog) cannot mask the gateway watchdog forever
- public get_activity_callback() accessor in tools/environments/base.py
  instead of importing the private _get_activity_callback cross-module
- tests: deterministic heartbeat test (event-gated, no timing sleep),
  no-callback test now asserts the thread is truly never created, new
  exception-survival guard; dead started event removed
- fix comment: delegate_task heartbeat cadence is 30s, not 10s
2026-08-02 14:04:52 +05:30
webtecnica 2314abcbb0 fix(cron): run job without blocking the calling turn (#76502) 2026-08-02 14:04:52 +05:30
kshitijk4poor 881ac52423 fix: widen category guard — hybrid skill-dir nesting and file collisions
Follow-up to the salvaged #76000 guard:
- refuse installing a skill INTO an existing skill directory (hybrid
  skill-plus-category dirs whose later update/uninstall rmtree would
  destroy the nested skill — sibling case of #75983)
- refuse a stray regular file at the install path with the caller's
  ValueError contract instead of an uncaught NotADirectoryError
- regression tests: nested-only category (skills at depth >= 2),
  category-inside-skill, file collision
2026-08-02 13:31:16 +05:30
x7peeps 75e85ef6ba fix(tool/skills): refuse to overwrite category bucket during skill install (issue #75983)
Fix #75983

## 根因分析

hermes skills install <url> --name <name> 在安装技能时,如果目标路径(即
<skills_dir>/<name>)已存在,会无条件调用 shutil.rmtree 删除该目录。当
<name> 碰巧与用户手动创建的类别目录(category bucket)同名时,rmtree 会
删除整个类别目录及其下所有无关技能,造成静默的、不可逆的数据丢失。

lock.json 的已有检查仅追踪通过 hub 安装的技能,不覆盖用户手动创建的目录。

## 修复方式

在 install_from_quarantine() 的 rmtree 之前,增加类别桶保护逻辑:
1. 如果 install_dir 已存在且是目录,但不包含顶层 SKILL.md(说明不是技能目录)
2. 检查该目录下是否包含其他技能子目录(含 SKILL.md 的子目录)
3. 如果是,则抛出 ValueError 拒绝安装,列出受影响的技能名称
4. 如果不是(空目录或仅含非技能文件),则允许继续(与原有行为一致)

这样既保护了用户的类别桶不被意外删除,又不影响正常技能目录的覆盖安装。

## 回归测试

新增 3 个测试用例:
- test_install_from_quarantine_rejects_category_bucket_overwrite:
  验证包含技能的类别桶被拒绝覆盖,且内部技能文件完好
- test_install_from_quarantine_allows_existing_skill_overwrite:
  验证已存在的技能目录(含 SKILL.md)仍可被覆盖安装
- test_install_from_quarantine_allows_empty_category_dir:
  验证空目录仍可被正常安装覆盖
2026-08-02 13:31:16 +05:30
Christopher fc61608a17 fix(security): isolate explicit Docker passthrough snapshots 2026-08-02 00:36:03 -07:00
Christopher 7138b9587a fix(security): scope passthrough env to routed profile 2026-08-02 00:36:03 -07:00
tachyon-r 3d9a146d81 fix(browser): scope Camofox session identity 2026-08-02 00:11:50 -07:00
tachyon-r 76cf19fee1 fix(tools): isolate model tools by multiplex profile 2026-08-02 00:11:50 -07:00
kshitij 582606f176 test(tts): reconcile test file with main and add regression tests
Start from main's 13 tests (renamed test_openai_available_reflects_key
to test_openai_available_reflects_audio_key_resolution, added 4 new
tests for xai oauth, elevenlabs secret resolver, openai configured
key, stream cap). Append 12 new regression tests from PR #71084 for
the prefetch pipeline, PCM misalignment, and PortAudio resilience.
Patch platform.system in stream-path tests for main's macOS guard.
2026-08-02 12:08:29 +05:30
Gille 58e85f4314 fix(browser): replace expired cloud sessions 2026-08-02 11:18:41 +05:30
Teknium 7f4d155159 fix(tools): validate timeout, reject whitespace old_string, narrow /private/var block
Three lower-severity core-tool robustness fixes from a targeted audit, each
reproduced live:

1. terminal_tool did not validate non-positive timeouts. 'timeout or default'
   silently coerced 0 to the config default (0 can't mean 'no timeout'), and a
   negative value is truthy so it flowed into 'deadline = now + timeout' and
   fired an immediate '-Ns' timeout. Reject timeout <= 0 with a clear message.

2. fuzzy_find_and_replace accepted a whitespace-only old_string, which matches
   trivially (blank line / run of spaces) and mass-replaces under replace_all
   or raises an opaque ambiguity error. Reject it alongside the empty check.

3. The '/private/var/' sensitive-path prefix over-blocked ALL macOS temp-file
   writes: , /tmp, and /var/folders realpath into /private/var/folders
   on macOS (and paths are resolved through symlinks), and /private/var/tmp is
   a normal temp dir. Narrowed to the genuinely-sensitive subtrees
   (/private/var/db, /private/var/root); /etc and /private/etc stay blocked.

All verified with sabotage-checked regression tests. 85 terminal/fuzzy/file
tests pass; normal timeouts, legit replacements, and /var + /boot + /etc
blocking are unaffected.
2026-08-01 15:41:21 -07:00
Teknium 62f00319db fix(patch-parser): tolerate CRLF patch bodies and Move-then-Update
Two V4A parse/validate bugs found in a core-tools audit, reproduced live:

1. CRLF patch body injected stray carriage returns. parse_v4a_patch split
   on '\n' only, so a CRLF-encoded patch kept '\r' inside every HunkLine
   and wrote mixed line endings into an LF file; the anchored Begin/End
   markers could also fail to match because of the trailing '\r'. Strip a
   trailing '\r' from each line at split time.

2. Move-then-Update of the same file was rejected. _validate_operations read
   the UPDATE target from disk before the MOVE ran, so 'Move a->b' + 'Update
   b' failed validation with 'b: file not found'. Added a small pending-move
   overlay so UPDATE/DELETE/MOVE reads during validation see prior ops'
   effects (moved-in destinations resolve, moved-away sources read as gone),
   while a genuine 'destination already exists' conflict is still caught.

Both verified with sabotage-checked regression tests. 113 patch/fuzzy/file
tests pass.
2026-08-01 15:40:39 -07:00
Teknium c0b0c88626 fix(fuzzy-match): stop context_aware from silently replacing wrong content
Strategy 9 (context_aware, the last-resort fuzzy strategy used by
patch_replace, V4A UPDATE hunks, and skill_manage) had two serious flaws,
both reproduced live against current main:

1. CORRECTNESS: it accepted a block when >=50% of its lines were >=0.80
   similar. A 2-line pattern with one real line and one garbage line matched,
   silently deleting the non-matching line and persisting a wrong edit as
   success. Now requires the first AND last lines to anchor-match and EVERY
   non-blank pattern line to be >=0.80 similar — one garbage line disqualifies
   the block.

2. PERFORMANCE: it scored every content window with per-line SequenceMatcher,
   so every failed match paid O(file_lines x pattern_lines) — measured ~5.5s
   for a single 40-line no-match on a 10k-line file, per hunk. The first/last
   line anchor pre-filter skips non-candidate windows: same case now ~160ms
   (34x faster).

Also gate replace_all: a similarity-based strategy (block_anchor,
context_aware) with multiple matches under replace_all would overwrite every
approximate block, not just exact ones. Now refused with a clear error
directing the caller to precise text.

All verified with sabotage-checked regression tests (fail against the old
50% logic). 158 file/patch/fuzzy tests pass; legit fuzzy edits (indent drift,
unique near-match) unaffected.
2026-08-01 15:40:13 -07:00
Teknium 021a076880 fix(file-ops): prevent non-UTF-8 corruption and symlink data-loss
Two DATA-LOSS bugs in ShellFileOperations found in a core-tools audit,
each reproduced live against current main:

1. Non-UTF-8 file content silently corrupted on read->write. The terminal
   env decodes stdout with errors='replace', so a latin-1/8859 file's bytes
   arrive as U+FFFD before _is_likely_binary inspects them. U+FFFD is
   'printable', so the >30%-non-printable check never flagged it, and the
   agent would read the mojibake and write it back, permanently replacing the
   original bytes. Fix: treat a sample containing U+FFFD as binary (read-only).

2. Writing through a symlink destroyed the link and orphaned the target. The
   atomic temp-file + 'mv -f' swap replaced the symlink itself with a plain
   file; the real target was never updated. Fix: resolve the link with
   readlink -f/realpath first and recompute the temp dir from the resolved
   target so the mv stays same-filesystem atomic. Broken links fall back to
   the original path (no regression).

Both verified with sabotage-checked regression tests (fail without the fix).
Proper UTF-8 text (incl. non-ASCII) and plain-file writes are unaffected.
2026-08-01 15:39:33 -07:00
Teknium 9d08c95464 fix(tools): dedup eviction task_id + workdir cwd leak
Two independent HIGH-severity correctness bugs found in a core-tools audit,
each reproduced live against current main:

1. Read-dedup was never evicted after a write on non-default tasks.
   _invalidate_dedup_for_path looked up the read-tracker under the correct
   task_id but resolved the path with _resolve_path(filepath) — which
   DEFAULTS task_id='default'. The dedup cache is keyed by the task-resolved
   absolute path, so for any task whose workspace cwd differs from the process
   cwd (every -w worktree / Desktop / ACP session using relative paths) the
   computed key never matched and the stale entry was never removed. A
   read_file after a write_file/patch could then return the OLD content stub
   when mtime coincided. Fix: pass task_id through.

2. A per-command workdir override permanently hijacked the session cwd.
   The post-command dual-write unconditionally recorded env.cwd (stamped to
   the transient workdir) into the durable session-cwd store, so every later
   command that omitted workdir inherited the one-off directory — contradicting
   the documented 'Working directory for this command' contract. Fix: skip the
   session-cwd record when workdir was explicitly supplied.

Both verified with sabotage-checked regression tests (fail without the fix).
2026-08-01 15:38:57 -07:00
dsad fcd5e2cc61 fix(file-tools): resolve local V4A patch paths before apply
patch_tool resolved V4A header paths against the task workspace for
locking, staleness, and reporting, but handed the original (often
relative) patch text to file_ops.patch_v4a — which re-resolved headers
against the backend env's own cwd. When the two diverge (the git-worktree
cwd bug), a relative header landed in a different directory than
everything the tool locked and reported: a silent wrong-file write.

Rewrite Update/Add/Delete/Move File headers to the resolved absolute
paths before apply, only for host-filesystem backends (container/remote
namespaces keep their own paths). Header patterns mirror patch_parser
(no-space ***Update File: form) and cover Move File: src -> dst.

Salvage of #53176 by @necoweb3, reimplemented onto current main (the
original branch predates the sensitive-path/Move-header extraction and
per-path locking now in patch_tool).

Co-authored-by: necoweb3 <sswdarius@gmail.com>
2026-08-01 14:31:51 -07:00
spfcraze 8c172726c8 fix(patch): anchor V4A Begin/End Patch markers to full lines
The boundary scan in parse_v4a_patch used substring matching, so a
content line mentioning "*** End Patch" (docs about the patch format,
nested patch text) truncated the patch, and "*** Begin Patch" in
content reset the start boundary — silently dropping already-parsed
operations while reporting success. Match only whole-line markers at
column 0, preserving the no-space "***Begin Patch" tolerance.
2026-08-01 14:31:48 -07:00
konsisumer f40f4711ed fix(install): support non-pid-1 container entrypoints
Replace the bare /init ENTRYPOINT with entrypoint-dispatch.sh: exec
/init + main-wrapper when the image owns PID 1, fall back to a direct
stage2 bootstrap (with the s6 helper PATH restored) on wrapped runtimes
where s6-overlay-suexec would abort with 'can only run as pid 1'
(Fly Machines, docker run --init, podman/FreeBSD setups).

Cherry-picked from PR #43763 by @konsisumer, conflicts with current
main resolved (tests/test_dockerfile_tini_compat_shim.py was moved to
tests/docker/, container_boot argv tests were reshaped upstream).

Fixes #38349
2026-08-01 10:52:34 -07:00
Teknium 5eeafc8d25 fix(security): cache OSV malware preflight verdicts and stop double component discovery (#75485)
Two amplifiers behind the 779K api.osv.dev DNS queries/16h report:

1. tools/osv_check.py: check_package_for_malware() hit OSV on EVERY
   call. MCP reconnect ladders, stdio recycles, and parked-server
   self-probes re-run the preflight for the same package on every spawn
   attempt, so a flapping server became a sustained OSV query/DNS
   stream. Verdicts (clean or blocked) are now cached for 1h
   (OSV_CHECK_CACHE_TTL to tune); network failures stay uncached so
   fail-open never masks a real advisory once connectivity returns.

2. hermes_cli/security_audit.py: cmd_security_audit() ran full
   component discovery twice per audit (_count_components + run_audit).
   Discovery now runs once via _discover_components() and run_audit()
   accepts the pre-discovered list.

Both regression tests fail against the previous code (verified via
sabotage run).
2026-08-01 10:47:20 -07:00
ajzrva-sys 34c11fa689 fix(terminal): honor explicit config keys over stale env
Let terminal keys explicitly present in config.yaml override matching stale TERMINAL_* values while preserving environment values for omitted keys. Merged defaults remain backfill-only.

Exercise the real config.yaml to _get_env_config path for backend selection, partial terminal sections, matching-key overrides, environment fallback, one-shot bridging, and config read failures.

Closes #71137
2026-08-01 15:03:42 +05:30
Eugeniusz Gilewski 64dd865912 fix(deps): repair Google transitive security floors (#72108)
Google API and authentication packages permit vulnerable httplib2 and pyasn1
transitives, while the Workspace and Google Chat runtime installers previously
treated any importable version as sufficient. Existing environments could
therefore remain vulnerable after the project dependency pins were repaired.

Carry the fixed versions through the Google and Vertex extras, lazy feature
requirements, lockfile, and both runtime installers. Route the documented
Google Chat installation path through its maintained secure requirements
instead of an unconstrained direct pip command.

Detect stale distributions, install only unsatisfied requirements, and verify
the result before continuing. Behavioral tests cover those repair invariants
without freezing manifests, lockfiles, or complete package sets.

Related #72108
Extracted from #72840
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-31 23:18:38 -07:00