diff --git a/CHANGELOG.md b/CHANGELOG.md index 04bc41cd3..ddb0467c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Controlled pinned-Chromium Agent Task success now binds the ChromeDriver browser root to its exact Linux `/proc//stat` start-time identity, binds every still-live PID from the already sampled bounded Chromium root-plus-descendant set before shutdown, explicitly records descendants that already exited between the `/proc` lineage snapshot and identity capture, and fails closed unless every retained exact identity terminates after session/driver shutdown; root disappearance or identity change remains an error, PID reuse counts only as termination of the original identity, and this does not attest cgroup/task ownership, processes appearing only after the sample, or OS-wide orphan absence. +- The controlled forced-close Agent Task recovery probe now binds the ChromeDriver browser root and its already sampled bounded Chromium descendant set to exact Linux PID/start-time identities before forcing the disposable context closed, and successful recovery is accepted only after session/driver shutdown proves root and sampled-set termination under one shared bounded monotonic deadline; this remains bounded compatibility evidence and does not attest cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. +- Controlled ordinary Agent Task teardown now observes the exact browser root and already sampled Chromium process set under one shared bounded monotonic deadline after session/driver shutdown, preserving separate root/process-set termination evidence and retaining `None` when no full process set was captured; this prevents sequential waiters from extending a nominal teardown budget while making no claim about cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. - Failed ordinary and forced-close Agent Task browser trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, and separate aggregate compatibility gates require cleanup proof from every trial rather than filtering unsuccessful trials out; this does not attest adversarial filesystem erasure, process termination, or arbitrary browser recovery. - Failed Manifest V3 restart trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, including reviewed ChromeDriver process-teardown `TimeoutExpired` failures; successful trials record the same cleanup fact, and an aggregate compatibility gate requires teardown proof from every MV3 trial before repeatability acceptance without retaining exception messages or command paths; this does not attest adversarial filesystem erasure, browser-process termination, or cleanup outside the controlled temporary profile. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index cdae859e5..3fe9a6925 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1448,15 +1448,22 @@ def _run_agent_task_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task browser process identity was not captured") - browser_process_terminated = _wait_for_linux_process_identity_exit( - browser_process_id, - browser_process_start_time_ticks, + full_process_set_captured = chromium_process_identities is not None + teardown_identities = ( + chromium_process_identities + if chromium_process_identities is not None + else ((browser_process_id, browser_process_start_time_ticks),) ) - chromium_process_set_terminated: bool | None = None - if chromium_process_identities is not None: - chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit( - chromium_process_identities + browser_process_terminated, observed_process_set_terminated = ( + _wait_for_linux_process_teardown( + browser_process_id, + browser_process_start_time_ticks, + teardown_identities, ) + ) + chromium_process_set_terminated: bool | None = ( + observed_process_set_terminated if full_process_set_captured else None + ) if ( browser_failure_type is not None or session_cleanup_failure_type is not None diff --git a/tests/test_agent_task_failure_process_set_termination_contract.py b/tests/test_agent_task_failure_process_set_termination_contract.py index 6fe48ea9d..46e055b41 100644 --- a/tests/test_agent_task_failure_process_set_termination_contract.py +++ b/tests/test_agent_task_failure_process_set_termination_contract.py @@ -24,8 +24,9 @@ def test_browser_pass_retains_sampled_process_set_teardown_after_failure(self) - end = runner.index("\ndef _run_agent_task_trial(", start) browser_pass = runner[start:end] for expected in ( - "if chromium_process_identities is not None:", - "chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit(", + "full_process_set_captured = chromium_process_identities is not None", + "_wait_for_linux_process_teardown(", + "observed_process_set_terminated if full_process_set_captured else None", 'failure_evidence["chromium_process_set_terminated"]', ): with self.subTest(expected=expected): @@ -91,4 +92,4 @@ def fail_before_process_set_capture( if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_agent_task_shared_teardown_deadline.py b/tests/test_agent_task_shared_teardown_deadline.py new file mode 100644 index 000000000..2a2930231 --- /dev/null +++ b/tests/test_agent_task_shared_teardown_deadline.py @@ -0,0 +1,37 @@ +"""Contract for one total post-shutdown teardown deadline in the ordinary Agent Task lane.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" + + +class AgentTaskSharedTeardownDeadlineContractTests(unittest.TestCase): + """Prevent ordinary Agent Task teardown polling from multiplying the budget.""" + + def test_browser_pass_uses_only_the_combined_teardown_waiter(self) -> None: + """Root and sampled-set evidence must share one timeout authority after shutdown.""" + + runner = RUNNER.read_text(encoding="utf-8") + start = runner.index("def _run_agent_task_browser_pass(") + end = runner.index("\ndef _run_agent_task_trial(", start) + browser_pass = runner[start:end] + + self.assertIn("_wait_for_linux_process_teardown(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_exit(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_set_exit(", browser_pass) + + shutdown = browser_pass.index("_terminate_owned_process_bounded(") + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") + failure_return = browser_pass.index( + "or driver_cleanup_failure_type is not None" + ) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) + + +if __name__ == "__main__": + unittest.main()