feat: pin bwrap rein runtimes and isolate private state
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
c2886b2f77
commit
d69827aaa2
12 changed files with 639 additions and 9 deletions
67
scripts/build-rein-runtime.py
Normal file
67
scripts/build-rein-runtime.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Build a standalone rein-aharness runtime for the fixed bwrap mount path.
|
||||
|
||||
Only package installation occurs here. No credentials or model calls are used.
|
||||
Run with the sand-boxer Python environment; uv must be available on PATH.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from sandboxer.extensions.runtime import RUNTIME_MOUNT, runtime_digest
|
||||
|
||||
|
||||
def checked(command: list[str]) -> str:
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"runtime build command failed: {Path(command[0]).name}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def build(output: Path, rein_source: Path, llm_source: Path) -> dict:
|
||||
revisions = {}
|
||||
for name, source in (("rein-aharness", rein_source), ("llm-connect", llm_source)):
|
||||
if checked(["git", "-C", str(source), "status", "--porcelain"]):
|
||||
raise ValueError(f"{name} source must be committed before building")
|
||||
revisions[name] = checked(["git", "-C", str(source), "rev-parse", "HEAD"])
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
checked(["/usr/bin/python3", "-m", "venv", "--copies", "--without-pip", str(output)])
|
||||
checked(["uv", "pip", "install", "--python", str(output / "bin/python3"),
|
||||
str(rein_source), str(llm_source)])
|
||||
# Console entrypoints must reference the in-sandbox mount, not the build host.
|
||||
for path in (output / "bin").iterdir():
|
||||
if not path.is_file() or path.is_symlink():
|
||||
continue
|
||||
with path.open("rb") as stream:
|
||||
first_line = stream.readline(4096)
|
||||
if first_line.startswith(f"#!{output}/bin/python".encode()):
|
||||
body = path.read_bytes().partition(b"\n")[2]
|
||||
path.write_bytes(f"#!{RUNTIME_MOUNT}/bin/python3\n".encode() + body)
|
||||
metadata = json.loads(checked([
|
||||
str(output / "bin/python3"), "-c",
|
||||
"import importlib.metadata,json,platform; "
|
||||
"print(json.dumps({'python':platform.python_version(),'packages':"
|
||||
"{d.metadata['Name']:d.version for d in importlib.metadata.distributions()}}))",
|
||||
]))
|
||||
metadata["source_revisions"] = revisions
|
||||
(output / "build-info.json").write_text(json.dumps(metadata, indent=2) + "\n")
|
||||
return {"runtime": {"path": str(output), "sha256": runtime_digest(output)},
|
||||
"build": metadata}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--rein-source", type=Path, required=True)
|
||||
parser.add_argument("--llm-source", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build(args.output.resolve(), args.rein_source.resolve(), args.llm_source.resolve())
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
105
scripts/smoke-bwrap-runtime.py
Normal file
105
scripts/smoke-bwrap-runtime.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Prove a real rein CLI starts in a pinned, read-only owner runtime.
|
||||
|
||||
No credential acquisition or model request is performed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from sandboxer.extensions.bwrap import BwrapExtension
|
||||
from sandboxer.models import Profile
|
||||
|
||||
PROBE = r'''
|
||||
import json, os, subprocess, sys
|
||||
from pathlib import Path
|
||||
import rein_aharness.adapter
|
||||
import llm_connect
|
||||
source = Path(sys.argv[1])
|
||||
runtime = Path('/opt/sandboxer/runtime')
|
||||
home = Path(os.environ['HOME'])
|
||||
private = home / 'private-state-proof'
|
||||
private.write_text('non-secret private state\n')
|
||||
assert not home.is_relative_to(Path.cwd())
|
||||
assert (home.stat().st_mode & 0o777) == 0o700
|
||||
assert private.is_file()
|
||||
try:
|
||||
(runtime / 'write-probe').write_text('must be refused')
|
||||
except OSError:
|
||||
readonly = True
|
||||
else:
|
||||
readonly = False
|
||||
assert readonly
|
||||
assert not source.exists()
|
||||
help_result = subprocess.run(['rein-aharness', '--help'], capture_output=True, timeout=15)
|
||||
assert help_result.returncode == 0
|
||||
assert b'usage:' in help_result.stdout
|
||||
assert subprocess.check_output(['git', 'status', '--porcelain', '--ignored=matching']) == b''
|
||||
interfaces = [line.split(':', 1)[0].strip()
|
||||
for line in Path('/proc/net/dev').read_text().splitlines()[2:]]
|
||||
assert interfaces == ['lo']
|
||||
print(json.dumps({'rein_cli_started': True, 'adapter_imported': True,
|
||||
'runtime_readonly': readonly, 'source_absent': True,
|
||||
'home_outside_workspace': True, 'home_mode': '0700',
|
||||
'worktree_clean': True, 'interfaces': interfaces,
|
||||
'python_prefix': sys.prefix,
|
||||
'credential_refs': json.loads(os.environ['SANDBOXER_CREDENTIAL_ROUTE_REFS'])}))
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--runtime-path", required=True)
|
||||
parser.add_argument("--runtime-sha256", required=True)
|
||||
args = parser.parse_args()
|
||||
with tempfile.TemporaryDirectory(prefix="sandboxer-runtime-proof-") as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "source"
|
||||
source.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(source)], check=True)
|
||||
extension = BwrapExtension({
|
||||
"base_dir": str(root / "sandboxes"),
|
||||
"runtime": {"path": args.runtime_path, "sha256": args.runtime_sha256},
|
||||
})
|
||||
profile = Profile(id="profile.runtime-proof", version="1", extension="ext.bwrap")
|
||||
handle = extension.provision(profile, {"repo": str(source)}, "localhost")
|
||||
try:
|
||||
extension.wait_ready(handle)
|
||||
result = extension.execute(
|
||||
handle, ["python3", "-c", PROBE, str(source)],
|
||||
credential_route_refs=[],
|
||||
execution_context={"actor": "agt", "project": "sand-boxer-runtime-proof",
|
||||
"run_id": "sand-wp-0015-proof"},
|
||||
timeout_seconds=30, max_output_bytes=65536,
|
||||
)
|
||||
persistence = extension.execute(
|
||||
handle, ["python3", "-c",
|
||||
"import os; from pathlib import Path; "
|
||||
"assert (Path(os.environ['HOME']) / 'private-state-proof').is_file()"],
|
||||
credential_route_refs=[],
|
||||
execution_context={"actor": "agt", "project": "sand-boxer-runtime-proof",
|
||||
"run_id": "sand-wp-0015-proof"},
|
||||
timeout_seconds=15, max_output_bytes=1024,
|
||||
)
|
||||
finally:
|
||||
teardown = extension.teardown(handle)
|
||||
passed = result["exit_code"] == 0 and not result["timed_out"]
|
||||
facts = json.loads(result["stdout"]) if passed else {}
|
||||
private_state_persisted = persistence["exit_code"] == 0
|
||||
passed = passed and private_state_persisted and teardown["workspace_removed"] == "True"
|
||||
print(json.dumps({
|
||||
"ok": passed, "sandbox_id": handle["sandbox_id"],
|
||||
"runtime_sha256": args.runtime_sha256, "proof": facts,
|
||||
"workspace_removed": teardown["workspace_removed"] == "True",
|
||||
"exit_code": result["exit_code"], "model_run_proven": False,
|
||||
"private_state_persisted": private_state_persisted,
|
||||
}, indent=2))
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue