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
|
|
@ -22,6 +22,7 @@
|
||||||
| workplan | SAND-WP-0012 | finished | — | workplans/SAND-WP-0012-packer-orchestration.md |
|
| workplan | SAND-WP-0012 | finished | — | workplans/SAND-WP-0012-packer-orchestration.md |
|
||||||
| workplan | SAND-WP-0013 | finished | — | workplans/SAND-WP-0013-bwrap-extension.md |
|
| workplan | SAND-WP-0013 | finished | — | workplans/SAND-WP-0013-bwrap-extension.md |
|
||||||
| workplan | SAND-WP-0014 | active | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
| workplan | SAND-WP-0014 | active | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
||||||
|
| workplan | SAND-WP-0015 | blocked | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
|
||||||
| task | SAND-WP-0001-T01 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
| task | SAND-WP-0001-T01 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
||||||
| task | SAND-WP-0001-T02 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
| task | SAND-WP-0001-T02 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
||||||
| task | SAND-WP-0001-T03 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
| task | SAND-WP-0001-T03 | done | — | workplans/SAND-WP-0001-statehub-bootstrap.md |
|
||||||
|
|
@ -117,3 +118,7 @@
|
||||||
| task | SAND-WP-0014-T03 | done | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
| task | SAND-WP-0014-T03 | done | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
||||||
| task | SAND-WP-0014-T04 | done | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
| task | SAND-WP-0014-T04 | done | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
||||||
| task | SAND-WP-0014-T05 | wait | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
| task | SAND-WP-0014-T05 | wait | — | workplans/SAND-WP-0014-owner-mediated-execution.md |
|
||||||
|
| task | SAND-WP-0015-T01 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
|
||||||
|
| task | SAND-WP-0015-T02 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
|
||||||
|
| task | SAND-WP-0015-T03 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
|
||||||
|
| task | SAND-WP-0015-T04 | wait | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
|
||||||
|
|
|
||||||
123
docs/bwrap-runtime.md
Normal file
123
docs/bwrap-runtime.md
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Bwrap runtime and private state
|
||||||
|
|
||||||
|
`ext.bwrap` can mount a standalone Python runtime selected by the owner's
|
||||||
|
extension configuration. A create/exec caller cannot select host paths or
|
||||||
|
replace the sanitized child environment.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
config:
|
||||||
|
runtime:
|
||||||
|
path: /srv/sandboxer/runtimes/rein-aharness-example
|
||||||
|
sha256: <reviewed-complete-artifact-digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
The path must be a canonical absolute directory with `pyvenv.cfg` and
|
||||||
|
`bin/python3`. Its complete tree digest includes file contents, modes, directory
|
||||||
|
entries, and internal symlinks. Absolute/escaping symlinks and special files
|
||||||
|
are refused. Missing files, added files, changed permissions, or changed
|
||||||
|
content invalidate the pin. The runtime must not overlap the source checkout
|
||||||
|
or writable sandbox workspace.
|
||||||
|
|
||||||
|
The owner mounts the artifact read-only at `/opt/sandboxer/runtime` and prepends
|
||||||
|
its `bin` directory to the child PATH. Console entrypoints must use that fixed
|
||||||
|
mount prefix. Artifacts remain an owner trust boundary: publish them in a
|
||||||
|
protected location and do not mutate them while sandboxes use them. Digest
|
||||||
|
validation occurs at creation; it is not a mechanism for making a mutable host
|
||||||
|
directory immutable to its owner.
|
||||||
|
|
||||||
|
## Building and testing the Python rein bundle
|
||||||
|
|
||||||
|
From this repository, with committed rein/llm-connect source checkouts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python scripts/build-rein-runtime.py \
|
||||||
|
--output /tmp/rein-runtime-candidate \
|
||||||
|
--rein-source ../rein-aharness \
|
||||||
|
--llm-source ../llm-connect
|
||||||
|
```
|
||||||
|
|
||||||
|
The builder creates a standalone system-Python venv, installs both packages
|
||||||
|
non-editably with their dependencies, rewrites Python console entrypoint
|
||||||
|
shebangs to the fixed mount path, and records source revisions plus resolved
|
||||||
|
package versions in `build-info.json`. It prints the resulting path/digest.
|
||||||
|
Dependency versions are resolved during this candidate build; the digest pins
|
||||||
|
the produced artifact, not a promise that another build will be bit-identical.
|
||||||
|
No interactive home, credentials, or provider login is copied into the bundle.
|
||||||
|
|
||||||
|
Prove the artifact using the returned values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python scripts/smoke-bwrap-runtime.py \
|
||||||
|
--runtime-path /tmp/rein-runtime-candidate \
|
||||||
|
--runtime-sha256 <returned-digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
The smoke uses an isolated owner extension configuration and the real broker.
|
||||||
|
It imports the rein's Claude adapter and llm-connect, runs `rein-aharness
|
||||||
|
--help`, proves the runtime rejects writes, writes private state outside the
|
||||||
|
worktree, verifies source absence and a clean Git tree, checks loopback-only
|
||||||
|
networking, and tears down. This is real CLI startup, not a model task.
|
||||||
|
|
||||||
|
The Python bundle does not package or pin the separate Claude executable.
|
||||||
|
That executable's release, startup behavior, and authentication still require
|
||||||
|
review in the eventual production runtime. No committed profile selects a
|
||||||
|
temporary `/tmp` build as its production runtime.
|
||||||
|
|
||||||
|
## Private writable state
|
||||||
|
|
||||||
|
The broker creates mode-0700 directories in the namespace's temporary root:
|
||||||
|
|
||||||
|
| Environment variable | Location |
|
||||||
|
|---|---|
|
||||||
|
| `HOME` | `/run/sandboxer/state/home` |
|
||||||
|
| `XDG_CONFIG_HOME` | `/run/sandboxer/state/config` |
|
||||||
|
| `XDG_CACHE_HOME` | `/run/sandboxer/state/cache` |
|
||||||
|
| `XDG_STATE_HOME` | `/run/sandboxer/state/data` |
|
||||||
|
| `TMPDIR` | `/run/sandboxer/state/tmp` |
|
||||||
|
|
||||||
|
These directories persist between exec requests in the same sandbox, remain
|
||||||
|
outside the Git worktree, and disappear with namespace teardown. Python user
|
||||||
|
site packages and bytecode writes are disabled. The owner's child environment
|
||||||
|
continues to carry exact consumer identity and value-free route references.
|
||||||
|
|
||||||
|
## Network and credentials remain separate gates
|
||||||
|
|
||||||
|
Bwrap now refuses a profile with `network.default: allow`, nonempty egress, or
|
||||||
|
setup secret references. The implementation currently provides only a
|
||||||
|
loopback-only network and no credential-delivery mechanism. Returning a
|
||||||
|
declared egress list as evidence would not make that list enforced or usable.
|
||||||
|
Exec credential route references continue to be labels, not credential values
|
||||||
|
or delivery grants.
|
||||||
|
|
||||||
|
On 2026-09-05, `warden route find anthropic --json` and `warden route find
|
||||||
|
claude-code --json` returned no matching workload routes. The generic OpenBao
|
||||||
|
entry is explicitly a routing template, not an executable credential lane.
|
||||||
|
The OpenRouter AppRole route belongs to the other rein and is not a substitute
|
||||||
|
for Claude authentication.
|
||||||
|
|
||||||
|
`SAND-WP-0015-T04` / `SAND-WP-0014-T05` and `GLAS-WP-0012-T02` retain the
|
||||||
|
production requirements: a concrete owner-approved Claude credential route,
|
||||||
|
delivery and revocation semantics, a pinned Claude runtime, and explicitly
|
||||||
|
enforced provider egress with negative tests. Glas local profiles remain
|
||||||
|
blocked until those requirements and the real-model acceptance pass.
|
||||||
|
|
||||||
|
## 2026-09-05 candidate evidence
|
||||||
|
|
||||||
|
Runtime SHA-256:
|
||||||
|
`4c316737ec2715936a12c4f49621a5e4be3d1f4fe4739130393f28cdda66fbd9`.
|
||||||
|
Candidate path: `/tmp/sandboxer-rein-aharness-runtime-20260905-v2`.
|
||||||
|
Source revisions: rein-aharness `1429db5ad4c83331b6375349ffde1eb13af9575b`,
|
||||||
|
llm-connect `00560945f81ba6ff1f5cacd9fe99c7fe756cc4b1`; Python 3.12.3.
|
||||||
|
|
||||||
|
Sandbox `f333fb66` passed the real CLI/import/read-only/private-state smoke.
|
||||||
|
`sys.prefix` was `/opt/sandboxer/runtime`, HOME was outside the worktree with
|
||||||
|
mode 0700, Git stayed clean, source was absent, only `lo` existed, credential
|
||||||
|
references were empty, and the workspace was removed. No model request or
|
||||||
|
credential acquisition occurred.
|
||||||
|
|
||||||
|
Final validation: `make check` passed lint and 132 tests. Authenticated owner
|
||||||
|
API smoke `223db65b` returned HTTP 200 with exact identity/stdin and complete
|
||||||
|
teardown. Follow-up runtime smoke `d4de9531` repeated the real CLI/startup
|
||||||
|
checks and proved private state survives a second exec in the same namespace.
|
||||||
|
The default unconfigured `profile.bwrap-local` still has no selected rein
|
||||||
|
bundle; candidate startup does not constitute production deployment.
|
||||||
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())
|
||||||
|
|
@ -15,6 +15,7 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sandboxer.extensions.base import SandboxExtension
|
from sandboxer.extensions.base import SandboxExtension
|
||||||
|
from sandboxer.extensions.runtime import RUNTIME_MOUNT, verified_runtime
|
||||||
from sandboxer.models import Profile
|
from sandboxer.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -72,6 +73,13 @@ 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"]
|
||||||
|
runtime = verified_runtime(self.config)
|
||||||
|
if runtime is not None:
|
||||||
|
workspace = Path(workspace_dir).resolve()
|
||||||
|
if runtime.is_relative_to(workspace) or workspace.is_relative_to(runtime):
|
||||||
|
raise ValueError("runtime and workspace must not overlap")
|
||||||
|
argv += ["--dir", "/opt", "--dir", "/opt/sandboxer"]
|
||||||
|
argv += ["--ro-bind", str(runtime), RUNTIME_MOUNT]
|
||||||
argv += ["--bind", workspace_dir, workspace_dir]
|
argv += ["--bind", workspace_dir, workspace_dir]
|
||||||
argv += ["--chdir", workspace_dir]
|
argv += ["--chdir", workspace_dir]
|
||||||
argv += [
|
argv += [
|
||||||
|
|
@ -80,6 +88,8 @@ class BwrapExtension(SandboxExtension):
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
f"{workspace_dir}/{self.control_socket_name}",
|
f"{workspace_dir}/{self.control_socket_name}",
|
||||||
]
|
]
|
||||||
|
if runtime is not None:
|
||||||
|
argv.append("--runtime")
|
||||||
return argv
|
return argv
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -102,6 +112,15 @@ class BwrapExtension(SandboxExtension):
|
||||||
def provision(
|
def provision(
|
||||||
self, profile: Profile, inputs: dict[str, str], host: str
|
self, profile: Profile, inputs: dict[str, str], host: str
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
|
if profile.network.default != "deny" or profile.network.egress:
|
||||||
|
raise ValueError("bwrap currently supports only default-deny with empty egress")
|
||||||
|
if profile.setup.secret_refs:
|
||||||
|
raise ValueError("bwrap has no setup credential delivery contract")
|
||||||
|
runtime = verified_runtime(self.config)
|
||||||
|
if runtime is not None and inputs.get("repo"):
|
||||||
|
source = Path(inputs["repo"]).resolve()
|
||||||
|
if source.is_relative_to(runtime) or runtime.is_relative_to(source):
|
||||||
|
raise ValueError("runtime and source checkout must not overlap")
|
||||||
sandbox_id = self.new_sandbox_id(inputs)
|
sandbox_id = self.new_sandbox_id(inputs)
|
||||||
workspace_dir = f"{self.base_dir}/{sandbox_id}"
|
workspace_dir = f"{self.base_dir}/{sandbox_id}"
|
||||||
Path(workspace_dir).mkdir(parents=True, exist_ok=True)
|
Path(workspace_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ def _bounded_output(value: bytes | None, limit: int) -> tuple[str, bool]:
|
||||||
return raw.decode("utf-8", errors="replace"), truncated
|
return raw.decode("utf-8", errors="replace"), truncated
|
||||||
|
|
||||||
|
|
||||||
def _run(payload: dict, workspace: Path) -> dict[str, object]:
|
def _run(
|
||||||
|
payload: dict, workspace: Path, *, runtime_enabled: bool = False
|
||||||
|
) -> dict[str, object]:
|
||||||
command = payload["command"]
|
command = payload["command"]
|
||||||
timeout_seconds = int(payload["timeout_seconds"])
|
timeout_seconds = int(payload["timeout_seconds"])
|
||||||
max_output_bytes = int(payload["max_output_bytes"])
|
max_output_bytes = int(payload["max_output_bytes"])
|
||||||
|
|
@ -31,12 +33,20 @@ def _run(payload: dict, workspace: Path) -> dict[str, object]:
|
||||||
context = payload.get("execution_context", {})
|
context = payload.get("execution_context", {})
|
||||||
stdin_text = payload.get("stdin_text")
|
stdin_text = payload.get("stdin_text")
|
||||||
child_env = {
|
child_env = {
|
||||||
"HOME": str(workspace),
|
"HOME": "/run/sandboxer/state/home",
|
||||||
|
"XDG_CONFIG_HOME": "/run/sandboxer/state/config",
|
||||||
|
"XDG_CACHE_HOME": "/run/sandboxer/state/cache",
|
||||||
|
"XDG_STATE_HOME": "/run/sandboxer/state/data",
|
||||||
|
"TMPDIR": "/run/sandboxer/state/tmp",
|
||||||
|
"PYTHONDONTWRITEBYTECODE": "1",
|
||||||
|
"PYTHONNOUSERSITE": "1",
|
||||||
"LANG": "C.UTF-8",
|
"LANG": "C.UTF-8",
|
||||||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
"SANDBOXER_CREDENTIAL_ROUTE_REFS": json.dumps(credential_refs),
|
"SANDBOXER_CREDENTIAL_ROUTE_REFS": json.dumps(credential_refs),
|
||||||
**{f"SANDBOXER_{key.upper()}": value for key, value in context.items()},
|
**{f"SANDBOXER_{key.upper()}": value for key, value in context.items()},
|
||||||
}
|
}
|
||||||
|
if runtime_enabled:
|
||||||
|
child_env["PATH"] = "/opt/sandboxer/runtime/bin:" + child_env["PATH"]
|
||||||
timed_out = False
|
timed_out = False
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
|
|
@ -73,6 +83,13 @@ def _run(payload: dict, workspace: Path) -> dict[str, object]:
|
||||||
def main() -> int:
|
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 = sys.argv[3:] == ["--runtime"]
|
||||||
|
if sys.argv[3:] and not runtime_enabled:
|
||||||
|
raise ValueError("invalid owner runtime mode")
|
||||||
|
state = Path("/run/sandboxer/state")
|
||||||
|
state.mkdir(mode=0o700)
|
||||||
|
for name in ("home", "config", "cache", "data", "tmp"):
|
||||||
|
(state / name).mkdir(mode=0o700)
|
||||||
socket_path.unlink(missing_ok=True)
|
socket_path.unlink(missing_ok=True)
|
||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
||||||
server.bind(str(socket_path))
|
server.bind(str(socket_path))
|
||||||
|
|
@ -95,7 +112,9 @@ def main() -> int:
|
||||||
try:
|
try:
|
||||||
if not chunks:
|
if not chunks:
|
||||||
raise ValueError("empty or oversized execution request")
|
raise ValueError("empty or oversized execution request")
|
||||||
response = _run(json.loads(b"".join(chunks)), workspace)
|
response = _run(
|
||||||
|
json.loads(b"".join(chunks)), workspace, runtime_enabled=runtime_enabled
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
response = {"boundary_error": str(exc)}
|
response = {"boundary_error": str(exc)}
|
||||||
connection.sendall(json.dumps(response).encode("utf-8"))
|
connection.sendall(json.dumps(response).encode("utf-8"))
|
||||||
|
|
|
||||||
52
src/sandboxer/extensions/runtime.py
Normal file
52
src/sandboxer/extensions/runtime.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Digest-pinned, owner-configured Python runtimes for bwrap."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RUNTIME_MOUNT = "/opt/sandboxer/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_digest(root: Path) -> str:
|
||||||
|
"""Hash the complete artifact, including modes and internal symlink targets."""
|
||||||
|
if not root.is_absolute() or root.resolve(strict=True) != root or not root.is_dir():
|
||||||
|
raise ValueError("runtime path must be a canonical absolute directory")
|
||||||
|
records = []
|
||||||
|
for path in sorted(root.rglob("*")):
|
||||||
|
mode = path.lstat().st_mode
|
||||||
|
if stat.S_ISLNK(mode):
|
||||||
|
target = os.readlink(path)
|
||||||
|
if Path(target).is_absolute() or not path.resolve(strict=True).is_relative_to(root):
|
||||||
|
raise ValueError("runtime symlink escapes the artifact")
|
||||||
|
value = ["symlink", target]
|
||||||
|
elif stat.S_ISREG(mode):
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
value = ["file", hashlib.file_digest(stream, "sha256").hexdigest()]
|
||||||
|
elif stat.S_ISDIR(mode):
|
||||||
|
value = ["directory"]
|
||||||
|
else:
|
||||||
|
raise ValueError("runtime contains an unsupported special file")
|
||||||
|
records.append([path.relative_to(root).as_posix(), stat.S_IMODE(mode), *value])
|
||||||
|
return hashlib.sha256(json.dumps(records, separators=(",", ":")).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verified_runtime(config: dict) -> Path | None:
|
||||||
|
"""Only owner extension configuration can select a runtime artifact."""
|
||||||
|
runtime = config.get("runtime")
|
||||||
|
if runtime is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(runtime, dict) or set(runtime) != {"path", "sha256"}:
|
||||||
|
raise ValueError("runtime requires exactly path and sha256")
|
||||||
|
if not isinstance(runtime["path"], str) or not isinstance(runtime["sha256"], str):
|
||||||
|
raise ValueError("runtime path and sha256 must be strings")
|
||||||
|
root = Path(runtime["path"])
|
||||||
|
if (not (root / "pyvenv.cfg").is_file() or not (root / "bin/python3").is_file()
|
||||||
|
or (root / ".git").exists()):
|
||||||
|
raise ValueError("runtime must be a standalone Python environment, not a checkout")
|
||||||
|
if runtime_digest(root) != runtime["sha256"]:
|
||||||
|
raise ValueError("runtime artifact digest does not match owner configuration")
|
||||||
|
return root
|
||||||
|
|
@ -188,8 +188,8 @@ class Reachability(BaseModel):
|
||||||
tunnel_via: str | None = None
|
tunnel_via: str | None = None
|
||||||
identity: str | None = None
|
identity: str | None = None
|
||||||
# Local (no-SSH-hop) descriptor — populated for same-host extensions
|
# Local (no-SSH-hop) descriptor — populated for same-host extensions
|
||||||
# like ext.bwrap. A consumer execs into the sandbox directly
|
# like ext.bwrap. Commands enter through owner-mediated execute;
|
||||||
# (e.g. `nsenter --target <pid> ...`) rather than over SSH.
|
# the pid is lifecycle metadata, not consumer setns authority.
|
||||||
pid: str | None = None
|
pid: str | None = None
|
||||||
workspace_dir: str | None = None
|
workspace_dir: str | None = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,12 +298,20 @@ def test_in_namespace_runner_uses_sanitized_environment(tmp_path) -> None:
|
||||||
]
|
]
|
||||||
assert set(child_env) == {
|
assert set(child_env) == {
|
||||||
"HOME",
|
"HOME",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
|
"XDG_CACHE_HOME",
|
||||||
|
"XDG_STATE_HOME",
|
||||||
|
"TMPDIR",
|
||||||
|
"PYTHONDONTWRITEBYTECODE",
|
||||||
|
"PYTHONNOUSERSITE",
|
||||||
"LANG",
|
"LANG",
|
||||||
"PATH",
|
"PATH",
|
||||||
"SANDBOXER_CREDENTIAL_ROUTE_REFS",
|
"SANDBOXER_CREDENTIAL_ROUTE_REFS",
|
||||||
"SANDBOXER_ACTOR",
|
"SANDBOXER_ACTOR",
|
||||||
"SANDBOXER_RUN_ID",
|
"SANDBOXER_RUN_ID",
|
||||||
}
|
}
|
||||||
|
assert child_env["HOME"] == "/run/sandboxer/state/home"
|
||||||
|
assert child_env["HOME"] != str(tmp_path)
|
||||||
assert result["stdout"] == "ok\n"
|
assert result["stdout"] == "ok\n"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
116
tests/test_bwrap_runtime.py
Normal file
116
tests/test_bwrap_runtime.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import socket
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sandboxer.extensions.bwrap import BwrapExtension
|
||||||
|
from sandboxer.extensions.bwrap_runner import _run
|
||||||
|
from sandboxer.extensions.runtime import RUNTIME_MOUNT, runtime_digest, verified_runtime
|
||||||
|
from sandboxer.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def artifact(tmp_path):
|
||||||
|
root = tmp_path / "runtime"
|
||||||
|
(root / "bin").mkdir(parents=True)
|
||||||
|
(root / "pyvenv.cfg").write_text("home = /usr/bin\n")
|
||||||
|
(root / "bin/python3").write_bytes(b"test-only-python")
|
||||||
|
(root / "bin/python3").chmod(0o755)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_mount_requires_matching_complete_digest(artifact, tmp_path):
|
||||||
|
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
|
||||||
|
assert verified_runtime(config) == artifact
|
||||||
|
argv = BwrapExtension(config)._bwrap_argv(str(tmp_path / "workspace"))
|
||||||
|
position = argv.index(str(artifact))
|
||||||
|
assert argv[position - 1:position + 2] == ["--ro-bind", str(artifact), RUNTIME_MOUNT]
|
||||||
|
assert argv[-1] == "--runtime"
|
||||||
|
(artifact / "bin/python3").write_bytes(b"modified")
|
||||||
|
with pytest.raises(ValueError, match="digest"):
|
||||||
|
BwrapExtension(config)._bwrap_argv(str(tmp_path / "workspace"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_mode_change_invalidates_runtime_pin(artifact):
|
||||||
|
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
|
||||||
|
(artifact / "bin/python3").chmod(0o777)
|
||||||
|
with pytest.raises(ValueError, match="digest"):
|
||||||
|
verified_runtime(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unexpected_file_invalidates_runtime_pin(artifact):
|
||||||
|
config = {"runtime": {"path": str(artifact), "sha256": runtime_digest(artifact)}}
|
||||||
|
(artifact / "injected.py").write_text("unexpected")
|
||||||
|
with pytest.raises(ValueError, match="digest"):
|
||||||
|
verified_runtime(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_symlinks_must_stay_inside_artifact(artifact, tmp_path):
|
||||||
|
(artifact / "python").symlink_to("bin/python3")
|
||||||
|
runtime_digest(artifact)
|
||||||
|
outside = tmp_path / "outside"
|
||||||
|
outside.write_text("host data")
|
||||||
|
(artifact / "escape").symlink_to(outside)
|
||||||
|
with pytest.raises(ValueError, match="symlink escapes"):
|
||||||
|
runtime_digest(artifact)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_rejects_socket(artifact):
|
||||||
|
with socket.socket(socket.AF_UNIX) as control:
|
||||||
|
control.bind(str(artifact / "host.sock"))
|
||||||
|
with pytest.raises(ValueError, match="special file"):
|
||||||
|
runtime_digest(artifact)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("runtime", [{}, {"path": "/tmp"}, {"path": 3, "sha256": "x"}])
|
||||||
|
def test_incomplete_runtime_config_refuses(runtime):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
verified_runtime({"runtime": runtime})
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_and_runtime_overlap_refuses_before_copy(artifact):
|
||||||
|
ext = BwrapExtension({"runtime": {"path": str(artifact),
|
||||||
|
"sha256": runtime_digest(artifact)}})
|
||||||
|
profile = Profile(id="profile.test", version="1", extension="ext.bwrap")
|
||||||
|
with (
|
||||||
|
patch("sandboxer.extensions.bwrap.shutil.copytree") as copy,
|
||||||
|
pytest.raises(ValueError, match="source checkout"),
|
||||||
|
):
|
||||||
|
ext.provision(profile, {"repo": str(artifact.parent)}, "localhost")
|
||||||
|
copy.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("network", [
|
||||||
|
{"default": "allow", "egress": []},
|
||||||
|
{"default": "deny", "egress": ["api.example.invalid:443"]},
|
||||||
|
])
|
||||||
|
def test_unsupported_network_refuses_before_provisioning(tmp_path, network):
|
||||||
|
profile = Profile(id="profile.test", version="1", extension="ext.bwrap", network=network)
|
||||||
|
base = tmp_path / "sandboxes"
|
||||||
|
with pytest.raises(ValueError, match="empty egress"):
|
||||||
|
BwrapExtension({"base_dir": str(base)}).provision(profile, {}, "localhost")
|
||||||
|
assert not base.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_path_is_not_taken_from_command_payload(tmp_path):
|
||||||
|
process = MagicMock(returncode=0)
|
||||||
|
process.communicate.return_value = (b"", b"")
|
||||||
|
payload = {"command": ["rein-aharness", "--help"], "timeout_seconds": 10,
|
||||||
|
"max_output_bytes": 100, "runtime_path": "/host/source", "env": {"HOME": "/host"}}
|
||||||
|
with patch("sandboxer.extensions.bwrap_runner.subprocess.Popen", return_value=process) as run:
|
||||||
|
_run(payload, tmp_path, runtime_enabled=True)
|
||||||
|
env = run.call_args.kwargs["env"]
|
||||||
|
assert env["PATH"].split(":")[0] == RUNTIME_MOUNT + "/bin"
|
||||||
|
assert not Path(env["HOME"]).is_relative_to(tmp_path)
|
||||||
|
assert "/host" not in str(env)
|
||||||
|
assert env["PYTHONNOUSERSITE"] == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_credentials_refuse_before_provisioning(tmp_path):
|
||||||
|
profile = Profile(id="profile.test", version="1", extension="ext.bwrap",
|
||||||
|
setup={"secret_refs": ["test-route"]})
|
||||||
|
base = tmp_path / "sandboxes"
|
||||||
|
with pytest.raises(ValueError, match="credential delivery contract"):
|
||||||
|
BwrapExtension({"base_dir": str(base)}).provision(profile, {}, "localhost")
|
||||||
|
assert not base.exists()
|
||||||
|
|
@ -8,7 +8,7 @@ status: active
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: owner-mediated-execution
|
topic_slug: owner-mediated-execution
|
||||||
created: "2026-09-04"
|
created: "2026-09-04"
|
||||||
updated: "2026-09-04"
|
updated: "2026-09-05"
|
||||||
state_hub_workstream_id: "b616d1cd-208f-5ecf-a4a0-a028396422c4"
|
state_hub_workstream_id: "b616d1cd-208f-5ecf-a4a0-a028396422c4"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -112,9 +112,16 @@ destroy the workspace, then update Glas readiness and Activity Core
|
||||||
`ACTIVITY-WP-0032-T05`. Do not trigger the production pilot before readiness
|
`ACTIVITY-WP-0032-T05`. Do not trigger the production pilot before readiness
|
||||||
changes.
|
changes.
|
||||||
|
|
||||||
This task depends on a reviewed Glas profile revision, the owner-fronted
|
This task depends on a reviewed Glas profile revision and its matching
|
||||||
`rein-openweights-openrouter-approle` read, and a deployed sand-boxer owner
|
credential/egress/runtime contract. Glas selected the Claude route first in
|
||||||
service. No credential value belongs in this workplan or State Hub.
|
GLAS-WP-0012; the earlier OpenRouter AppRole dependency applies only to the
|
||||||
|
separate open-weight profile and does not establish Claude authentication.
|
||||||
|
|
||||||
|
2026-09-05: SAND-WP-0015 implements pinned Python runtime mounts and private
|
||||||
|
namespace state, with a real rein CLI startup proof. Claude workload credential
|
||||||
|
routing, enforced provider egress, pinning/deploying the Claude executable,
|
||||||
|
and the real-model acceptance remain open in SAND-WP-0015-T04. No credential
|
||||||
|
value belongs in this workplan or State Hub.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
|
|
|
||||||
109
workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md
Normal file
109
workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
---
|
||||||
|
id: SAND-WP-0015
|
||||||
|
type: workplan
|
||||||
|
title: "Provide a pinned bwrap rein runtime and private state"
|
||||||
|
domain: infotech
|
||||||
|
repo: sand-boxer
|
||||||
|
status: blocked
|
||||||
|
owner: codex
|
||||||
|
topic_slug: bwrap-runtime-and-private-state
|
||||||
|
created: "2026-09-05"
|
||||||
|
updated: "2026-09-05"
|
||||||
|
state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Provide a pinned bwrap rein runtime and private state
|
||||||
|
|
||||||
|
Implement the runtime prerequisites returned by Glas in `GLAS-WP-0012` and
|
||||||
|
`docs/local-profile-acceptance.md`. Continue owner work `SAND-WP-0014-T05` and
|
||||||
|
live residual `GLAS-IN-0002`. Runtime startup is a separate acceptance gate
|
||||||
|
from credential delivery, enforced provider egress, and a real model task.
|
||||||
|
|
||||||
|
## Mount an owner-selected, digest-pinned Python runtime
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: SAND-WP-0015-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "93e4aad8-ba9b-5850-93d4-b5bf78ac8978"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add exact artifact verification to trusted extension configuration and mount
|
||||||
|
the standalone runtime read-only at a fixed namespace path. Reject altered
|
||||||
|
content/modes, unexpected files, escaping symlinks, special files, and source
|
||||||
|
or workspace overlap. A caller cannot choose runtime paths through exec input.
|
||||||
|
Provide a non-editable rein-aharness/llm-connect bundle builder with recorded
|
||||||
|
source revisions and resolved dependency versions.
|
||||||
|
|
||||||
|
Completed 2026-09-05. `extensions/runtime.py` verifies complete artifact
|
||||||
|
contents/modes before a read-only mount; the builder installed committed
|
||||||
|
rein-aharness `1429db5` and llm-connect `0056094` without editable source paths.
|
||||||
|
Candidate digest and source/package evidence are in `docs/bwrap-runtime.md`.
|
||||||
|
|
||||||
|
## Keep writable runtime state outside the repository
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: SAND-WP-0015-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "c8a689f8-bdf2-57a2-8521-a1c0ca44e750"
|
||||||
|
```
|
||||||
|
|
||||||
|
Create private mode-0700 HOME/config/cache/state/tmp directories within the
|
||||||
|
namespace, preserve them across exec requests, and remove them with teardown.
|
||||||
|
Keep the command environment sanitized and disable Python user-site/bytecode
|
||||||
|
writes. Fail closed on profile network/setup-credential declarations that the
|
||||||
|
current bwrap implementation cannot honor.
|
||||||
|
|
||||||
|
Completed 2026-09-05. Mode-0700 namespace HOME and XDG/TMP directories sit
|
||||||
|
outside the copied Git tree. Explicit runtime PATH selection remains owner
|
||||||
|
controlled. Unsupported egress/default-allow and setup credentials now refuse
|
||||||
|
before workspace creation; regression tests cover these boundaries.
|
||||||
|
|
||||||
|
## Verify the real rein runtime and owner regression paths
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: SAND-WP-0015-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "1a3002f6-c6a8-59d0-9d38-942028419de5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `make check`, the existing authenticated cross-request owner smoke, and a
|
||||||
|
real runtime startup smoke. Verify the actual rein CLI and adapter imports,
|
||||||
|
read-only runtime, private HOME, clean worktree, absent source, loopback-only
|
||||||
|
network, and teardown. Keep the production Glas profile blocked.
|
||||||
|
|
||||||
|
Completed 2026-09-05. `make check`: lint clean, 132 tests passed. Authenticated
|
||||||
|
owner API smoke `223db65b` returned HTTP 200 and proved exact consumer identity,
|
||||||
|
stdin delivery, absent source, loopback-only network and complete teardown.
|
||||||
|
Pinned runtime smoke `d4de9531` ran the actual rein CLI and imported its Claude
|
||||||
|
adapter, proved read-only runtime/private HOME/clean worktree, retained private
|
||||||
|
state across a second exec, and removed the workspace. No model call or
|
||||||
|
credential acquisition occurred; T04 remains waiting.
|
||||||
|
|
||||||
|
## Resolve Claude credentials, enforced egress, and production acceptance
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: SAND-WP-0015-T04
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "58817ef6-76d9-5e34-908f-c024e6c99f93"
|
||||||
|
```
|
||||||
|
|
||||||
|
The 2026-09-05 routing lookup found no concrete Anthropic/Claude workload lane.
|
||||||
|
The generic OpenBao template is not a delegable lane; the OpenRouter AppRole
|
||||||
|
belongs to another rein. Credential owner railiance-platform/OpenBao plus
|
||||||
|
rein-aharness must establish a concrete Claude-compatible route and delivery/
|
||||||
|
revocation contract before values can be requested or supplied to the runtime.
|
||||||
|
|
||||||
|
Sand-boxer must then implement the matching enforced provider egress contract
|
||||||
|
(including DNS/TLS and denied-destination proof) and pin/deploy the actual
|
||||||
|
Claude executable. No unrestricted-network or interactive-login substitute is
|
||||||
|
permitted. Review the deployed runtime/profile with Glas, run its real-rein
|
||||||
|
acceptance fixture, and update only the proven profile's readiness. No
|
||||||
|
production schedule is enabled by the runtime-startup smoke.
|
||||||
|
|
||||||
|
This task and SAND-WP-0014-T05 remain open until those gates pass. The detailed
|
||||||
|
return contract and runtime proof are in `docs/bwrap-runtime.md`; the live
|
||||||
|
cross-repo residual remains `GLAS-IN-0002`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue