Confine metered runs to an ephemeral owner Messages route
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 22:20:56 +02:00
parent 3e49a98a0e
commit bfe0e4c4c8
7 changed files with 231 additions and 5 deletions

View file

@ -0,0 +1,34 @@
# Owner-metered Messages route
An explicitly supplied `SandboxManager(messages_route=OwnerMessagesRoute(...))`
accepts one exact profile/actor/project/run binding and one sandbox. This trusted
in-process capability is never loaded from a profile, caller input or stored status.
Rein's `MessagesOwner` constructs it after parent spend admission, using llm-connect's
private Unix listener. Only that socket is mounted at `/run/sandboxer/messages.sock`.
The existing bounded byte bridge exposes a namespace-local HTTP base URL. The
broker injects an opaque run token, with exact-output redaction, into each child.
The actual provider key stays in the trusted forwarding process outside bwrap.
Metered mode requires ext.bwrap, default-deny with an empty egress list, no owner
CONNECT allowlist, no setup secrets or provider credential routes, and only standard
system read-only mounts. Private owner state cannot overlap source/workspace/runtime
or those mounts. No provider key/ledger directory or host network interface is
exposed. Workload changes to URLs, proxy variables, Git helpers or HTTP paths cannot
create another route. Raw access to the mounted socket still reaches the same
metered protocol. Host owner code remains trusted; this adds no public API authority.
Route expiry, ledger admission and revocation belong to rein. Sand-boxer owns
namespace confinement and teardown. Tokens are absent from argv and persisted
SandboxStatus/inputs. A manager without the ephemeral binding cannot inject the
required token into an existing metered broker after a restart. Unknown request
liability is retained; bootstrap/recovery must not rebind or mint replacement tokens.
The local proof in rein `tests/test_messages_owner.py` uses the actual manager,
owner execution transport, bwrap namespaces and host-side fake provider. It proves
private state and PID separation, only lo, blocked direct host/public-IP access,
successful guarded streaming, and no second upstream call after revocation. The
combined worker proof also covers allowed commit import and lost-close replay.
`make check`: lint clean, 199 passed. No runtime/profile installation or real secret
read occurred. Existing direct-CONNECT profiles/credential delivery remain separate;
the factory needs a newly reviewed empty-egress profile and admitted owner bootstrap
on Railiance, under SAND-WP-0015-T04 and HFACT-WP-0001-T03/T04.

View file

@ -6,6 +6,7 @@ import re
from pathlib import Path
from threading import Lock
from sandboxer.extensions.messages_route import OwnerMessagesRoute
from sandboxer.extensions.registry import load_extension, resolve_backend
from sandboxer.lifecycle.expire import (
ExpireCandidate,
@ -52,10 +53,14 @@ class SandboxManager:
store: SandboxStore | None = None,
credits: CreditsStore | None = None,
snapshots: SnapshotStore | None = None,
*,
messages_route: OwnerMessagesRoute | None = None,
) -> None:
self.store = store or SandboxStore()
self.credits = credits or CreditsStore()
self.snapshots = snapshots or SnapshotStore()
self._messages_route = messages_route
self._messages_created = False
self._exec_locks: dict[str, Lock] = {}
@staticmethod
@ -101,6 +106,12 @@ class SandboxManager:
profile = load_profile(request.profile)
extension = resolve_extension(profile, request.inputs, host_override=host)
backend = resolve_backend(extension)
if self._messages_route is not None:
self._messages_route.validate(profile, request.consumer, backend, request.inputs)
if self._messages_created:
raise ValueError("metered owner manager is single-run and single-sandbox")
self._messages_created = True
backend.messages_route = self._messages_route
resolved_host = self._resolved_host(profile, extension, host)
wants_telemetry = profile_wants_telemetry(profile)
base_dir = extension.config.get("base_dir", "/tmp/sandboxer")
@ -276,6 +287,11 @@ class SandboxManager:
profile = load_profile(status.profile_id)
extension = load_extension(status.extension_id)
backend = resolve_backend(extension)
if self._messages_route is not None:
self._messages_route.validate(profile, status.consumer, backend, status.inputs)
if request.credential_route_refs:
raise ValueError("metered execution refuses provider credential delivery")
backend.messages_route = self._messages_route
if not backend.supports_execution():
raise RuntimeError(
f"Extension {status.extension_id} has no owner-mediated execution boundary"

View file

@ -17,6 +17,7 @@ from typing import Any
from sandboxer.extensions.base import SandboxExtension
from sandboxer.extensions.egress import destinations
from sandboxer.extensions.messages_route import OwnerMessagesRoute
from sandboxer.extensions.runtime import RUNTIME_MOUNT, verified_runtime
from sandboxer.models import Profile
@ -35,6 +36,7 @@ class BwrapExtension(SandboxExtension):
def __init__(self, config: dict[str, Any] | None = None) -> None:
super().__init__(config)
self.messages_route: OwnerMessagesRoute | None = None
cfg = self.config
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer-bwrap")
self.bwrap_bin: str = cfg.get("bwrap_bin", "bwrap")
@ -75,9 +77,16 @@ class BwrapExtension(SandboxExtension):
runner = Path(__file__).with_name("bwrap_runner.py")
argv += ["--dir", "/run", "--dir", "/run/sandboxer"]
argv += ["--ro-bind", str(runner), "/run/sandboxer/bwrap_runner.py"]
if egress_socket:
if self.messages_route is not None:
if egress_socket:
raise ValueError("metered route cannot coexist with CONNECT egress")
argv += [
"--ro-bind", str(self.messages_route.socket_path), "/run/sandboxer/messages.sock"
]
if egress_socket or self.messages_route is not None:
egress_module = Path(__file__).with_name("egress.py")
argv += ["--ro-bind", str(egress_module), "/run/sandboxer/egress.py"]
if egress_socket:
argv += ["--ro-bind", egress_socket, "/run/sandboxer/egress.sock"]
runtime = verified_runtime(self.config)
if runtime is not None:
@ -98,6 +107,8 @@ class BwrapExtension(SandboxExtension):
argv.append("--runtime")
if egress_socket:
argv.append("--egress")
if self.messages_route is not None:
argv.append("--messages")
return argv
@staticmethod
@ -211,7 +222,7 @@ class BwrapExtension(SandboxExtension):
try:
return self._wait_ready(handle)
except Exception:
if handle.get("egress_pid"):
if handle.get("egress_pid") or self.messages_route is not None:
self.teardown(handle)
raise
@ -276,6 +287,10 @@ class BwrapExtension(SandboxExtension):
"max_output_bytes": max_output_bytes,
"stdin_text": stdin_text,
}
if self.messages_route is not None:
if credential_route_refs:
raise ValueError("metered route refuses provider credentials")
request["messages_token"] = self.messages_route.token
if credential_route_refs:
from sandboxer.extensions.credential_delivery import execute

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import json
import os
import re
import signal
import socket
import subprocess
@ -24,7 +25,8 @@ def _bounded_output(value: bytes | None, limit: int) -> tuple[str, bool]:
def _run(
payload: dict, workspace: Path, *, runtime_enabled: bool = False, proxy_port: int | None = None
payload: dict, workspace: Path, *, runtime_enabled: bool = False, proxy_port: int | None = None,
messages_port: int | None = None,
) -> dict[str, object]:
command = payload["command"]
timeout_seconds = int(payload["timeout_seconds"])
@ -63,6 +65,16 @@ def _run(
)
):
raise ValueError("invalid owner credential envelope")
messages_token = payload.get("messages_token")
if messages_port is not None:
if (proxy_port is not None or credential_env or credential_refs
or not isinstance(messages_token, str)
or not re.fullmatch(r"[A-Za-z0-9_-]{43,100}", messages_token)):
raise ValueError("invalid metered owner envelope")
child_env["ANTHROPIC_BASE_URL"] = f"http://127.0.0.1:{messages_port}"
credential_env = {"ANTHROPIC_API_KEY": messages_token}
elif messages_token is not None:
raise ValueError("metered owner route unavailable")
child_env.update(credential_env)
timed_out = False
process = subprocess.Popen(
@ -103,13 +115,20 @@ def main() -> int:
workspace = Path(sys.argv[1]).resolve(strict=True)
socket_path = Path(sys.argv[2])
runtime_enabled = "--runtime" in sys.argv[3:]
if any(flag not in {"--runtime", "--egress"} for flag in sys.argv[3:]):
if any(flag not in {"--runtime", "--egress", "--messages"} for flag in sys.argv[3:]):
raise ValueError("invalid owner runtime mode")
proxy_port = None
if "--egress" in sys.argv[3:]:
from egress import bridge
proxy_port = bridge("/run/sandboxer/egress.sock")
messages_port = None
if "--messages" in sys.argv[3:]:
if proxy_port is not None:
raise ValueError("metered route cannot coexist with CONNECT egress")
from egress import bridge
messages_port = bridge("/run/sandboxer/messages.sock")
state = Path("/run/sandboxer/state")
state.mkdir(mode=0o700)
for name in ("home", "config", "cache", "data", "tmp"):
@ -141,6 +160,7 @@ def main() -> int:
workspace,
runtime_enabled=runtime_enabled,
proxy_port=proxy_port,
messages_port=messages_port,
)
except Exception as exc:
response = {"boundary_error": str(exc)}

