"""Fail closed when an RC Compose plan can overlap a protected installation. The public-candidate workflow proves a disposable API/Web/Database stack. P12 adds a GPU-capable Node Agent and Runtime Worker, which makes an accidental shared volume, host network, port, or resource name materially more dangerous. This helper validates the rendered Compose JSON and the resources observed after startup. It deliberately accepts no implicit production defaults: callers must provide the candidate root and every protected port/name they want to keep outside the validation project namespace. """ from __future__ import annotations import argparse import json import re import subprocess from collections.abc import Mapping, Sequence from pathlib import Path from typing import Any PROJECT_PATTERN = re.compile( r"modelforge-validation-[a-z0-9](?:[a-z0-9-]{0,39}[a-z0-9])?\Z" ) VOLUME_NAME_PATTERN = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.-]*") CONFIG_TARGET = "/app/config" CONFIG_MANIFEST = "models/initial-candidates.yaml" class IsolationError(ValueError): """A candidate can share state or reach outside its owned RC namespace.""" def _mapping(value: object, label: str) -> Mapping[str, Any]: if not isinstance(value, Mapping): raise IsolationError(f"{label} must be an object") return value def _sequence(value: object, label: str) -> Sequence[Any]: if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): raise IsolationError(f"{label} must be a list") return value def validate_project(project: str) -> str: if PROJECT_PATTERN.fullmatch(project) is None: raise IsolationError( "validation project must use a bounded modelforge-validation-* namespace" ) return project def _is_relative_to(path: Path, root: Path) -> bool: try: path.relative_to(root) except ValueError: return False return True def _published_port(port: Mapping[str, Any], service_name: str, public_tls_port: int | None) -> int | None: published = port.get("published") if published in (None, ""): return None try: result = int(str(published)) except ValueError as exc: raise IsolationError(f"{service_name} has an invalid published port") from exc if not 0 <= result <= 65535: raise IsolationError(f"{service_name} has an invalid published port") host_ip = str(port.get("host_ip") or "") if host_ip not in {"127.0.0.1", "::1"} and not ( service_name == "tls" and public_tls_port == result == 18444 and host_ip == "0.0.0.0" and str(port.get("target")) == "8443" and port.get("protocol", "tcp") == "tcp" ): raise IsolationError(f"{service_name} publishes a non-loopback port") return result def _validate_service_mount( mount: Mapping[str, Any], *, service_name: str, candidate_root: Path, declared_volumes: set[str] ) -> None: mount_type = mount.get("type") source = mount.get("source") target = str(mount.get("target") or "") if target == "/var/run/docker.sock": raise IsolationError(f"{service_name} mounts the Docker socket") if mount_type == "bind": if not isinstance(source, str) or not source: raise IsolationError(f"{service_name} has an invalid bind source") resolved = Path(source) if not resolved.is_absolute(): resolved = candidate_root / resolved try: resolved = resolved.resolve() except (OSError, RuntimeError) as exc: raise IsolationError(f"{service_name} has an invalid bind source") from exc if not _is_relative_to(resolved, candidate_root): raise IsolationError(f"{service_name} bind source escapes the candidate root") try: resolved = resolved.resolve(strict=True) except FileNotFoundError as exc: raise IsolationError(f"{service_name} has a missing bind source") from exc except (OSError, RuntimeError) as exc: raise IsolationError(f"{service_name} has an invalid bind source") from exc if not _is_relative_to(resolved, candidate_root): raise IsolationError(f"{service_name} bind source escapes the candidate root") if mount.get("read_only") is not True: raise IsolationError(f"{service_name} has a writable bind mount") if target == CONFIG_TARGET: manifest = resolved / CONFIG_MANIFEST try: resolved_manifest = manifest.resolve(strict=True) except FileNotFoundError as exc: raise IsolationError( f"{service_name} {CONFIG_TARGET} bind is missing {CONFIG_MANIFEST} under its source" ) from exc except (OSError, RuntimeError) as exc: raise IsolationError( f"{service_name} {CONFIG_TARGET} bind has an invalid {CONFIG_MANIFEST}" ) from exc if not _is_relative_to(resolved_manifest, resolved) or not resolved_manifest.is_file(): raise IsolationError( f"{service_name} {CONFIG_TARGET} bind is missing {CONFIG_MANIFEST} under its source" ) return if mount_type == "volume": if not isinstance(source, str) or VOLUME_NAME_PATTERN.fullmatch(source) is None: raise IsolationError(f"{service_name} has an invalid named volume") if source not in declared_volumes: raise IsolationError(f"{service_name} uses an undeclared volume {source}") return if mount_type in {"tmpfs", "npipe"}: return raise IsolationError(f"{service_name} has unsupported mount type {mount_type!r}") def validate_compose( document: Mapping[str, Any], *, project: str, candidate_root: Path, protected_ports: set[int], protected_resource_names: set[str], public_tls_port: int | None = None, ) -> dict[str, object]: """Validate a rendered Compose document before any RC container starts.""" validate_project(project) candidate_root = candidate_root.resolve() services = _mapping(document.get("services"), "services") if not services: raise IsolationError("Compose plan has no services") top_volumes = _mapping(document.get("volumes", {}), "volumes") top_networks = _mapping(document.get("networks", {}), "networks") declared_volumes = set(top_volumes) declared_networks = set(top_networks) if not declared_volumes or not declared_networks: raise IsolationError("validation plan requires its own declared volumes and network") if public_tls_port is not None and public_tls_port != 18444: raise IsolationError("the only admitted public TLS test port is 18444") if public_tls_port is not None and "tls" not in services: raise IsolationError("public TLS test port requires the isolated tls service") for name, configuration in top_volumes.items(): if VOLUME_NAME_PATTERN.fullmatch(name) is None: raise IsolationError(f"invalid declared volume {name!r}") config = _mapping(configuration, f"volume {name}") if config.get("external"): raise IsolationError(f"volume {name} is external") actual_name = config.get("name") if actual_name is not None and actual_name != f"{project}_{name}": raise IsolationError(f"volume {name} has a non-project name") for name, configuration in top_networks.items(): if VOLUME_NAME_PATTERN.fullmatch(name) is None: raise IsolationError(f"invalid declared network {name!r}") config = _mapping(configuration, f"network {name}") if config.get("external"): raise IsolationError(f"network {name} is external") actual_name = config.get("name") if actual_name is not None and actual_name != f"{project}_{name}": raise IsolationError(f"network {name} has a non-project name") published_ports: set[int] = set() for name, configuration in services.items(): service = _mapping(configuration, f"service {name}") if service.get("container_name"): raise IsolationError(f"{name} declares a global container name") if service.get("network_mode") == "host": raise IsolationError(f"{name} uses the host network") configured_networks = service.get("networks", []) if isinstance(configured_networks, Mapping): service_networks: Sequence[Any] = list(configured_networks) else: service_networks = _sequence(configured_networks, f"{name}.networks") network_names = [] for network in service_networks: network_name = network if isinstance(network, str) else _mapping(network, f"{name}.network").get("target") if network_name not in declared_networks: raise IsolationError(f"{name} uses an undeclared network") network_names.append(network_name) if name == "tls" and public_tls_port is not None: if set(network_names) != {"ingress"}: raise IsolationError("tls service must join only the candidate ingress network") if service.get("image") != "caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d": raise IsolationError("tls service must use the pinned Caddy image") if service.get("read_only") is not True or service.get("cap_drop") != ["ALL"]: raise IsolationError("tls service must have read-only root and no capabilities") if service.get("privileged") or service.get("cap_add") or service.get("build"): raise IsolationError("tls service cannot be privileged or locally built") if "no-new-privileges:true" not in _sequence(service.get("security_opt", []), "tls.security_opt"): raise IsolationError("tls service must refuse new privileges") mounts = _sequence(service.get("volumes", []), "tls.volumes") targets = {str(_mapping(mount, "tls.volume").get("target")) for mount in mounts} if targets != {"/etc/caddy/Caddyfile", "/run/tls/fullchain.pem", "/run/tls/privkey.pem"} or len(mounts) != 3: raise IsolationError("tls service must mount only its config and certificate pair") tls_ports = _sequence(service.get("ports", []), "tls.ports") if len(tls_ports) != 1: raise IsolationError("tls service must publish exactly one port") tls_port = _mapping(tls_ports[0], "tls.port") if str(tls_port.get("published")) != "18444" or tls_port.get("host_ip") != "0.0.0.0": raise IsolationError("tls service must bind only public test port 18444") for mount in _sequence(service.get("volumes", []), f"{name}.volumes"): _validate_service_mount( _mapping(mount, f"{name}.volume"), service_name=name, candidate_root=candidate_root, declared_volumes=declared_volumes, ) for port in _sequence(service.get("ports", []), f"{name}.ports"): published = _published_port(_mapping(port, f"{name}.port"), name, public_tls_port) if published and published in protected_ports: raise IsolationError(f"{name} reuses protected host port {published}") if published: published_ports.add(published) serialized = json.dumps(document, sort_keys=True) referenced = sorted(name for name in protected_resource_names if name and name in serialized) if referenced: raise IsolationError(f"Compose plan references protected resources: {', '.join(referenced)}") return { "project": project, "services": sorted(services), "declared_volumes": sorted(declared_volumes), "declared_networks": sorted(declared_networks), "published_ports": sorted(published_ports), } def validate_observed_resources( document: Mapping[str, Any], *, project: str ) -> dict[str, list[str]]: """Ensure resources created by Compose retain the exact validation project label and name.""" validate_project(project) expected_label = project prefixes = {"containers": f"{project}-", "volumes": f"{project}_", "networks": f"{project}_"} result: dict[str, list[str]] = {} for kind, prefix in prefixes.items(): items = _sequence(document.get(kind), kind) if not items: raise IsolationError(f"no observed {kind} for {project}") names: list[str] = [] for item in items: resource = _mapping(item, kind) name = resource.get("name") labels = _mapping(resource.get("labels"), f"{kind}.labels") if not isinstance(name, str) or not name.startswith(prefix): raise IsolationError(f"{kind} resource has a non-project name") if labels.get("com.docker.compose.project") != expected_label: raise IsolationError(f"{kind} resource has no exact project label") names.append(name) result[kind] = sorted(names) return result def _docker(*args: str) -> str: completed = subprocess.run( ("docker", *args), check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) if completed.returncode: raise IsolationError(f"Docker inspection failed: {' '.join(args)}") return completed.stdout def collect_observed_resources(project: str) -> dict[str, list[dict[str, object]]]: """Collect only resources carrying the exact Compose project label. ``docker inspect`` is used instead of parsing ``docker * ls`` label strings: a label value can itself contain commas or equals signs, and P12 must not mistake a truncated display value for resource ownership evidence. """ validate_project(project) kinds = { "containers": ("container", ("ls", "--all", "--quiet")), "volumes": ("volume", ("ls", "--quiet")), "networks": ("network", ("ls", "--quiet")), } result: dict[str, list[dict[str, object]]] = {} for plural, (singular, list_args) in kinds.items(): identifiers = [ value for value in _docker( singular, *list_args, "--filter", f"label=com.docker.compose.project={project}" ).splitlines() if value ] resources: list[dict[str, object]] = [] for identifier in identifiers: try: inspected = json.loads(_docker(singular, "inspect", identifier)) except json.JSONDecodeError as exc: raise IsolationError(f"Docker returned invalid {singular} inspection JSON") from exc if not isinstance(inspected, list) or len(inspected) != 1 or not isinstance(inspected[0], Mapping): raise IsolationError(f"Docker returned ambiguous {singular} inspection data") item = inspected[0] labels = item.get("Labels") if singular == "container": config = _mapping(item.get("Config"), "container.Config") labels = config.get("Labels") resources.append( { "name": str(item.get("Name") or "").lstrip("/"), "labels": dict(_mapping(labels or {}, f"{singular}.Labels")), } ) result[plural] = resources return result def _read_json(path: Path) -> Mapping[str, Any]: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise IsolationError(f"cannot read JSON evidence {path}") from exc return _mapping(value, str(path)) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project", required=True) parser.add_argument("--candidate-root", required=True, type=Path) parser.add_argument("--compose-json", required=True, type=Path) parser.add_argument("--observed-json", type=Path) parser.add_argument("--inspect-docker", action="store_true") parser.add_argument("--protected-port", action="append", default=[], type=int) parser.add_argument("--public-tls-port", type=int) parser.add_argument("--protected-resource", action="append", default=[]) args = parser.parse_args(argv) try: if args.observed_json is not None and args.inspect_docker: raise IsolationError("choose observed JSON or Docker inspection, not both") result: dict[str, object] = { "preflight": validate_compose( _read_json(args.compose_json), project=args.project, candidate_root=args.candidate_root, protected_ports=set(args.protected_port), protected_resource_names=set(args.protected_resource), public_tls_port=args.public_tls_port, ) } if args.observed_json is not None: result["observed"] = validate_observed_resources( _read_json(args.observed_json), project=args.project ) elif args.inspect_docker: result["observed"] = validate_observed_resources( collect_observed_resources(args.project), project=args.project ) except IsolationError as exc: print(json.dumps({"result": "FAIL", "error": str(exc)}, sort_keys=True)) return 2 print(json.dumps({"result": "PASS", **result}, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())