From 3130d95cc5027003e129934efc72bc9f6b4f91a3 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Wed, 10 Jun 2026 21:15:30 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20sandbox=20primitives=20=E2=80=94=20Bind?= =?UTF-8?q?,=20SandboxPolicy,=20is=5Favailable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core sandbox infrastructure for isolating harness sessions. Bind defines read-only/writable path bindings, SandboxPolicy specifies filesystem and network isolation modes, and is_available() is the single source of truth for bubblewrap availability across the application. Co-Authored-By: Claude Opus 4.8 --- src/hqt/sandbox.py | 33 +++++++++++++++++++++++++++++++++ tests/test_sandbox.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/hqt/sandbox.py create mode 100644 tests/test_sandbox.py diff --git a/src/hqt/sandbox.py b/src/hqt/sandbox.py new file mode 100644 index 0000000..f1d5219 --- /dev/null +++ b/src/hqt/sandbox.py @@ -0,0 +1,33 @@ +import os # noqa: F401 +import shutil +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Bind: + """A host path to expose inside the sandbox (read-only unless writable).""" + + src: Path + writable: bool = False + + +@dataclass(frozen=True) +class SandboxPolicy: + """Per-session sandbox settings. + + fs: "rw" binds the project dir writable; "ro" binds it read-only. + net: True shares the host network namespace; False = no connectivity. + """ + + fs: str + net: bool + + +def is_available() -> bool: + """True when bubblewrap can be used: Linux with `bwrap` on PATH. + + The single source of truth for both `hqt doctor` and the New Session dialog. + """ + return sys.platform.startswith("linux") and shutil.which("bwrap") is not None diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py new file mode 100644 index 0000000..52884f5 --- /dev/null +++ b/tests/test_sandbox.py @@ -0,0 +1,34 @@ +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