From 287b1eff68e9ffdfe0c18af900ba8d9b9e31a278 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Wed, 26 Aug 2026 16:21:28 -0400 Subject: [PATCH] fix(runtime): persist resume egress posture before execution --- openadapt_flow/runtime/durable/resume.py | 17 ++++++++++ tests/test_durable_runtime.py | 43 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/openadapt_flow/runtime/durable/resume.py b/openadapt_flow/runtime/durable/resume.py index b503f46b..aaaa1cd9 100644 --- a/openadapt_flow/runtime/durable/resume.py +++ b/openadapt_flow/runtime/durable/resume.py @@ -882,6 +882,23 @@ def _resume_under_lease( ContinuationCoordinator(run_dir, key=key).bind_approval( continuation_token, approved ) + resumed_screenshots_may_leave_box = bool( + getattr(replayer, "_screenshots_may_leave_box", False) + ) + if resumed_screenshots_may_leave_box and not manifest.screenshots_may_leave_box: + continuation_guard = getattr(replayer, "_durable_continuation_guard", None) + if continuation_guard is None: + raise StateDiverged( + "durable continuation lost its audit-evidence authority guard" + ) + updated_manifest = manifest.model_copy( + update={"screenshots_may_leave_box": True} + ) + store.cas_manifest(store.model_digest(manifest), updated_manifest) + # Commit the sticky privacy posture to the same continuation authority + # before retained-state checks or resumed execution can call a model. + continuation_guard.acknowledge_progress() + manifest = updated_manifest if pending.delivery_uncertainty is not None: last_linear = store.last_checkpoint() last_program = store.last_program_checkpoint() diff --git a/tests/test_durable_runtime.py b/tests/test_durable_runtime.py index 1e05f04b..abb00ffb 100644 --- a/tests/test_durable_runtime.py +++ b/tests/test_durable_runtime.py @@ -404,6 +404,49 @@ def test_resume_egress_posture_stays_sticky_across_a_second_resume(tmp_path): assert manifest.screenshots_may_leave_box is True +def test_resume_persists_new_egress_posture_before_an_early_exit(tmp_path): + """A crashed resumed leg cannot hide its newly admitted egress posture.""" + + _report, run_dir, bundle, _backend, verifier = _run_to_halt(tmp_path) + store = CheckpointStore(run_dir) + manifest = store.read_manifest() + assert manifest is not None + assert manifest.screenshots_may_leave_box is False + + verifier.refute.clear() + resumed_backend = FakeBackend() + + class CrashBeforeResumedStep(Replayer): + def _run_step(self, *args, **kwargs): + del args, kwargs + retained = store.read_manifest() + assert retained is not None + assert retained.screenshots_may_leave_box is True + raise KeyboardInterrupt("simulated early exit before resumed execution") + + with pytest.raises(KeyboardInterrupt, match="simulated early exit"): + resume( + run_dir, + CrashBeforeResumedStep( + resumed_backend, + vision=_vision_ok(), + grounder=_EgressGrounder(), + allow_model_grounding=True, + effect_verifier=verifier, + poll_interval_s=0.01, + ), + approval=_approval(bundle), + ) + + retained = store.read_manifest() + assert retained is not None + assert retained.screenshots_may_leave_box is True + assert resumed_backend.actions == [] + authority = DurableAuthority(run_dir, store).validate(retained) + assert authority.progress_digest == store.continuation_state_digest() + assert store.read_pending() is not None + + def test_durable_resume_projection_failure_keeps_fail_closed_terminal_evidence( tmp_path, ):