fix(ssh): keep bulk sync extraction scoped to .hermes

This commit is contained in:
Stark-X 2026-05-11 16:39:52 +08:00 committed by Teknium
parent 4a2fa77c15
commit eb51fb6f50
2 changed files with 47 additions and 8 deletions

View file

@ -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"

View file

@ -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