fix(file-safety): add sandbox-mirror soft guard for writes to per-task .hermes mirrors (#32213)
#32049 reports that under terminal.backend: docker, write_file / patch calls to authoritative profile state (SOUL.md, memories, etc.) land on the sandbox-local mirror at ``<HERMES_HOME>/profiles/<name>/sandboxes/<backend>/<task>/home/.hermes/...`` — a path the host Hermes process never reads. The tool reports success, the user sees no behavior change, and on disk two divergent copies of SOUL.md (or any other profile file) accumulate. The existing classify_cross_profile_target guard does not catch this: its parts[2] check sees "sandboxes" and returns None, and the path is in-profile from the inner-mirror perspective so even a fixed version would not fire. Add a parallel sandbox-mirror classifier in agent/file_safety: * classify_sandbox_mirror_target() detects the ``…/sandboxes/<backend>/<task>/home/.hermes/…`` shape via path parts. Detection is path-shape only — backend-agnostic, does not require the file to exist, and works regardless of which HERMES_HOME resolves. * get_sandbox_mirror_warning() returns a model-facing warning that names the mirror root and the inner authoritative path the agent likely meant. Wire both detectors through tools/file_tools._check_cross_profile_path so the existing write_file and v4a patch call sites pick up the new guard with no API change. The bypass kwarg (``cross_profile=True``) remains shared between the two guards — same "I know what I'm doing" escape valve after explicit user direction. This is the defense-in-depth piece of the proposal in #32049 ("any …/sandboxes/<backend>/…/home/…hermes/… path as sandbox-mirror"). It catches the host-side speculation case where the agent writes a literal sandbox-mirror path. The inner-container case (where the bind mount strips the ``sandboxes/`` prefix from the agent's path view) is out of scope for this surgical change — that requires either a dispatch-layer host-side check before the container handoff, or the host-side ``profile_state`` / ``soul`` tool the issue also proposes. Soft guard, NOT a security boundary — matches the existing classify_cross_profile_target contract. Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> Co-authored-by: Ben Barclay <ben@nousresearch.com>
This commit is contained in:
@ -290,20 +290,32 @@ def _check_sensitive_path(filepath: str, task_id: str = "default") -> str | None
|
||||
|
||||
|
||||
def _check_cross_profile_path(filepath: str, task_id: str = "default") -> str | None:
|
||||
"""Return a cross-profile warning string when ``filepath`` lands in
|
||||
another Hermes profile's skills/plugins/cron/memories directory.
|
||||
"""Return a soft-guard warning when ``filepath`` lands in another Hermes
|
||||
profile's scoped area or a sandbox-mirror of authoritative profile state.
|
||||
|
||||
Returns ``None`` when the write is in-scope (same profile) or outside
|
||||
Hermes scope entirely. Soft guard — the agent can override by passing
|
||||
``cross_profile=True`` to its write tool after explicit user direction.
|
||||
Two detectors run in order:
|
||||
|
||||
Defense-in-depth, NOT a security boundary — the terminal tool runs
|
||||
as the same OS user and can write any of these paths directly.
|
||||
See ``agent/file_safety.classify_cross_profile_target`` for the
|
||||
detection rules.
|
||||
* cross-profile (#TBD) — writes that hit another profile's
|
||||
``skills/plugins/cron/memories`` directory.
|
||||
* sandbox-mirror (#32049) — writes that hit the
|
||||
``…/sandboxes/<backend>/<task>/home/.hermes/…`` mirror created by a
|
||||
non-local terminal backend (Docker, Daytona, etc.), where the host
|
||||
Hermes process never reads the mirror and the authoritative file is
|
||||
left untouched.
|
||||
|
||||
Returns ``None`` when the write is in-scope or outside Hermes scope.
|
||||
Both detectors are soft guards — the agent can override either by
|
||||
passing ``cross_profile=True`` to its write tool after explicit user
|
||||
direction. Defense-in-depth, NOT a security boundary — the terminal
|
||||
tool runs as the same OS user and can write any of these paths
|
||||
directly. See ``agent/file_safety.classify_cross_profile_target`` and
|
||||
``classify_sandbox_mirror_target`` for the detection rules.
|
||||
"""
|
||||
try:
|
||||
from agent.file_safety import get_cross_profile_warning
|
||||
from agent.file_safety import (
|
||||
get_cross_profile_warning,
|
||||
get_sandbox_mirror_warning,
|
||||
)
|
||||
except Exception:
|
||||
# Fail open on import error — the existing sensitive-path guard
|
||||
# plus the write_denied list still apply.
|
||||
@ -317,7 +329,10 @@ def _check_cross_profile_path(filepath: str, task_id: str = "default") -> str |
|
||||
except (OSError, ValueError):
|
||||
resolved = filepath
|
||||
|
||||
return get_cross_profile_warning(resolved)
|
||||
warning = get_cross_profile_warning(resolved)
|
||||
if warning is not None:
|
||||
return warning
|
||||
return get_sandbox_mirror_warning(resolved)
|
||||
|
||||
|
||||
def _is_expected_write_exception(exc: Exception) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user