View file

@ -0,0 +1,65 @@
"""Ephemeral trusted-owner binding; never a profile, API or stored secret."""
from __future__ import annotations
import os
import re
import stat
from dataclasses import dataclass, field
from pathlib import Path
@dataclass(frozen=True)
class OwnerMessagesRoute:
socket_path: Path = field(repr=False)
token: str = field(repr=False)
profile_id: str
actor: str
project: str
run_id: str
private_paths: tuple[Path, ...] = field(default=(), repr=False)
def validate(self, profile, consumer, backend, inputs) -> None:
from sandboxer.extensions.bwrap import BwrapExtension
if type(backend) is not BwrapExtension:
raise ValueError("metered route requires the bwrap owner")
if (
profile.id != self.profile_id
or consumer.actor.value != self.actor
or consumer.project != self.project
or consumer.run_id != self.run_id
or not self.run_id
):
raise ValueError("metered route consumer mismatch")
if profile.setup.secret_refs:
raise ValueError("metered route refuses setup credential acquisition")
if profile.network.default != "deny" or profile.network.egress:
raise ValueError("metered route requires empty default-deny egress")
if backend.config.get("allowed_egress") or backend.config.get("credential_routes"):
raise ValueError("metered route cannot coexist with provider egress or credentials")
if not re.fullmatch(r"[A-Za-z0-9_-]{43,100}", self.token):
raise ValueError("invalid metered route token")
path = self.socket_path
metadata = path.lstat()
parent = path.parent.lstat()
if (
not path.is_absolute() or path.resolve() != path
or not stat.S_ISSOCK(metadata.st_mode) or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o600
or parent.st_uid != os.getuid() or stat.S_IMODE(parent.st_mode) != 0o700
):
raise ValueError("metered route requires a private owner socket")
safe_binds = {"/usr", "/bin", "/lib", "/lib64", "/etc/resolv.conf", "/etc/ssl/certs"}
if not set(backend.ro_binds).issubset(safe_binds):
raise ValueError("metered route refuses additional host mounts")
exposed = [Path(backend.base_dir), *map(Path, backend.ro_binds)]
if inputs.get("repo"):
exposed.append(Path(inputs["repo"]))
if backend.config.get("runtime"):
exposed.append(Path(backend.config["runtime"]["path"]))
for private in (path.parent, *self.private_paths):
for visible in exposed:
a, b = private.resolve(), visible.resolve()
if a.is_relative_to(b) or b.is_relative_to(a):
raise ValueError("metered owner state overlaps a workload mount")

