From 721b5ca8318216442f811fd67db217522577d7f6 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:57:57 +0100 Subject: [PATCH] fix(runtime): retain malformed launch publications safely --- src/agent_runtime/process_resources.py | 3 ++- tests/test_wave3_launch_cost_lifecycle.py | 25 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/agent_runtime/process_resources.py b/src/agent_runtime/process_resources.py index 9c4b9f053..ef244730f 100644 --- a/src/agent_runtime/process_resources.py +++ b/src/agent_runtime/process_resources.py @@ -438,7 +438,8 @@ def retire_launch(launch, containment_id, *, job=None): published = json.loads(path.read_text()) except FileNotFoundError: return False - if (published.get("launch") != launch.to_dict() + if (not isinstance(published, dict) + or published.get("launch") != launch.to_dict() or published.get("containment_id") != containment_id or published.get("job") != (job.to_dict() if job else None)): return False diff --git a/tests/test_wave3_launch_cost_lifecycle.py b/tests/test_wave3_launch_cost_lifecycle.py index acfb9bc43..55cf92960 100644 --- a/tests/test_wave3_launch_cost_lifecycle.py +++ b/tests/test_wave3_launch_cost_lifecycle.py @@ -94,6 +94,31 @@ async def test_retired_publication_cannot_replay_bound_reservation(workspace): tool_execution._active_workspace.reset(token) +@pytest.mark.parametrize('publication', [[], None, 'malformed']) +def test_nonobject_publication_cannot_be_retired(workspace, publication): + resource, rec = seed(workspace, status='done') + path = resources.launch_path(resource.generation) + path.write_text(json.dumps(publication)) + launch = identities.ProcessLaunchResource.from_dict(rec['launch_resource']) + assert not resources.retire_launch(launch, resource.containment_id, job=resource) + assert json.loads(path.read_text()) == publication + + +async def test_corrupt_publication_retirement_preserves_command_result(workspace, monkeypatch): + attach = resources.attach_containment_processes + paths = [] + def corrupt_after_attachment(launch, containment_id): + observed = attach(launch, containment_id) + path = resources.launch_path(launch.generation) + path.write_text('[]') + paths.append(path) + return observed + monkeypatch.setattr(resources, 'attach_containment_processes', corrupt_after_attachment) + _, result = await dispatch(authority(workspace), 'bash', 'printf completed') + assert result['exit_code'] == 0 and result['output'] == 'completed', result + assert paths[0].read_text() == '[]' + + @pytest.mark.parametrize('status,followed_up,old,removed', [ ('running', True, True, False), ('done', False, True, False), ('done', True, False, False), ('done', True, True, True), ('failed', True, True, True),