5f29418186
Address three integration gaps found in review: - Bind ~/.gitconfig read-only in the sandbox base layer so `git commit` finds user.name/email inside the jail (spec §3). - Bind a worktree session's repo common .git dir writable: the worktree's real gitdir lives at <repo>/.git/worktrees/<branch>, outside the bound cwd, so git was broken in sandboxed worktree sessions. - doctor now gates on sandbox.is_available() (the single source of truth shared with the New Session dialog) rather than a raw shutil.which, so it agrees with the service on non-Linux platforms (spec §6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.0 KiB
Python
124 lines
4.0 KiB
Python
from pathlib import Path
|
|
|
|
from hqt import sandbox
|
|
from hqt.sandbox import Bind, SandboxPolicy
|
|
|
|
|
|
def test_bind_defaults_readonly():
|
|
b = Bind(Path("/home/u/.claude"))
|
|
assert b.src == Path("/home/u/.claude")
|
|
assert b.writable is False
|
|
|
|
|
|
def test_sandbox_policy_fields():
|
|
p = SandboxPolicy(fs="rw", net=True)
|
|
assert p.fs == "rw"
|
|
assert p.net is True
|
|
|
|
|
|
def test_is_available_true_on_linux_with_bwrap(monkeypatch):
|
|
monkeypatch.setattr(sandbox.sys, "platform", "linux")
|
|
monkeypatch.setattr(sandbox.shutil, "which", lambda name: "/usr/bin/bwrap")
|
|
assert sandbox.is_available() is True
|
|
|
|
|
|
def test_is_available_false_without_bwrap(monkeypatch):
|
|
monkeypatch.setattr(sandbox.sys, "platform", "linux")
|
|
monkeypatch.setattr(sandbox.shutil, "which", lambda name: None)
|
|
assert sandbox.is_available() is False
|
|
|
|
|
|
def test_is_available_false_off_linux(monkeypatch):
|
|
monkeypatch.setattr(sandbox.sys, "platform", "darwin")
|
|
monkeypatch.setattr(sandbox.shutil, "which", lambda name: "/usr/bin/bwrap")
|
|
assert sandbox.is_available() is False
|
|
|
|
|
|
def _split(args, flag):
|
|
"""Return list of (src, dst) pairs that follow each occurrence of flag."""
|
|
out = []
|
|
for i, a in enumerate(args):
|
|
if a == flag:
|
|
out.append((args[i + 1], args[i + 2]))
|
|
return out
|
|
|
|
|
|
def test_wrap_command_after_double_dash(tmp_path):
|
|
cmd = ["claude", "--session-id", "x"]
|
|
args = sandbox.wrap(cmd, tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert args[0] == "bwrap"
|
|
assert "--" in args
|
|
assert args[args.index("--") + 1 :] == cmd
|
|
|
|
|
|
def test_wrap_base_flags(tmp_path):
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert "--die-with-parent" in args
|
|
assert "--unshare-all" in args
|
|
assert "--proc" in args and "--dev" in args
|
|
|
|
|
|
def test_wrap_net_on_shares_net(tmp_path):
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert "--share-net" in args
|
|
|
|
|
|
def test_wrap_net_off_does_not_share(tmp_path):
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=False), [])
|
|
assert "--share-net" not in args
|
|
|
|
|
|
def test_wrap_cwd_rw_is_bind(tmp_path):
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert (str(tmp_path), str(tmp_path)) in _split(args, "--bind")
|
|
|
|
|
|
def test_wrap_cwd_ro_is_ro_bind(tmp_path):
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="ro", net=True), [])
|
|
assert (str(tmp_path), str(tmp_path)) in _split(args, "--ro-bind")
|
|
|
|
|
|
def test_wrap_writable_bind_spliced(tmp_path):
|
|
cred = tmp_path / "cred"
|
|
cred.mkdir()
|
|
args = sandbox.wrap(
|
|
["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(cred, writable=True)]
|
|
)
|
|
assert (str(cred), str(cred)) in _split(args, "--bind")
|
|
|
|
|
|
def test_wrap_readonly_bind_spliced(tmp_path):
|
|
cred = tmp_path / "cred"
|
|
cred.mkdir()
|
|
args = sandbox.wrap(
|
|
["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(cred)]
|
|
)
|
|
assert (str(cred), str(cred)) in _split(args, "--ro-bind")
|
|
|
|
|
|
def test_wrap_skips_missing_binds(tmp_path):
|
|
missing = tmp_path / "nope"
|
|
args = sandbox.wrap(
|
|
["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [Bind(missing)]
|
|
)
|
|
assert str(missing) not in args
|
|
|
|
|
|
def test_wrap_binds_gitconfig_readonly_when_present(tmp_path, monkeypatch):
|
|
"""~/.gitconfig is bound read-only so `git commit` works inside the jail."""
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
gitconfig = home / ".gitconfig"
|
|
gitconfig.write_text("[user]\n\tname = Test\n")
|
|
monkeypatch.setattr(sandbox.Path, "home", classmethod(lambda cls: home))
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert (str(gitconfig), str(gitconfig)) in _split(args, "--ro-bind")
|
|
|
|
|
|
def test_wrap_skips_gitconfig_when_absent(tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
monkeypatch.setattr(sandbox.Path, "home", classmethod(lambda cls: home))
|
|
args = sandbox.wrap(["sh"], tmp_path, SandboxPolicy(fs="rw", net=True), [])
|
|
assert str(home / ".gitconfig") not in args
|