20 Commits

Author SHA1 Message Date
teknium1 818d781c0b test(tools): trim the salvaged #110632 / #110307 tests to two invariants each
The file-safety salvage carried five tests and the execute_code salvage four;
fold them into the two behaviour contracts per fix: (a) the named-profile scope
exempts the root's direct files and the write lands with no prompt; a child env
sees the ACTIVE profile's home per turn; (b) negatives hold: a checkout's
.hermes/config.yaml stays gated fail-closed, a lookalike profiles/ tree exempts
nothing; a dedicated process with no override is untouched. Both red on base.
Also compress the _hermes_exempt_homes docstring (WHY only; cite #110630).
2026-09-14 16:13:51 -07:00
moxian ab78e71a9e fix(file-safety): exempt the Hermes ROOT, not just the profile home
Under a named profile (``hermes -p <name>``, i.e. ``HERMES_HOME=<root>/profiles/<name>``)
the protected-instruction gate exempted only the profile dir, so the ROOT's DIRECT
files (LEDGER.md / MEMORY.md / USER.md / SOUL.md / AGENTS.md / DECISIONS.md ...) fell
through to the ``.hermes`` component rule and were treated as project-local
``<repo>/.hermes/config.yaml``. That gate always-asks and fails closed without a human
channel, so every write there was refused headless — it blocked #54 (LEDGER.md edit
could not land). Subdirectories (scripts/, cron/, data/) were unaffected, which is why
#55 sailed through and #54 did not.

``_hermes_exempt_homes()`` now returns the active profile home plus the Hermes ROOT
when that home really is a named profile (``named_profile_home`` validates: ``.hermes``
name, home markers, tombstone, or the resolved default root), so a coincidental
``profiles/`` directory never exempts its parent. ``_get_real_hermes_home()`` keeps its
per-profile semantics — the #107327 multiplex regression test pins that — and the
exemption consumes the new helper instead.

Refs: https://gitlab.yx.netease.com/agent-projects/platforms/agent-mentor/-/issues/60
2026-09-14 16:13:51 -07:00
Teknium 72cc96578a test: trim salvaged multiplex tests to the invariant pair per fix 2026-09-11 15:29:15 -07:00
PRATHAMESH75 7af5006b24 fix(file-safety): bind the write-guard resolver fallback to the active profile
The per-call home/config getters fell back to `_expand_tilde("~/.hermes...")`
when the primary resolver raised. `_expand_tilde` follows the subprocess-HOME
contract, which under host `auto` mode can be the real/default user home rather
than the active multiplex `HERMES_HOME`. So on the exception path the guards
recreated the very cross-profile authority bug the happy path fixed: beta's
`config.yaml` was compared against the default/root config (hard-block fails
open), and the protected-instruction exemption resolved against the wrong home.

Re-derive both fallbacks from the same `get_hermes_home()` key the happy path
uses (`Path(home)/config.yaml`, `realpath(home)`), and substitute no unrelated
home if the active security path cannot be established — a `None` fails closed
at the protected-instruction consumer (exemption skipped, gate runs).

Adds opposite-side regressions: forcing the primary config resolver to raise
keeps beta's own config refused (and does not spuriously protect alpha's under
beta's scope); forcing the primary home resolver to raise keeps beta's
instruction-file exemption resolved against beta.

Addresses the fallback-authority review on #107335 (thanks @andrexibiza).

(cherry picked from commit 119d88b46f745ba081f12adf3f6ebba457d95d68)
2026-09-11 15:29:15 -07:00
PRATHAMESH75 1271622e4b fix(file-safety): resolve HERMES_HOME/config per call so multiplex profiles don't poison the write guards (#107327)
In a multiplexed gateway (`gateway.multiplex_profiles: true`) each profile turn
scopes `HERMES_HOME` through a per-turn contextvar. But
`tools/file_tools_write_guards.py` memoised the resolved home and config path in
process-global module state, filled once by whichever profile ran first. Both
the protected agent-instruction approval gate (`_get_real_hermes_home` →
exemption for a profile's own home) and the `config.yaml` hard-block
(`_get_hermes_config_resolved`) therefore became order-dependent: a later
profile's own `workspace/AGENTS.md` was gated against a *sibling* profile's home,
and — worse — its own `config.yaml` stopped matching the block, so a
prompt-injected agent could rewrite the very file the block exists to protect
(reproduced end-to-end in #107327).

Resolve both values per call instead. `get_hermes_home()` / `get_config_path()`
are contextvar-scoped, so the getters now track the active profile; the guard
already pays a `realpath` per call, so the extra cost is negligible. The two
module slots are kept purely as a test-override surface (set the slot + its
`_loaded` flag to pin a value); production leaves them unset and resolves live,
which also removes the cross-test poisoning the process memo could cause.

Adds regression coverage: both getters track the active profile after a prior
profile's scope, and the `config.yaml` hard-block fires for beta's own config
even after an alpha turn ran first.

(cherry picked from commit 36b257391da497ac31e6c440727dc57ecd584e71)
2026-09-11 15:29:15 -07:00
Teknium 707161e77b simplify(compat): file_tools/send_message_tool/cronjob_tools/process_registry — drop 34 re-exports, repoint 5 callers + 27 test files 2026-09-03 13:30:27 -07:00
Teknium b818085298 simplify(compat): doctor/status — drop 13 re-exports + the doctor_* globals() facade (97 names), repoint 6 callers / 13 tests 2026-09-03 13:10:52 -07:00
Teknium 14a64ea985 refactor(tools): repoint tests to moved file_tools guard/path symbols 2026-09-02 14:43:45 -07:00
Brooklyn Nicholson 165d1849e2 fix(approval): stop the CLI and ACP offering a scope the protected gate discards
The protected agent-instruction gate grants one operation and persists
nothing, but only the TUI/desktop and Runs transports were taught that.
The prompt_toolkit panel, the input() fallback, and the ACP editor menu
still rendered "Allow for session", so a user editing SOUL.md tapped it,
got re-prompted on the next write, and read the gate as broken.

Thread allow_session through prompt_dangerous_approval so a caller that
re-asks every time collapses every surface to once/deny, and cover the
producer-to-transport contract end to end.
2026-08-23 17:45:47 -05: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
Teknium fe66596df3 feat(security): protected agent-instruction files always require write approval
write_file/patch targeting AGENTS.md, CLAUDE.md, SOUL.md, .cursorrules, or a
project-local .hermes config dir now ALWAYS prompt the human for approval —
even under --yolo/auto-approve — and fail closed when no human channel
exists. These files steer future agent behavior, so an injected write to
them is a prompt-injection persistence vector.

Design:
- New _check_protected_instruction_write() in tools/file_tools.py, a
  sibling of _check_sensitive_path that returns approval-required rather
  than a hard error. It realpaths before matching (symlink lesson from
  #41351), matches basenames case-insensitively in ANY directory, rejects
  './x/../AGENTS.md' traversal via normpath, and gates files whose
  immediate parent dir is `.hermes` (project-local config) while exempting
  the authoritative ~/.hermes home (governed by its own guards).
- Approval is ONE-OPERATION only: no session/permanent persistence, no
  yolo bypass — intentionally does not route through _run_approval_gate.
  Gateway sessions get the button round-trip with allow_permanent and
  allow_session both False; CLI uses the per-thread approval callback;
  no channel at all = BLOCKED (fail closed).
- Multi-file V4A patches: ONE protected file gates the ENTIRE patch (a
  single prompt lists all protected targets; deny applies nothing).
- Config: security.protected_instruction_files (default true) and
  security.protected_instruction_extra_patterns (fnmatch on basename).
  Config read failure keeps the gate ON.

Tests: 22 new cases in tests/tools/test_file_write_safety.py covering the
adversarial checklist — deny/approve/yolo-bypass attempt, symlink at a
protected target, case variants, relative traversal, arbitrary-directory
basenames, project-local .hermes, checkout-nested-under-~/.hermes
non-gating, patch replace + V4A multi-file atomicity, gateway round-trip,
fail-closed with no human, config off/extra patterns.

Ported from: RooCodeInc/Roo-Code RooProtectedController (Apache-2.0);
companion: #58631 (terminal vector), symlink lesson from #41351.
2026-08-07 08:58:38 -07:00
kshitijk4poor 8c19e29259 refactor(file-ops): fold simplify-pass findings
- write_file: encode content once, share bytes between bytes_written and
  the sha256 verification (drops a second full-content encode per write)
- patch_parser: replace the except-TypeError retry around
  write_file(pre_content=...) with signature-based feature detection so a
  TypeError raised inside a capable implementation propagates instead of
  triggering a duplicate write; tests for both duck-typing contracts
- tests: real-ops V4A BOM round-trip + _file_has_bom disk-probe guard
  (the teknium1-review regression previously only covered by a fake)
- comment: document dirs_created's long-standing "parent ensured" meaning
2026-08-04 14:34:24 +05:30
Teknium 39975613b1 test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00
kshitijk4poor 320e886f3d test(file-safety): add integration tests for safe-root denial messages
Exercises the actual ShellFileOperations.write_file and patch_replace
code paths (not just the helper in isolation) to verify that
safe-root denials surface 'outside HERMES_WRITE_SAFE_ROOT' and
credential-path denials surface 'protected system/credential file'.

Adapted from PR #55615 by @liuhao1024.
2026-07-14 17:09:40 +05:30
HexLab98 55d826ccef fix(file-safety): distinguish safe-root write denial from credential blocks
Return actionable errors when HERMES_WRITE_SAFE_ROOT blocks a path instead of
labeling every denial as a protected credential file. Wire the helper through
write_file, patch, delete/move, and the Copilot ACP shim; sync docs examples.
2026-07-14 17:09:40 +05:30
kshitijk4poor cdb1dfbc49 fix: use os.pathsep, add tests, update tips for multi-root support
- Use os.pathsep instead of literal ':' so Windows paths (C:\dir) and
  the Windows separator ';' work correctly.
- Add 9 tests covering multi-root behavior: writes inside first/second
  root, writes outside all roots, trailing/leading/double separators,
  all-separators edge case, static deny priority, duplicate dedup.
- Update hermes_cli/tips.py tip string to mention multiple paths.
- Update docs to mention os.pathsep / ; on Windows.

Follow-up for salvaged PR #49557.
2026-06-27 04:01:12 +05:30
Teknium 5f84c9144a fix(file-tools): handle UTF-8 BOM in read_file / write_file / patch (#35278)
Some Windows editors prepend an invisible UTF-8 BOM (U+FEFF) to text
files. We had no awareness of it, so: read_file surfaced a phantom
U+FEFF as the first character; patch matches against the true first
line could miss; and a write/patch round-trip silently stripped the
marker, changing the file's byte signature.

Now:
- read_file / read_file_raw strip a single leading BOM so the model
  never sees it (only on the first chunk — the marker lives at byte 0).
- patch_replace strips the BOM before fuzzy-matching (so an exact
  first-line match works) and its post-write verification compares
  BOM-stripped content.
- write_file restores the BOM when the original file had one and the
  new content doesn't, mirroring the existing line-ending preservation
  (detect on disk via a cheap `head -c 3` probe or reuse pre_content,
  re-prepend across the edit). Guards against double-BOM.

Mid-content U+FEFF is left alone (it's data there, not a file marker).

Tests: TestBomHandling (real LocalEnvironment) — read-strips, raw-read
strips, write preserves, no-BOM-when-original-had-none, no-double-BOM,
patch round-trip preserves, patch matches first line through a BOM,
plus helper unit tests. 208 file-tool tests green.
2026-05-30 06:25:50 -07:00
Teknium 39f6b6e9d2 fix(file-tools): make write_file/patch atomic (temp-file + rename) (#35252)
* Inspired by Claude Code: /compress here [N] — boundary-aware 'summarize up to here'

Adds a user-chosen compression boundary to the existing /compress command.
/compress here [N] summarizes everything except the most recent N exchanges
(default 2), which are preserved verbatim — letting the user pick the
compression boundary instead of relying on the automatic token-budget heuristic.

Inspired by Claude Code's Rewind 'Summarize up to here' action (v2.1.139,
Week 20, May 2026): https://code.claude.com/docs/en/whats-new/2026-w20

- hermes_cli/partial_compress.py: pure split/parse helpers + seam-alternation
  guard (shared by CLI and gateway).
- cli.py / gateway/run.py: route 'here [N]' / '--keep N' to partial compression;
  compress only the head, re-append the verbatim tail through the seam guard.
- Preserves message-flow role alternation (seam guard merges any illegal
  user->user / assistant->assistant adjacency).
- Reuses the existing _compress_context session-rotation/lock machinery — no
  changes to the compression core.
- Bare /compress (full) and /compress <focus> behavior unchanged.

Tests: 12 helper unit tests + 5 CLI integration tests + E2E (interleaved
tool-call transcript, degenerate/multimodal seams, real handler path).

* fix(file-tools): make write_file/patch atomic (temp-file + rename)

write_file streamed content straight into the target via `cat > path`, so
a crash, SIGKILL, or truncated pipe mid-write left the file half-written
and corrupt. patch_replace routes through write_file, so it shared the flaw.

Now writes stream into a temp file in the SAME directory and `mv` it over
the target — a real same-filesystem rename, which is atomic on POSIX and on
every terminal backend (local/docker/ssh/modal). A failed write leaves the
original byte-intact and leaks no temp file. The existing file's mode is
preserved across the swap (stat + chmod, GNU/BSD), and content still rides
stdin so there's no ARG_MAX limit. A trap cleans the temp on any error path.

Tests: added TestAtomicWrite (real LocalEnvironment, no mocks) covering
inode-change-on-overwrite, mode preservation, failed-write-leaves-original,
no-temp-leak, special chars, and patch routing. Updated two mocks in
test_file_operations.py that keyed on the literal `cat >` write command to
key on the stdin_data behavioral signal instead. 200 file-tool tests green.
2026-05-30 02:07:50 -07:00
konsisumer 311dac1971 fix(file_tools): block /private/etc writes on macOS symlink bypass
On macOS, /etc is a symlink to /private/etc, so os.path.realpath()
resolves /etc/hosts to /private/etc/hosts. The sensitive path check
only matched /etc/ prefixes against the resolved path, allowing
writes to system files on macOS.

- Add /private/etc/ and /private/var/ to _SENSITIVE_PATH_PREFIXES
- Check both realpath-resolved and normpath-normalized paths
- Add regression tests for macOS symlink bypass

Closes #8734
Co-authored-by: ElhamDevelopmentStudio (PR #8829)
2026-04-13 05:15:05 -07:00
Teknium 2c7c30be69 fix(security): harden terminal safety and sandbox file writes (#1653)
* fix(security): harden terminal safety and sandbox file writes

Two security improvements:

1. Dangerous command detection: expand shell -c pattern to catch
   combined flags (bash -lc, bash -ic, ksh -c) that were previously
   undetected. Pattern changed from matching only 'bash -c' to
   matching any shell invocation with -c anywhere in the flags.

2. File write sandboxing: add HERMES_WRITE_SAFE_ROOT env var that
   constrains all write_file/patch operations to a configured directory
   tree. Opt-in — when unset, behavior is unchanged. Useful for
   gateway/messaging deployments that should only touch a workspace.

Based on PR #1085 by ismoilh.

* fix: correct "POSIDEON" typo to "POSEIDON" in banner ASCII art

The poseidon skin's banner_logo had the E and I letters swapped,
spelling "POSIDEON-AGENT" instead of "POSEIDON-AGENT".

---------

Co-authored-by: ismoilh <ismoilh@users.noreply.github.com>
Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
2026-03-17 02:22:12 -07:00