"""R4 (partial) — containment guard for the SOR nested-SSH forwarder. This module lands **only instrument-validation gate item 5**: forwarders are *isolated-engine-only*. It is the guard that every forwarder MUST pass before it does anything — ``assert engine != local`` or refuse. It moves no traffic, opens no socket, spawns no engine, and builds no SSH chain. The traffic-moving half of R4 (gate item 1: a 3-hop self-traffic circuit across the grid delivering a known payload e2e, with per-hop pcaps) is intentionally **NOT** implemented here — it is HELD for a live isolated engine + grid (see OVERSEER-STATUS.md). Committing an unrunnable forwarder would violate the build discipline; this guard, by contrast, is fully verifiable offline: constructing a forwarder plan for a ``local`` (or unknown) engine refuses, for an isolated engine it yields the isolation exec prefix. Containment law (CLAUDE.md §Containment #1) is encoded structurally: there is no code path in this module that returns an exec prefix for the host. ``local`` — the explicit warned exception the *chat* sandbox allows (``bridge.py:540``) — is never valid for a SOR run. The allow-list is imported from ``provenance`` so the forwarder and the run manifest can never disagree about what counts as isolated. """ from __future__ import annotations from dataclasses import dataclass from typing import List from cmd_chat.sor.provenance import ISOLATED_ENGINES # The host. Never a valid engine for a SOR forwarder — its whole purpose is to # keep every forwarder/circuit process off the host (blast-radius containment). LOCAL_ENGINE = "local" class ContainmentError(RuntimeError): """A forwarder was asked to run on a non-isolated engine (or with no container/VM to exec into). Raised instead of running — the code refuses.""" def assert_isolated(engine: str) -> None: """Raise :class:`ContainmentError` unless ``engine`` is an isolated engine. This is the ``assert engine != local`` half of the R4 acceptance check (instrument-validation gate item 5), hardened to also reject any engine not on the manifest allow-list (unknown engines are refused, not assumed safe).""" if engine == LOCAL_ENGINE: raise ContainmentError( "containment: SOR forwarders never run on the host " f"({LOCAL_ENGINE!r}); isolated engine required (one of {ISOLATED_ENGINES})" ) if engine not in ISOLATED_ENGINES: raise ContainmentError( f"containment: engine {engine!r} is not an isolated engine " f"(allowed: {ISOLATED_ENGINES})" ) def isolation_prefix(engine: str, container: str) -> List[str]: """The argv prefix that runs a command *inside* the named isolated engine — the same shape the chat sandbox uses (``bridge.py:_exec_prefix``), minus the ``local`` host exception, which is refused here. The caller appends the actual program + args as separate argv items (never a shell-interpolated string), so circuit descriptors can't inject shell metacharacters. Refuses (raises) on a non-isolated engine or a missing container name.""" assert_isolated(engine) if not container: raise ContainmentError( f"containment: no container/VM name to exec into for engine {engine!r}" ) if engine == "docker": return ["docker", "exec", "-i", container] if engine == "multipass": return ["multipass", "exec", container, "--"] # Unreachable: assert_isolated already constrained `engine` to ISOLATED_ENGINES # and both members are handled above. Refuse rather than fall through silently. raise ContainmentError(f"containment: no isolation prefix for engine {engine!r}") @dataclass(frozen=True) class ForwarderPlan: """A validated, inert description of *where* a hop forwarder would run. Its construction is the containment gate: a plan for a ``local``/unknown engine, or with no container, cannot be built. It carries no credentials, opens no connection, and moves no bytes — the traffic-moving forwarder that would consume it (gate item 1) is HELD for the human + a live grid.""" engine: str container: str def __post_init__(self) -> None: assert_isolated(self.engine) if not self.container: raise ContainmentError( f"containment: no container/VM name for engine {self.engine!r}" ) def exec_prefix(self) -> List[str]: """The isolated exec argv prefix for this plan (validated at build time).""" return isolation_prefix(self.engine, self.container)