From c38ee087208f0d66a4ddc36f418dd1fdd2086ebc Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 14:53:50 +0200 Subject: [PATCH] fix: validate sandbox readiness before rein handoff Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb --- SCOPE.md | 5 +- docs/harness-contract.md | 9 ++++ src/glas_harness/gateway.py | 52 ++++++++++--------- tests/test_gateway.py | 34 ++++++++++++ ...12-first-local-profile-production-proof.md | 9 ++++ ...GLAS-WP-0014-sandbox-handoff-validation.md | 12 ++++- 6 files changed, 92 insertions(+), 29 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index 1f145dd..0de4033 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -27,7 +27,8 @@ by the maturity target in `INTENT.md`. No committed profile is `ready`. successful readiness check. - A synchronous outer lifecycle: resolve, create, start, dispatch `run_task`, summarize, clean up and destroy. Manager initialization failures return - normalized creation-failure evidence. Cleanup and destruction are attempted + normalized creation-failure evidence. Non-READY owner handoffs refuse + before rein startup, and reachability preparation errors retain their stage. Cleanup and destruction are attempted after session failures, with independent cleanup/destruction outcomes even when an earlier execution error remains primary. This is not a durable recovery/retry service. @@ -67,7 +68,7 @@ universal redaction guarantee for arbitrary caller/provider data. | Surface | Evidence available | Current limit | |---|---|---| -| Catalog and adapters | 121 tests passed after GLAS-WP-0013; packaged catalog validation passed | Tests do not prove provider authentication or deployed runtime readiness | +| Catalog and adapters | 129 tests passed after GLAS-WP-0014; packaged catalog validation passed | Tests do not prove provider authentication or deployed runtime readiness | | Same-host owner boundary | Non-secret live proof: source absent, mutation denied, sandbox-only commit, private task removal and workspace destruction | Deterministic dispatch, not a real model session | | Standalone runtime and private state | Owner proof of rein CLI startup with pinned read-only Python runtime and private HOME/XDG/TMP state | Temporary proof artifact; pinned Claude executable and production selection still pending | | Provider egress | Owner extension and persisted-manager proofs of provider TLS reachability, undeclared host/direct-IP denial and cleanup | Destination control, not HTTP-path or TLS-SNI filtering; not a complete real-rein proof | diff --git a/docs/harness-contract.md b/docs/harness-contract.md index 6ad7abe..bd4898d 100644 --- a/docs/harness-contract.md +++ b/docs/harness-contract.md @@ -169,3 +169,12 @@ without them load as `unknown`, rather than implying that cleanup or reporting occurred. Consumers using older strict schemas must update to accept these fields before consuming new serialized results. No profile pins or readiness states change with this addition. + +## Sandbox handoff validation (GLAS-WP-0014) + +The synchronous create handoff must return a READY sandbox before any rein +method starts. Non-ready returns fail at sandbox_create and trigger destruction +of the returned sandbox. Errors preparing its reachability or SandboxHandle +fail at session_start, with destruction still attempted. Readiness here is the +owner lifecycle state; it does not promote a Glas profile's operational +readiness or independently prove runtime/provider health. diff --git a/src/glas_harness/gateway.py b/src/glas_harness/gateway.py index ce562c8..de856d4 100644 --- a/src/glas_harness/gateway.py +++ b/src/glas_harness/gateway.py @@ -12,7 +12,7 @@ from datetime import UTC, datetime from pydantic import ValidationError from sandboxer.core.manager import SandboxManager -from sandboxer.models import Consumer, SandboxCreateRequest, SandboxExecRequest +from sandboxer.models import Consumer, SandboxCreateRequest, SandboxExecRequest, SandboxState from glas_harness import hub from glas_harness.contract import ( @@ -124,38 +124,40 @@ def run_execution( ) ) sandbox_id = status.sandbox_id + if status.state != SandboxState.READY: + raise RuntimeError("sandbox owner did not return a ready sandbox") except Exception as exc: failure_stage = "sandbox_create" error = str(exc) raise - reachability = ( - status.reachability.model_dump(mode="json", exclude_none=True) - if status.reachability - else {} - ) - sandbox = SandboxHandle( - sandbox_id=status.sandbox_id, - host=status.host or "", - reachability=reachability, - ) - - def owner_execute(argv, input_text, timeout): - return manager.execute( - sandbox.sandbox_id, - SandboxExecRequest( - command=list(argv), - consumer=consumer, - credential_route_refs=profile.credential_route_refs, - stdin_text=input_text, - timeout_seconds=timeout, - ), + try: + reachability = ( + status.reachability.model_dump(mode="json", exclude_none=True) + if status.reachability + else {} + ) + sandbox = SandboxHandle( + sandbox_id=status.sandbox_id, + host=status.host or "", + reachability=reachability, ) - sandbox._owner_execute = owner_execute - sandbox._timeout_seconds = profile.limits.timeout_seconds or 600 + def owner_execute(argv, input_text, timeout): + return manager.execute( + sandbox.sandbox_id, + SandboxExecRequest( + command=list(argv), + consumer=consumer, + credential_route_refs=profile.credential_route_refs, + stdin_text=input_text, + timeout_seconds=timeout, + ), + ) + + sandbox._owner_execute = owner_execute + sandbox._timeout_seconds = profile.limits.timeout_seconds or 600 - try: session = selected_rein.start_session( profile, { diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 3927a0f..ca28b08 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -519,3 +519,37 @@ def test_old_result_records_have_unknown_outcomes(): assert restored.hub_report_status == 'unknown' assert restored.evidence.session_cleanup == 'unknown' assert restored.evidence.sandbox_destroy == 'unknown' + + +@pytest.mark.parametrize('state', [state for state in SandboxState if state != SandboxState.READY]) +def test_nonready_owner_handoff_never_starts_rein(state): + manager = MagicMock() + manager.create.return_value = _fake_status().model_copy(update={'state': state}) + rein = _FakeRein() + result = run_execution(_request(), catalog=_catalog_with_readiness(), rein=rein, manager=manager) + assert not result.ok + assert result.evidence.failure_stage == 'sandbox_create' + assert result.evidence.session_cleanup == 'not_attempted' + assert result.evidence.sandbox_destroy == 'succeeded' + assert rein.calls == [] + manager.execute.assert_not_called() + manager.destroy.assert_called_once_with('sbx1') + + +def test_reachability_preparation_failure_is_reported_and_destroyed(): + manager = MagicMock() + reachability = MagicMock() + reachability.model_dump.side_effect = RuntimeError('private owner detail') + manager.create.return_value = _fake_status().model_copy(update={'reachability': reachability}) + rein = _FakeRein() + with patch('glas_harness.gateway.hub.post_progress_event', return_value=True) as post: + result = run_execution(_request(report_to_hub=True), catalog=_catalog_with_readiness(), + rein=rein, manager=manager) + assert not result.ok + assert result.evidence.failure_stage == 'session_start' + assert result.tool_error == 'private owner detail' + assert result.evidence.session_cleanup == 'not_attempted' + assert result.evidence.sandbox_destroy == 'succeeded' + assert 'private owner detail' not in str(post.call_args) + assert rein.calls == [] + manager.destroy.assert_called_once_with('sbx1') diff --git a/workplans/GLAS-WP-0012-first-local-profile-production-proof.md b/workplans/GLAS-WP-0012-first-local-profile-production-proof.md index 1b55176..337bb06 100644 --- a/workplans/GLAS-WP-0012-first-local-profile-production-proof.md +++ b/workplans/GLAS-WP-0012-first-local-profile-production-proof.md @@ -249,3 +249,12 @@ production service identity and verified lane activation. Publication is not deployment. T02 stays waiting, and runtime pinning and combined acceptance remain required. No new owner inbox message or activation evidence was found in this review. + +## 2026-09-06 digest verification follow-up + +Reviewed secrets-engine `6e9c152` and `925d028`: owner corrected correlation +field handling in the flex-auth digest and refreshed fixtures. Its tests also +record an unresolved approval/native action-vocabulary and embedded-claim +digest mismatch for dual-control requests. This is owner integration evidence, +not successful live activation; SECRETS-WP-0009-T03 remains wait. No new +activation receipt was found in the Glas inbox. T02 remains waiting. diff --git a/workplans/GLAS-WP-0014-sandbox-handoff-validation.md b/workplans/GLAS-WP-0014-sandbox-handoff-validation.md index 2bed8fa..35c376b 100644 --- a/workplans/GLAS-WP-0014-sandbox-handoff-validation.md +++ b/workplans/GLAS-WP-0014-sandbox-handoff-validation.md @@ -4,7 +4,7 @@ type: workplan title: "Validate sandbox readiness before rein handoff" domain: infotech repo: glas-harness -status: active +status: finished owner: codex topic_slug: sandbox-handoff-validation created: "2026-09-06" @@ -23,7 +23,7 @@ normalized failure evidence while preserving destruction attempts. ```task id: GLAS-WP-0014-T01 -status: progress +status: done priority: high state_hub_task_id: "64b1e1ab-421b-566d-b2ee-64284446db8a" ``` @@ -35,3 +35,11 @@ the returned sandbox, and keep raw error details out of Hub evidence. Preserve successful behavior and existing cleanup status semantics. Add regression coverage for non-ready states and malformed reachability; run the full suite and catalog validation. No owner implementation or profile readiness changes. + +## Completion + +Completed 2026-09-06. All seven non-READY owner states are rejected before rein +startup and the returned sandbox is destroyed. Reachability preparation runs +inside the session_start failure boundary. Eight regressions failed before the +fix and pass afterwards; full suite 129 passed and catalog validation passed. +No residuals from this fix; real-profile acceptance remains GLAS-WP-0012.