mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(ssh): keep bulk sync extraction scoped to .hermes
This commit is contained in:
parent
4a2fa77c15
commit
eb51fb6f50
2 changed files with 47 additions and 8 deletions
|
|
@ -91,7 +91,7 @@ class TestSSHBulkUpload:
|
|||
assert "/home/testuser/.hermes/credentials" in mkdir_str
|
||||
|
||||
def test_staging_symlinks_mirror_remote_layout(self, mock_env, tmp_path):
|
||||
"""Symlinks in staging dir should mirror the remote path structure."""
|
||||
"""Symlinks in staging dir should mirror the .hermes-relative layout."""
|
||||
f1 = tmp_path / "local_a.txt"
|
||||
f1.write_text("content a")
|
||||
|
||||
|
|
@ -107,9 +107,7 @@ class TestSSHBulkUpload:
|
|||
c_idx = cmd.index("-C")
|
||||
staging_dir = cmd[c_idx + 1]
|
||||
# Check the symlink exists
|
||||
expected = os.path.join(
|
||||
staging_dir, "home/testuser/.hermes/skills/my_skill.md"
|
||||
)
|
||||
expected = os.path.join(staging_dir, "skills/my_skill.md")
|
||||
staging_paths.append(expected)
|
||||
assert os.path.islink(expected), f"Expected symlink at {expected}"
|
||||
assert os.readlink(expected) == os.path.abspath(str(f1))
|
||||
|
|
@ -166,14 +164,42 @@ class TestSSHBulkUpload:
|
|||
assert "-" in tar_cmd # stdout
|
||||
assert "-C" in tar_cmd
|
||||
|
||||
# ssh: extract from stdin at /, preserving existing dir modes (#17767)
|
||||
# ssh: extract from stdin at ~/.hermes, preserving existing dir modes (#17767)
|
||||
ssh_str = " ".join(ssh_cmd)
|
||||
assert "ssh" in ssh_str
|
||||
assert "tar xf -" in ssh_str
|
||||
assert "--no-overwrite-dir" in ssh_str
|
||||
assert "-C /" in ssh_str
|
||||
assert "-C /home/testuser/.hermes" in ssh_str
|
||||
assert "testuser@example.com" in ssh_str
|
||||
|
||||
def test_bulk_upload_never_stages_remote_home_prefix(self, mock_env, tmp_path):
|
||||
"""Regression: do not archive /home/<user> path components."""
|
||||
f1 = tmp_path / "nested.txt"
|
||||
f1.write_text("nested")
|
||||
files = [(str(f1), "/home/testuser/.hermes/cache/nested.txt")]
|
||||
|
||||
def capture_tar_cmd(cmd, **kwargs):
|
||||
if cmd[0] == "tar":
|
||||
c_idx = cmd.index("-C")
|
||||
staging_dir = cmd[c_idx + 1]
|
||||
assert not os.path.exists(os.path.join(staging_dir, "home"))
|
||||
expected = os.path.join(staging_dir, "cache/nested.txt")
|
||||
assert os.path.islink(expected)
|
||||
|
||||
mock = MagicMock()
|
||||
mock.stdout = MagicMock()
|
||||
mock.returncode = 0
|
||||
mock.poll.return_value = 0
|
||||
mock.communicate.return_value = (b"", b"")
|
||||
mock.stderr = MagicMock()
|
||||
mock.stderr.read.return_value = b""
|
||||
return mock
|
||||
|
||||
with patch.object(subprocess, "run",
|
||||
return_value=subprocess.CompletedProcess([], 0)), \
|
||||
patch.object(subprocess, "Popen", side_effect=capture_tar_cmd):
|
||||
mock_env._ssh_bulk_upload(files)
|
||||
|
||||
def test_mkdir_failure_raises(self, mock_env, tmp_path):
|
||||
"""mkdir failure should raise RuntimeError before tar pipe."""
|
||||
f1 = tmp_path / "y.txt"
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class SSHEnvironment(BaseEnvironment):
|
|||
if not files:
|
||||
return
|
||||
|
||||
base = f"{self._remote_home}/.hermes"
|
||||
parents = unique_parent_dirs(files)
|
||||
if parents:
|
||||
cmd = self._build_ssh_command()
|
||||
|
|
@ -180,7 +181,19 @@ class SSHEnvironment(BaseEnvironment):
|
|||
# Symlink staging avoids fragile GNU tar --transform rules.
|
||||
with tempfile.TemporaryDirectory(prefix="hermes-ssh-bulk-") as staging:
|
||||
for host_path, remote_path in files:
|
||||
staged = os.path.join(staging, remote_path.lstrip("/"))
|
||||
try:
|
||||
rel_remote = os.path.relpath(remote_path, base)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
f"remote path {remote_path!r} is not under sync base {base!r}"
|
||||
) from exc
|
||||
|
||||
if rel_remote == "." or rel_remote.startswith("../"):
|
||||
raise RuntimeError(
|
||||
f"remote path {remote_path!r} escapes sync base {base!r}"
|
||||
)
|
||||
|
||||
staged = os.path.join(staging, rel_remote)
|
||||
os.makedirs(os.path.dirname(staged), exist_ok=True)
|
||||
os.symlink(os.path.abspath(host_path), staged)
|
||||
|
||||
|
|
@ -190,7 +203,7 @@ class SSHEnvironment(BaseEnvironment):
|
|||
# existing directories (e.g. /home/<user>) with the staging
|
||||
# directory's mode. Without this, a umask 002 produces 0775
|
||||
# dirs which breaks sshd StrictModes (refuses authorized_keys).
|
||||
ssh_cmd.append("tar xf - --no-overwrite-dir -C /")
|
||||
ssh_cmd.append(f"tar xf - --no-overwrite-dir -C {shlex.quote(base)}")
|
||||
|
||||
tar_proc = subprocess.Popen(
|
||||
tar_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue