mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(approval): resolve Hermes home at detection time, not import time
helix4u's fix snapshotted the resolved HERMES_HOME into the static config/env patterns at module-import time. That breaks when HERMES_HOME is set after tools.approval is imported (the hermetic test conftest, any deferred-profile-resolution path), and made the PR's own 4 new tests red. Move the resolution into _normalize_command_for_detection(): rewrite the live resolved absolute home prefix (and its symlink-resolved form) to the canonical ~/.hermes/ form before pattern matching. Tracks the live env, needs no regex recompile, and folds the absolute form into the shared _SENSITIVE_WRITE_TARGET so > redirects, tee, cp, etc. are covered too — not just sed/perl/ruby in-place edits.
This commit is contained in:
parent
b0efe1d64b
commit
89d380261d
1 changed files with 48 additions and 23 deletions
|
|
@ -152,30 +152,15 @@ def _is_gateway_approval_context() -> bool:
|
|||
|
||||
# Sensitive write targets that should trigger approval even when referenced
|
||||
# via shell expansions like $HOME or $HERMES_HOME, or by the resolved absolute
|
||||
# active profile home path such as /home/hermes/.hermes/config.yaml.
|
||||
# active profile home path such as /home/hermes/.hermes/config.yaml. The
|
||||
# resolved-absolute form is folded into the ~/.hermes/ patterns at detection
|
||||
# time by _normalize_command_for_detection() — see the rewrite step there — so
|
||||
# these static patterns stay free of any import-time path snapshot (which would
|
||||
# go stale when HERMES_HOME is set after this module is imported, e.g. under the
|
||||
# hermetic test conftest or any deferred-profile-resolution path).
|
||||
_SSH_SENSITIVE_PATH = r'(?:~|\$home|\$\{home\})/\.ssh(?:/|$)'
|
||||
|
||||
|
||||
def _resolved_hermes_home_path_pattern() -> str:
|
||||
try:
|
||||
from hermes_constants import get_hermes_home
|
||||
home = get_hermes_home().expanduser()
|
||||
candidates = [
|
||||
str(home).rstrip("/"),
|
||||
str(home.resolve(strict=False)).rstrip("/"),
|
||||
]
|
||||
except Exception:
|
||||
candidates = []
|
||||
escaped = [re.escape(path) for path in dict.fromkeys(candidates) if path]
|
||||
if not escaped:
|
||||
return r"(?!)"
|
||||
return r"(?:" + "|".join(escaped) + r")/"
|
||||
|
||||
|
||||
_RESOLVED_HERMES_HOME_PATH = _resolved_hermes_home_path_pattern()
|
||||
_HERMES_ENV_PATH = (
|
||||
r'(?:~\/\.hermes/|'
|
||||
rf'{_RESOLVED_HERMES_HOME_PATH}|'
|
||||
r'(?:\$home|\$\{home\})/\.hermes/|'
|
||||
r'(?:\$hermes_home|\$\{hermes_home\})/)'
|
||||
r'\.env\b'
|
||||
|
|
@ -190,7 +175,6 @@ _HERMES_ENV_PATH = (
|
|||
# well as ~/.hermes/.
|
||||
_HERMES_CONFIG_PATH = (
|
||||
r'(?:~\/\.hermes/|'
|
||||
rf'{_RESOLVED_HERMES_HOME_PATH}|'
|
||||
r'(?:\$home|\$\{home\})/\.hermes/|'
|
||||
r'(?:\$hermes_home|\$\{hermes_home\})/)'
|
||||
r'config\.yaml\b'
|
||||
|
|
@ -561,8 +545,49 @@ def _normalize_command_for_detection(command: str) -> str:
|
|||
command = unicodedata.normalize('NFKC', command)
|
||||
# Strip shell backslash-escapes: r\m → rm. Prevents \-injection bypass.
|
||||
command = re.sub(r'\\([^\n])', r'\1', command)
|
||||
# Strip empty-string literals that split tokens: r''m → rm, r""m → rm.
|
||||
# Strip empty-string literals that split tokens: r''m → rm, r"\"m → rm.
|
||||
command = re.sub(r"''|\"\"", '', command)
|
||||
# Fold the resolved absolute active-profile home path into the canonical
|
||||
# ~/.hermes/ form so the Hermes config/env patterns catch it. In Docker and
|
||||
# gateway deployments the agent often references the resolved absolute path
|
||||
# directly (e.g. `sed -i ... /home/hermes/.hermes/config.yaml`) rather than
|
||||
# ~, $HOME, or $HERMES_HOME. Done at detection time (not via an import-time
|
||||
# pattern snapshot) so it tracks the live HERMES_HOME even when that is set
|
||||
# after this module is imported — as the hermetic test conftest does.
|
||||
command = _rewrite_resolved_hermes_home(command)
|
||||
return command
|
||||
|
||||
|
||||
def _rewrite_resolved_hermes_home(command: str) -> str:
|
||||
"""Rewrite the resolved absolute Hermes home prefix to ``~/.hermes/``.
|
||||
|
||||
Resolves the active ``HERMES_HOME`` at call time (and its symlink-resolved
|
||||
form) and replaces an occurrence of ``<home>/`` in *command* with
|
||||
``~/.hermes/`` so the static ``_HERMES_CONFIG_PATH`` / ``_HERMES_ENV_PATH``
|
||||
patterns match. No-op when the path can't be resolved or doesn't appear.
|
||||
"""
|
||||
try:
|
||||
from hermes_constants import get_hermes_home
|
||||
home = get_hermes_home().expanduser()
|
||||
candidates = [
|
||||
str(home).rstrip("/"),
|
||||
str(home.resolve(strict=False)).rstrip("/"),
|
||||
]
|
||||
except Exception:
|
||||
return command
|
||||
seen: set[str] = set()
|
||||
for path in candidates:
|
||||
if not path or path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
# Guard against a degenerate HERMES_HOME (e.g. "/" or "") rewriting
|
||||
# unrelated paths: require an absolute path with at least one non-root
|
||||
# component. The active profile home is always a real directory like
|
||||
# /home/hermes/.hermes or a per-test tempdir, never a bare root.
|
||||
normalized = path.rstrip("/")
|
||||
if not normalized.startswith("/") or normalized.count("/") < 2:
|
||||
continue
|
||||
command = command.replace(normalized + "/", "~/.hermes/")
|
||||
return command
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue