fix: validate sandbox readiness before rein handoff
Some checks failed
ci / validate (push) Has been cancelled
Some checks failed
ci / validate (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
31dd5aaca8
commit
c38ee08720
6 changed files with 92 additions and 29 deletions
5
SCOPE.md
5
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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue