Confine metered runs to an ephemeral owner Messages route
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
3e49a98a0e
commit
bfe0e4c4c8
7 changed files with 231 additions and 5 deletions
34
docs/bwrap-messages-route.md
Normal file
34
docs/bwrap-messages-route.md
Normal 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.
|
||||||
|
|
@ -6,6 +6,7 @@ import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
|
from sandboxer.extensions.messages_route import OwnerMessagesRoute
|
||||||
from sandboxer.extensions.registry import load_extension, resolve_backend
|
from sandboxer.extensions.registry import load_extension, resolve_backend
|
||||||
from sandboxer.lifecycle.expire import (
|
from sandboxer.lifecycle.expire import (
|
||||||
ExpireCandidate,
|
ExpireCandidate,
|
||||||
|
|
@ -52,10 +53,14 @@ class SandboxManager:
|
||||||
store: SandboxStore | None = None,
|
store: SandboxStore | None = None,
|
||||||
credits: CreditsStore | None = None,
|
credits: CreditsStore | None = None,
|
||||||
snapshots: SnapshotStore | None = None,
|
snapshots: SnapshotStore | None = None,
|
||||||
|
*,
|
||||||
|
messages_route: OwnerMessagesRoute | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.store = store or SandboxStore()
|
self.store = store or SandboxStore()
|
||||||
self.credits = credits or CreditsStore()
|
self.credits = credits or CreditsStore()
|
||||||
self.snapshots = snapshots or SnapshotStore()
|
self.snapshots = snapshots or SnapshotStore()
|
||||||
|
self._messages_route = messages_route
|
||||||
|
self._messages_created = False
|
||||||
self._exec_locks: dict[str, Lock] = {}
|
self._exec_locks: dict[str, Lock] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -101,6 +106,12 @@ class SandboxManager:
|
||||||
profile = load_profile(request.profile)
|
profile = load_profile(request.profile)
|
||||||
extension = resolve_extension(profile, request.inputs, host_override=host)
|
extension = resolve_extension(profile, request.inputs, host_override=host)
|
||||||
backend = resolve_backend(extension)
|
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)
|
resolved_host = self._resolved_host(profile, extension, host)
|
||||||
wants_telemetry = profile_wants_telemetry(profile)
|
wants_telemetry = profile_wants_telemetry(profile)
|
||||||
base_dir = extension.config.get("base_dir", "/tmp/sandboxer")
|
base_dir = extension.config.get("base_dir", "/tmp/sandboxer")
|
||||||
|
|
@ -276,6 +287,11 @@ class SandboxManager:
|
||||||
profile = load_profile(status.profile_id)
|
profile = load_profile(status.profile_id)
|
||||||
extension = load_extension(status.extension_id)
|
extension = load_extension(status.extension_id)
|
||||||
backend = resolve_backend(extension)
|
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():
|
if not backend.supports_execution():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Extension {status.extension_id} has no owner-mediated execution boundary"
|
f"Extension {status.extension_id} has no owner-mediated execution boundary"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from typing import Any
|
||||||
|
|
||||||
from sandboxer.extensions.base import SandboxExtension
|
from sandboxer.extensions.base import SandboxExtension
|
||||||
from sandboxer.extensions.egress import destinations
|
from sandboxer.extensions.egress import destinations
|
||||||
|
from sandboxer.extensions.messages_route import OwnerMessagesRoute
|
||||||
from sandboxer.extensions.runtime import RUNTIME_MOUNT, verified_runtime
|
from sandboxer.extensions.runtime import RUNTIME_MOUNT, verified_runtime
|
||||||
from sandboxer.models import Profile
|
from sandboxer.models import Profile
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ class BwrapExtension(SandboxExtension):
|
||||||
|
|
||||||
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
self.messages_route: OwnerMessagesRoute | None = None
|
||||||
cfg = self.config
|
cfg = self.config
|
||||||
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer-bwrap")
|
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer-bwrap")
|
||||||
self.bwrap_bin: str = cfg.get("bwrap_bin", "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")
|
runner = Path(__file__).with_name("bwrap_runner.py")
|
||||||
argv += ["--dir", "/run", "--dir", "/run/sandboxer"]
|
argv += ["--dir", "/run", "--dir", "/run/sandboxer"]
|
||||||
argv += ["--ro-bind", str(runner), "/run/sandboxer/bwrap_runner.py"]
|
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")
|
egress_module = Path(__file__).with_name("egress.py")
|
||||||
argv += ["--ro-bind", str(egress_module), "/run/sandboxer/egress.py"]
|
argv += ["--ro-bind", str(egress_module), "/run/sandboxer/egress.py"]
|
||||||
|
if egress_socket:
|
||||||
argv += ["--ro-bind", egress_socket, "/run/sandboxer/egress.sock"]
|
argv += ["--ro-bind", egress_socket, "/run/sandboxer/egress.sock"]
|
||||||
runtime = verified_runtime(self.config)
|
runtime = verified_runtime(self.config)
|
||||||
if runtime is not None:
|
if runtime is not None:
|
||||||
|
|
@ -98,6 +107,8 @@ class BwrapExtension(SandboxExtension):
|
||||||
argv.append("--runtime")
|
argv.append("--runtime")
|
||||||
if egress_socket:
|
if egress_socket:
|
||||||
argv.append("--egress")
|
argv.append("--egress")
|
||||||
|
if self.messages_route is not None:
|
||||||
|
argv.append("--messages")
|
||||||
return argv
|
return argv
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -211,7 +222,7 @@ class BwrapExtension(SandboxExtension):
|
||||||
try:
|
try:
|
||||||
return self._wait_ready(handle)
|
return self._wait_ready(handle)
|
||||||
except Exception:
|
except Exception:
|
||||||
if handle.get("egress_pid"):
|
if handle.get("egress_pid") or self.messages_route is not None:
|
||||||
self.teardown(handle)
|
self.teardown(handle)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -276,6 +287,10 @@ class BwrapExtension(SandboxExtension):
|
||||||
"max_output_bytes": max_output_bytes,
|
"max_output_bytes": max_output_bytes,
|
||||||
"stdin_text": stdin_text,
|
"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:
|
if credential_route_refs:
|
||||||
from sandboxer.extensions.credential_delivery import execute
|
from sandboxer.extensions.credential_delivery import execute
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -24,7 +25,8 @@ def _bounded_output(value: bytes | None, limit: int) -> tuple[str, bool]:
|
||||||
|
|
||||||
|
|
||||||
def _run(
|
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]:
|
) -> dict[str, object]:
|
||||||
command = payload["command"]
|
command = payload["command"]
|
||||||
timeout_seconds = int(payload["timeout_seconds"])
|
timeout_seconds = int(payload["timeout_seconds"])
|
||||||
|
|
@ -63,6 +65,16 @@ def _run(
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise ValueError("invalid owner credential envelope")
|
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)
|
child_env.update(credential_env)
|
||||||
timed_out = False
|
timed_out = False
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
|
|
@ -103,13 +115,20 @@ def main() -> int:
|
||||||
workspace = Path(sys.argv[1]).resolve(strict=True)
|
workspace = Path(sys.argv[1]).resolve(strict=True)
|
||||||
socket_path = Path(sys.argv[2])
|
socket_path = Path(sys.argv[2])
|
||||||
runtime_enabled = "--runtime" in sys.argv[3:]
|
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")
|
raise ValueError("invalid owner runtime mode")
|
||||||
proxy_port = None
|
proxy_port = None
|
||||||
if "--egress" in sys.argv[3:]:
|
if "--egress" in sys.argv[3:]:
|
||||||
from egress import bridge
|
from egress import bridge
|
||||||
|
|
||||||
proxy_port = bridge("/run/sandboxer/egress.sock")
|
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 = Path("/run/sandboxer/state")
|
||||||
state.mkdir(mode=0o700)
|
state.mkdir(mode=0o700)
|
||||||
for name in ("home", "config", "cache", "data", "tmp"):
|
for name in ("home", "config", "cache", "data", "tmp"):
|
||||||
|
|
@ -141,6 +160,7 @@ def main() -> int:
|
||||||
workspace,
|
workspace,
|
||||||
runtime_enabled=runtime_enabled,
|
runtime_enabled=runtime_enabled,
|
||||||
proxy_port=proxy_port,
|
proxy_port=proxy_port,
|
||||||
|
messages_port=messages_port,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
response = {"boundary_error": str(exc)}
|
response = {"boundary_error": str(exc)}
|
||||||
|
|
|
||||||
65
src/sandboxer/extensions/messages_route.py
Normal file
65
src/sandboxer/extensions/messages_route.py
Normal 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")
|
||||||
64
tests/test_messages_route.py
Normal file
64
tests/test_messages_route.py
Normal 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, {})
|
||||||
|
|
@ -8,7 +8,7 @@ status: blocked
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: bwrap-runtime-and-private-state
|
topic_slug: bwrap-runtime-and-private-state
|
||||||
created: "2026-09-05"
|
created: "2026-09-05"
|
||||||
updated: "2026-09-08"
|
updated: "2026-09-09"
|
||||||
state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d"
|
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`.
|
`docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json`.
|
||||||
T04 retains owner execution configuration, native credential/egress and real-model
|
T04 retains owner execution configuration, native credential/egress and real-model
|
||||||
acceptance; Railiance installation needs its own target-specific return.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue