Hermes-agent
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""Tests for the sandbox-mirror write guard in agent/file_safety.
|
||||
|
||||
The guard fires when a tool tries to write into the per-task mirror
|
||||
directory created by a non-local terminal backend (Docker, Daytona, etc.).
|
||||
Those paths look like ``…/sandboxes/<backend>/<task>/home/.hermes/…`` and
|
||||
they accumulate divergent copies of authoritative profile state (SOUL.md,
|
||||
config.yaml, memories/*.md) because the host Hermes process never reads
|
||||
them. Soft guard — defense in depth, NOT a security boundary.
|
||||
|
||||
Reference: #32049 — under ``terminal.backend: docker``, the agent's
|
||||
``write_file`` / ``patch`` calls landed on the sandbox mirror of SOUL.md
|
||||
while the host process kept loading the untouched authoritative file.
|
||||
The agent reported success; the rule never took effect.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# classify_sandbox_mirror_target — pure path-shape detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestClassifySandboxMirrorTarget:
|
||||
def test_docker_mirror_soul_md_classified(self, tmp_path):
|
||||
"""The exact path shape reported in #32049."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "profiles" / "group1"
|
||||
/ "sandboxes" / "docker" / "default" / "home" / ".hermes"
|
||||
/ "profiles" / "group1" / "SOUL.md"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("# mirror copy\n")
|
||||
|
||||
result = classify_sandbox_mirror_target(str(target))
|
||||
assert result is not None
|
||||
assert result["target_path"] == str(target.resolve())
|
||||
assert result["mirror_root"].endswith(
|
||||
"sandboxes/docker/default/home/.hermes"
|
||||
)
|
||||
assert result["inner_path"] == "profiles/group1/SOUL.md"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"backend,inner",
|
||||
[
|
||||
("docker", "profiles/coder/memories/MEMORY.md"),
|
||||
("daytona", "profiles/default/cron/jobs.json"),
|
||||
("podman", ".env"),
|
||||
],
|
||||
)
|
||||
def test_other_backends_and_inner_files_match(self, tmp_path, backend, inner):
|
||||
"""The detector is backend-agnostic — sandbox-mirror shape is what matters."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "sandboxes" / backend / "task-42" / "home" / ".hermes"
|
||||
/ Path(inner)
|
||||
)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text("x")
|
||||
|
||||
result = classify_sandbox_mirror_target(str(target))
|
||||
assert result is not None
|
||||
assert result["inner_path"] == inner
|
||||
assert backend in result["mirror_root"]
|
||||
|
||||
def test_path_outside_sandbox_returns_none(self, tmp_path):
|
||||
"""A plain Hermes path is not a mirror."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = tmp_path / ".hermes" / "profiles" / "group1" / "SOUL.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("# real SOUL\n")
|
||||
|
||||
assert classify_sandbox_mirror_target(str(target)) is None
|
||||
|
||||
def test_sandboxes_segment_without_home_hermes_returns_none(self, tmp_path):
|
||||
"""A ``sandboxes/`` directory unrelated to Hermes-state mirroring (e.g.
|
||||
the sandbox workspace itself) is not flagged."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "sandboxes" / "docker" / "task-42" / "workspace" / "main.py"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("print('hi')\n")
|
||||
|
||||
assert classify_sandbox_mirror_target(str(target)) is None
|
||||
|
||||
def test_sandboxes_segment_with_home_but_no_hermes_returns_none(self, tmp_path):
|
||||
"""``sandboxes/<backend>/<task>/home/anything-not-hermes`` is not a mirror."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "sandboxes" / "docker" / "task-42" / "home" / ".bashrc"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("alias ll='ls -la'\n")
|
||||
|
||||
assert classify_sandbox_mirror_target(str(target)) is None
|
||||
|
||||
def test_truncated_sandbox_path_returns_none(self, tmp_path):
|
||||
"""``…/sandboxes/<backend>/<task>`` without ``home/.hermes/<thing>`` is not a mirror."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = tmp_path / "sandboxes" / "docker" / "task-42"
|
||||
target.mkdir(parents=True)
|
||||
|
||||
assert classify_sandbox_mirror_target(str(target)) is None
|
||||
|
||||
def test_non_existent_path_still_classifies_by_shape(self, tmp_path):
|
||||
"""Detection is path-shape only — it must not require the file to exist
|
||||
(the agent is about to CREATE the mirror file, that's the bug)."""
|
||||
from agent.file_safety import classify_sandbox_mirror_target
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "profiles" / "group1"
|
||||
/ "sandboxes" / "docker" / "default" / "home" / ".hermes"
|
||||
/ "profiles" / "group1" / "SOUL.md"
|
||||
)
|
||||
# Parent directory exists so .resolve() doesn't strip the tail
|
||||
# under strict mode, but the file itself does NOT exist.
|
||||
target.parent.mkdir(parents=True)
|
||||
assert not target.exists()
|
||||
|
||||
result = classify_sandbox_mirror_target(str(target))
|
||||
assert result is not None
|
||||
assert result["inner_path"] == "profiles/group1/SOUL.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_sandbox_mirror_warning — the model-facing string
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetSandboxMirrorWarning:
|
||||
def test_non_mirror_returns_none(self, tmp_path):
|
||||
from agent.file_safety import get_sandbox_mirror_warning
|
||||
|
||||
target = tmp_path / ".hermes" / "profiles" / "group1" / "SOUL.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("# real SOUL\n")
|
||||
|
||||
assert get_sandbox_mirror_warning(str(target)) is None
|
||||
|
||||
def test_mirror_warning_names_mirror_root_and_inner_path(self, tmp_path):
|
||||
from agent.file_safety import get_sandbox_mirror_warning
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "profiles" / "group1"
|
||||
/ "sandboxes" / "docker" / "default" / "home" / ".hermes"
|
||||
/ "profiles" / "group1" / "SOUL.md"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("# mirror copy\n")
|
||||
|
||||
warn = get_sandbox_mirror_warning(str(target))
|
||||
assert warn is not None
|
||||
# Must name the mirror root so the user can locate the sandbox.
|
||||
assert "sandboxes/docker/default/home/.hermes" in warn
|
||||
# Must hint at what the agent likely meant.
|
||||
assert "profiles/group1/SOUL.md" in warn
|
||||
# Must name the bypass kwarg shared with the cross-profile guard.
|
||||
assert "cross_profile=True" in warn
|
||||
|
||||
def test_warning_is_defense_in_depth_not_boundary(self, tmp_path):
|
||||
from agent.file_safety import get_sandbox_mirror_warning
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "sandboxes" / "docker" / "t" / "home" / ".hermes"
|
||||
/ "profiles" / "g" / "SOUL.md"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("x")
|
||||
|
||||
warn = get_sandbox_mirror_warning(str(target))
|
||||
# Must self-document as defense-in-depth so future reviewers
|
||||
# don't promote it to a hard block (matches the existing
|
||||
# cross-profile guard's contract).
|
||||
assert "not a security boundary" in warn.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Independence from cross-profile classifier
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSandboxMirrorIsOrthogonalToCrossProfile:
|
||||
"""The sandbox-mirror guard must fire even when the inner path is
|
||||
in-profile from the host's view — the bug is the mirror, not the
|
||||
profile mismatch."""
|
||||
|
||||
def test_same_profile_mirror_still_flagged(self, tmp_path, monkeypatch):
|
||||
import agent.file_safety as fs
|
||||
monkeypatch.setattr(fs, "_hermes_root_path", lambda: tmp_path)
|
||||
monkeypatch.setattr(fs, "_hermes_home_path", lambda: tmp_path / "profiles" / "group1")
|
||||
|
||||
target = (
|
||||
tmp_path
|
||||
/ "profiles" / "group1"
|
||||
/ "sandboxes" / "docker" / "default" / "home" / ".hermes"
|
||||
/ "profiles" / "group1" / "SOUL.md"
|
||||
)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text("x")
|
||||
|
||||
# cross-profile classifier: active profile == target's inner-mirror
|
||||
# profile name; on the existing detector the path's parts[2] is
|
||||
# ``sandboxes``, not a scoped area, so it returns None.
|
||||
assert fs.classify_cross_profile_target(str(target)) is None
|
||||
# sandbox-mirror classifier: fires unconditionally on the shape.
|
||||
assert fs.classify_sandbox_mirror_target(str(target)) is not None
|
||||
Reference in New Issue
Block a user