diff --git a/scripts/smoke-bwrap-runtime.py b/scripts/smoke-bwrap-runtime.py index e4133c8..79ddfd9 100644 --- a/scripts/smoke-bwrap-runtime.py +++ b/scripts/smoke-bwrap-runtime.py @@ -14,7 +14,25 @@ from pathlib import Path from sandboxer.extensions.bwrap import BwrapExtension from sandboxer.models import Profile -PROBE = r''' +ENTRYPOINT_PROBE = r''' +def entrypoint_started(runtime, name, timeout=15): + launcher = runtime / 'bin' / name + if launcher.is_symlink() or not launcher.is_file(): + return False + try: + result = subprocess.run([str(launcher), '--help'], capture_output=True, + timeout=timeout) + except (OSError, subprocess.TimeoutExpired): + return False + return result.returncode == 0 and b'usage:' in result.stdout.lower() + + +def package_inside_runtime(module, runtime): + origin = getattr(module, '__file__', None) + return bool(origin) and Path(origin).resolve().is_relative_to(runtime.resolve()) +''' + +PROBE = ENTRYPOINT_PROBE + r''' import json, os, subprocess, sys from pathlib import Path import rein_aharness.adapter @@ -35,9 +53,22 @@ 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 entrypoint_started(runtime, 'rein-aharness'), 'rein entrypoint did not start' +owner_entrypoints_required = sys.argv[3] == 'true' +glas_cli_started = False +packages = [rein_aharness.adapter, llm_connect] +if owner_entrypoints_required: + import glas_harness + import sandboxer + packages.extend([glas_harness, sandboxer]) + glas_cli_started = entrypoint_started(runtime, 'glas-harness') + assert glas_cli_started, 'glas entrypoint did not start' +packages_inside_runtime = all(package_inside_runtime(module, runtime) for module in packages) +assert packages_inside_runtime, 'package outside runtime' +assert json.loads(os.environ['SANDBOXER_CREDENTIAL_ROUTE_REFS']) == [] +assert not any(name in os.environ for name in + ('ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'OPENAI_API_KEY', + 'OPENROUTER_API_KEY')), 'provider credential present' claude_version = None proxy_present = False if sys.argv[2]: @@ -55,6 +86,8 @@ interfaces = [line.split(':', 1)[0].strip() assert interfaces == ['lo'] print(json.dumps({'rein_cli_started': True, 'adapter_imported': True, 'runtime_readonly': readonly, 'source_absent': True, + 'owner_entrypoints_required': owner_entrypoints_required, + 'glas_cli_started': glas_cli_started, 'packages_inside_runtime': True, 'home_outside_workspace': True, 'home_mode': '0700', 'worktree_clean': True, 'interfaces': interfaces, 'python_prefix': sys.prefix, @@ -69,6 +102,8 @@ def main() -> int: parser.add_argument("--runtime-path", required=True) parser.add_argument("--runtime-sha256", required=True) parser.add_argument("--claude-version", default="") + parser.add_argument("--require-owner-entrypoints", action="store_true", + help="Require Glas and rein startup and all four owner packages") args = parser.parse_args() with tempfile.TemporaryDirectory(prefix="sandboxer-runtime-proof-") as temporary: root = Path(temporary) @@ -87,11 +122,12 @@ def main() -> int: try: extension.wait_ready(handle) result = extension.execute( - handle, ["python3", "-c", PROBE, str(source), args.claude_version], + handle, ["python3", "-c", PROBE, str(source), args.claude_version, + str(args.require_owner_entrypoints).lower()], 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, + timeout_seconds=60, max_output_bytes=65536, ) persistence = extension.execute( handle, ["python3", "-c", diff --git a/tests/test_runtime_smoke.py b/tests/test_runtime_smoke.py new file mode 100644 index 0000000..34a7ef1 --- /dev/null +++ b/tests/test_runtime_smoke.py @@ -0,0 +1,77 @@ +import importlib.util +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +spec = importlib.util.spec_from_file_location( + "runtime_smoke", Path(__file__).parents[1] / "scripts/smoke-bwrap-runtime.py" +) +smoke = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke) +namespace = {"subprocess": subprocess, "Path": Path} +exec(smoke.ENTRYPOINT_PROBE, namespace) +started = namespace["entrypoint_started"] +inside = namespace["package_inside_runtime"] + + +def launcher(tmp_path, body): + runtime = tmp_path / "relocated runtime" + (runtime / "bin").mkdir(parents=True) + entry = runtime / "bin/glas-harness" + entry.write_text(f"#!{sys.executable}\n{body}\n") + entry.chmod(0o755) + return runtime, entry + + +def test_real_relocated_launcher_help_starts(tmp_path): + runtime, _ = launcher(tmp_path, "print('Usage: glas-harness [options]')") + assert started(runtime, "glas-harness") + + +@pytest.mark.parametrize("body", [ + "print('not command help')", + "import sys; print('usage: fixture'); sys.exit(127)", + "import sys; print('stderr-canary', file=sys.stderr); sys.exit(1)", +]) +def test_failed_or_non_help_launcher_is_not_evidence(tmp_path, body, capsys): + runtime, _ = launcher(tmp_path, body) + assert not started(runtime, "glas-harness") + assert capsys.readouterr() == ("", "") + + +def test_vanished_build_interpreter_and_missing_launcher_refuse(tmp_path): + runtime, entry = launcher(tmp_path, "print('usage: fixture')") + entry.write_text("#!/bin/sh\nexec /vanished/build/bin/python3 -c 'print(1)'\n") + assert not started(runtime, "glas-harness") + assert not started(runtime, "rein-aharness") + + +def test_launcher_timeout_is_not_startup_success(tmp_path): + runtime, _ = launcher(tmp_path, "import time; time.sleep(10)") + assert not started(runtime, "glas-harness", timeout=0.05) + + +def test_external_symlink_launcher_refuses_even_if_help_succeeds(tmp_path): + runtime, entry = launcher(tmp_path, "print('usage: fixture')") + external = tmp_path / "external" + entry.rename(external) + entry.symlink_to(external) + assert not started(runtime, "glas-harness") + + +def test_package_provenance_rejects_external_and_symlink_origins(tmp_path): + runtime = tmp_path / "runtime" + runtime.mkdir() + installed = runtime / "package.py" + installed.write_text("") + assert inside(SimpleNamespace(__file__=str(installed)), runtime) + external = tmp_path / "external.py" + external.write_text("") + installed.unlink() + installed.symlink_to(external) + assert not inside(SimpleNamespace(__file__=str(installed)), runtime) + assert not inside(SimpleNamespace(__file__=str(external)), runtime) + assert not inside(SimpleNamespace(), runtime) diff --git a/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md b/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md index 04a0f4d..7711b13 100644 --- a/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md +++ b/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md @@ -9,7 +9,7 @@ flavor: implementation owner: codex topic_slug: bwrap-runtime-and-private-state created: "2026-09-05" -updated: "2026-09-27" +updated: "2026-10-04" state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d" --- @@ -88,8 +88,8 @@ credential acquisition occurred; T04 remains waiting. ```task id: SAND-WP-0015-T04 status: wait -blocked_on: message-from:railiance-platform -blocking_reason: "Credential owner railiance-platform/OpenBao with rein-aharness must establish a Claude-compatible route and delivery/revocation contract." +blocked_on: message-from:rein-aharness +blocking_reason: "Native credential delivery/revocation succeeded in the September 27 single attempt; rein-aharness must return corrected immutable artifact/pin admission, held EUR10 reconciliation and separately authorized successful owner-runtime acceptance." priority: high state_hub_task_id: "58817ef6-76d9-5e34-908f-c024e6c99f93" ``` @@ -309,3 +309,30 @@ admission and a separately authorized successful model/tool/commit proof remain. The existing SAND-WP-0015 / REINAH-WP-0003 / SECRETS-WP-0009 records retain the work. No new task or workplan. Detailed non-secret receipts are in rein-aharness `docs/evidence/2026-09-27-metered-*.json`. + + +## 2026-10-04 offline owner startup proof preparation + +The existing no-credential smoke command now supports +`--require-owner-entrypoints`: the admitted digest-pinned runtime must start both +`rein-aharness --help` and `glas-harness --help`, and imported rein, llm-connect, +Glas and sandboxer modules must originate inside the mounted artifact. Broken or +missing launchers, failed help, timeout and external package origins cannot be +recorded as startup success. The default standalone mode remains supported. +Launcher diagnostics are not copied into the receipt. Focused tests exercise +real subprocesses, including the vanished-build-interpreter failure. + +This is preparation for a corrected immutable candidate's independent native +startup proof; it is not that candidate's receipt or owner acceptance. T04 stays +`wait`, the plan stays `blocked`, and the Sept27 artifact/liability evidence +remains intact. Replacement artifact admission and pins, explicit retained +EUR 10 reconciliation, and a separately authorized successful model/tool/commit +attempt remain with the existing owners. No credentials, provider request, +queue claim, policy activation or retry authorization is supplied here. + +2026-10-04 current blocker reconciliation: September 27 native provider/companion +apply/verify/exec and successful revocation supersede the original missing +platform route. The remaining owner return is rein-aharness corrected artifact/ +profile admission, retained EUR10 reconciliation and fresh retry authority. +T04 stays wait; no renewed trial is authorized. Full owner check: lint clean, +219 tests passed (one existing Starlette deprecation warning).