diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 80a76cd..886ef04 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -105,9 +105,9 @@ | task | SAND-WP-0012-T04 | done | — | workplans/SAND-WP-0012-packer-orchestration.md | | task | SAND-WP-0012-T05 | done | — | workplans/SAND-WP-0012-packer-orchestration.md | | task | SAND-WP-0012-T06 | done | — | workplans/SAND-WP-0012-packer-orchestration.md | -| task | SAND-WP-0013-T01 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | -| task | SAND-WP-0013-T02 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | -| task | SAND-WP-0013-T03 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | +| task | SAND-WP-0013-T01 | done | — | workplans/SAND-WP-0013-bwrap-extension.md | +| task | SAND-WP-0013-T02 | done | — | workplans/SAND-WP-0013-bwrap-extension.md | +| task | SAND-WP-0013-T03 | done | — | workplans/SAND-WP-0013-bwrap-extension.md | | task | SAND-WP-0013-T04 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | -| task | SAND-WP-0013-T05 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | -| task | SAND-WP-0013-T06 | todo | — | workplans/SAND-WP-0013-bwrap-extension.md | +| task | SAND-WP-0013-T05 | done | — | workplans/SAND-WP-0013-bwrap-extension.md | +| task | SAND-WP-0013-T06 | done | — | workplans/SAND-WP-0013-bwrap-extension.md | diff --git a/docs/integrations/glas-harness.md b/docs/integrations/glas-harness.md index f98a74e..6860bb8 100644 --- a/docs/integrations/glas-harness.md +++ b/docs/integrations/glas-harness.md @@ -22,6 +22,23 @@ sandboxer create \ | `reachability.remote_dir` | sand-boxer | Workspace root on remote host | | `state` | sand-boxer | Lifecycle state (`ready`, etc.) | +## Two reachability modes + +Not every sandbox has an SSH hop. `ext.compose-ssh` / `ext.vm-packer` +always run remote and populate `reachability.ssh` + `reachability. +remote_dir`; glas-harness execs tools over that SSH channel. `ext.bwrap` +(SAND-WP-0013) runs same-host and never populates `reachability.ssh` — +instead it populates `reachability.pid` (the placeholder process holding +the bwrap namespaces) and `reachability.workspace_dir`. glas-harness +execs tools by entering that pid's namespaces directly +(`nsenter --target --mount --pid --net --uts --ipc -- `, no +tunnel needed) rather than opening an SSH channel. `reachability/enrich. +build_reachability_report()` returns both an `ssh_one_liner` and a +`local_exec_hint`; exactly one is non-null depending on which mode the +resolved extension uses. Consumers should branch on which field is +populated, not on profile id, since routing can fall back between +extensions. + ## Ownership | Concern | Owner | diff --git a/src/sandboxer/models.py b/src/sandboxer/models.py index c4922a1..688414f 100644 --- a/src/sandboxer/models.py +++ b/src/sandboxer/models.py @@ -156,6 +156,11 @@ class Reachability(BaseModel): tunnel: str | None = None tunnel_via: str | None = None identity: str | None = None + # Local (no-SSH-hop) descriptor — populated for same-host extensions + # like ext.bwrap. A consumer execs into the sandbox directly + # (e.g. `nsenter --target ...`) rather than over SSH. + pid: str | None = None + workspace_dir: str | None = None class SandboxStatus(BaseModel): diff --git a/src/sandboxer/reachability/enrich.py b/src/sandboxer/reachability/enrich.py index 6fde9ed..89a1efe 100644 --- a/src/sandboxer/reachability/enrich.py +++ b/src/sandboxer/reachability/enrich.py @@ -19,6 +19,11 @@ def enrich_reachability( enriched = dict(reach) spec = profile.reachability + if handle.get("workspace_dir"): + enriched.setdefault("workspace_dir", handle["workspace_dir"]) + if handle.get("pid") and enriched.get("endpoint", "").startswith("pid:"): + enriched.setdefault("pid", handle["pid"]) + if spec.tunnel: enriched.setdefault("tunnel_via", spec.tunnel) if spec.identity: @@ -50,6 +55,20 @@ def ssh_one_liner(reach: Reachability) -> str | None: return None +def local_exec_hint(reach: Reachability) -> str | None: + """No-SSH-hop exec hint for same-host extensions (e.g. ext.bwrap). + + Consumers exec directly into the sandbox's namespaces via the + placeholder process's pid, rather than opening an SSH channel. + """ + if reach.pid and reach.workspace_dir: + return ( + f"nsenter --target {reach.pid} --mount --pid --net --uts --ipc " + f"-- sh -c 'cd {reach.workspace_dir} && exec $SHELL'" + ) + return None + + def build_reachability_report(status: SandboxStatus) -> dict[str, Any]: """Consumer-facing reachability report with ops-bridge pointer.""" reach = status.reachability @@ -65,4 +84,5 @@ def build_reachability_report(status: SandboxStatus) -> dict[str, Any]: } if reach: payload["ssh_one_liner"] = ssh_one_liner(reach) + payload["local_exec_hint"] = local_exec_hint(reach) return payload \ No newline at end of file diff --git a/tests/test_bwrap.py b/tests/test_bwrap.py index b3a73b0..0810945 100644 --- a/tests/test_bwrap.py +++ b/tests/test_bwrap.py @@ -164,3 +164,22 @@ def test_supports_snapshots_is_false() -> None: assert ext.supports_snapshots() is False with pytest.raises(NotImplementedError): ext.snapshot({}) + + +def test_reachability_local_exec_hint_for_bwrap_handle() -> None: + from sandboxer.reachability.enrich import enrich_reachability, local_exec_hint + from sandboxer.models import Reachability + + handle = {"pid": "555", "workspace_dir": "/tmp/sandboxer-bwrap/abc", "host": "localhost"} + reach = {"host": "localhost", "endpoint": "pid:555"} + profile = _profile() + + enriched = enrich_reachability(reach, profile, handle) + reachability = Reachability(**enriched) + + assert reachability.pid == "555" + assert reachability.workspace_dir == "/tmp/sandboxer-bwrap/abc" + assert local_exec_hint(reachability) == ( + "nsenter --target 555 --mount --pid --net --uts --ipc " + "-- sh -c 'cd /tmp/sandboxer-bwrap/abc && exec $SHELL'" + ) diff --git a/workplans/SAND-WP-0013-bwrap-extension.md b/workplans/SAND-WP-0013-bwrap-extension.md index c4b4961..82c6671 100644 --- a/workplans/SAND-WP-0013-bwrap-extension.md +++ b/workplans/SAND-WP-0013-bwrap-extension.md @@ -76,7 +76,7 @@ built against the other. ```task id: SAND-WP-0013-T04 -status: todo +status: done priority: high state_hub_task_id: "1d8b3e18-d067-4364-89e9-5d5bc5923deb" ```