View file

@ -0,0 +1,64 @@
"""Trusted binding refuses authority expansion before provisioning."""
import socket
from dataclasses import replace
from types import SimpleNamespace
import pytest
from sandboxer.extensions.bwrap import BwrapExtension
from sandboxer.extensions.messages_route import OwnerMessagesRoute
from sandboxer.models import Consumer
from sandboxer.profiles.loader import load_profile
@pytest.fixture
def binding(tmp_path):
private = tmp_path / "private"
private.mkdir(mode=0o700)
path = private / "route.sock"
with socket.socket(socket.AF_UNIX) as listener:
listener.bind(str(path))
path.chmod(0o600)
yield OwnerMessagesRoute(path, "a" * 43, "profile.bwrap-local", "agt", "fixture", "run-1")
def test_exact_route_has_only_fixed_socket_mount(binding, tmp_path):
backend = BwrapExtension({"base_dir": str(tmp_path / "workspaces")})
profile = load_profile("profile.bwrap-local")
consumer = Consumer(actor="agt", project="fixture", run_id="run-1")
binding.validate(profile, consumer, backend, {})
backend.messages_route = binding
argv = backend._bwrap_argv(str(tmp_path / "workspaces" / "one"))
assert "--unshare-net" in argv and "--messages" in argv
assert str(binding.socket_path) in argv and binding.token not in argv
assert "--egress" not in argv
assert binding.token not in repr(binding)
@pytest.mark.parametrize("change", ["actor", "project", "run_id", "profile", "network",
"allowlist", "credentials", "mount", "private", "backend"])
def test_route_refuses_expansion(binding, tmp_path, change):
backend = BwrapExtension({"base_dir": str(tmp_path / "workspaces")})
profile = load_profile("profile.bwrap-local")
consumer = Consumer(actor="agt", project="fixture", run_id="run-1")
if change in {"actor", "project", "run_id"}:
consumer = consumer.model_copy(update={change: "adm" if change == "actor" else "other"})
if change == "actor":
consumer = Consumer(actor="adm", project="fixture", run_id="run-1")
elif change == "profile":
binding = replace(binding, profile_id="profile.other")
elif change == "network":
profile.network.egress = ["api.anthropic.com:443"]
elif change == "allowlist":
backend.config["allowed_egress"] = ["api.anthropic.com:443"]
elif change == "credentials":
backend.config["credential_routes"] = {"route": {}}
elif change == "mount":
backend.ro_binds.append("/home")
elif change == "private":
backend.base_dir = str(binding.socket_path.parent)
elif change == "backend":
backend = SimpleNamespace()
with pytest.raises(ValueError):
binding.validate(profile, consumer, backend, {})

View file

@ -8,7 +8,7 @@ status: blocked
owner: codex
topic_slug: bwrap-runtime-and-private-state
created: "2026-09-05"
updated: "2026-09-08"
updated: "2026-09-09"
state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d"
---
@ -248,3 +248,15 @@ full check used that canonical basename. Evidence:
`docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json`.
T04 retains owner execution configuration, native credential/egress and real-model
acceptance; Railiance installation needs its own target-specific return.
## 2026-09-09 factory metered route source return
Implemented the trusted, ephemeral Messages route described in
[docs/bwrap-messages-route.md](../docs/bwrap-messages-route.md). Actual local
bwrap owner transport proves provider key/ledger separation, direct-route denial,
revocation and teardown; rein also proves metered request plus commit import and
close replay. `make check`: lint clean, 199 passed. T04 remains waiting for the
admitted provider-to-owner bootstrap, updated protected runtime/profile, Railiance
placement, live compatibility and G0. Existing child-provider-key/direct-CONNECT
proofs do not admit this different credential holder or metered profile. No CCR,
secret read, deployment or paid request was performed.